Merge branch 'stable-2.10' into stable-2.11

* stable-2.10:
  Update version to 2.10.3

Conflicts:
	VERSION
	gerrit-extension-api/pom.xml
	gerrit-plugin-api/pom.xml
	gerrit-plugin-archetype/pom.xml
	gerrit-plugin-gwt-archetype/pom.xml
	gerrit-plugin-gwtui/pom.xml
	gerrit-plugin-js-archetype/pom.xml
	gerrit-war/pom.xml

Change-Id: Ieab4dc23641132cf11a84a30fb8e5af1434112b3
diff --git a/.buckconfig b/.buckconfig
index 43bfa5e..e4a19f1 100644
--- a/.buckconfig
+++ b/.buckconfig
@@ -6,11 +6,12 @@
   war_deploy = //tools/maven:war_deploy
   war_install = //tools/maven:war_install
   chrome = //:chrome
-  docs = //Documentation:html
+  docs = //Documentation:searchfree
   firefox = //:firefox
   gerrit = //:gerrit
   release = //:release
   safari = //:safari
+  soyc = //gerrit-gwtui:ui_soyc
   withdocs = //:withdocs
 
 [buildfile]
diff --git a/.buckversion b/.buckversion
index a0c6bc2..9c09744 100644
--- a/.buckversion
+++ b/.buckversion
@@ -1 +1 @@
-0fe4569e871fd6588f7cbfb4b1d4a14baa791a9f
+79d36de9f5284f6e833cca81867d6088a25685fb
diff --git a/.gitignore b/.gitignore
index fec4747..c30cee6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
+/.apt_generated
 /.classpath
+/.factorypath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
 /.settings/org.eclipse.m2e.core.prefs
@@ -13,6 +15,7 @@
 /.buckd
 /buck-cache
 /buck-out
+/extras
 /local.properties
 *.pyc
 /gwt-unitCache
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 0000000..75aea08
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,21 @@
+Adrian Görler <adrian.goerler@sap.com>        Adrian Goerler <adrian.goerler@sap.com>
+Alex Ryazantsev <alex.ryazantsev@gmail.com>   alex <alex.ryazantsev@gmail.com>
+Alex Ryazantsev <alex.ryazantsev@gmail.com>   alex.ryazantsev <alex.ryazantsev@gmail.com>
+Carlos Eduardo Baldacin <carloseduardo.baldacin@sonyericsson.com> carloseduardo.baldacin <carloseduardo.baldacin@sonyericsson.com>
+Deniz Türkoglu <deniz@spotify.com>            Deniz Turkoglu <deniz@spotify.com>
+Deniz Türkoglu <deniz@spotify.com>            Deniz Türkoglu <deniz@spotify.com>
+Edwin Kempin <edwin.kempin@sap.com>           Edwin Kempin <edwin.kempin@gmail.com>
+Hugo Arès <hugo.ares@ericsson.com>            Hugo Ares <hugo.ares@ericsson.com>
+Jason Huntley <jhuntley@houghtonassociates.com> jhuntley <jhuntley@houghtonassociates.com>
+Johan Björk <jbjoerk@gmail.com>               Johan Bjork <phb@spotify.com>
+Lincoln Oliveira Campos Do Nascimento <lincoln.oliveiracamposdonascimento@sonyericsson.com> lincoln <lincoln.oliveiracamposdonascimento@sonyericsson.com>
+Mônica Dionísio <monica.dionisio@sonyericsson.com> monica.dionisio <monica.dionisio@sonyericsson.com>
+Peter Jönsson <peter.joensson@gmail.com>      Peter Jönsson <peter.joensson@gmail.com>
+Rafael Rabelo Silva <rafael.rabelosilva@sonyericsson.com> rafael.rabelosilva <rafael.rabelosilva@sonyericsson.com>
+Saša Živkov <sasa.zivkov@sap.com>             Sasa Zivkov <sasa.zivkov@sap.com>
+Saša Živkov <sasa.zivkov@sap.com>             Saša Živkov <zivkov@gmail.com>
+Shawn Pearce <sop@google.com>                 Shawn O. Pearce <sop@google.com>
+Tomas Westling <thomas.westling@sonyericsson.com> thomas.westling <thomas.westling@sonyericsson.com>
+Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>  Ulrik Sjolin <ulrik.sjolin@gmail.com>
+Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>  Ulrik Sjolin <ulrik.sjolin@sonyericsson.com>
+Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>  Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>
diff --git a/.pydevproject b/.pydevproject
index be43141..40e9f40 100644
--- a/.pydevproject
+++ b/.pydevproject
@@ -1,7 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<?eclipse-pydev version="1.0"?>
-
-<pydev_project>
+<?eclipse-pydev version="1.0"?><pydev_project>
 <pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
-<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.6.5</pydev_property>
+<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
 </pydev_project>
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index 2a585e4..0fa494d 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,5 @@
 eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled
 org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
 org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
 org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
@@ -14,11 +15,11 @@
 org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
 org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
 org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
-org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.emptyStatement=warning
 org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
-org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
 org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled
-org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore
+org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
 org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
 org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
 org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
@@ -32,27 +33,28 @@
 org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
 org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore
 org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
-org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore
-org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=warning
 org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
 org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
 org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
 org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
 org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
 org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning
 org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
 org.eclipse.jdt.core.compiler.problem.nullReference=warning
 org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
 org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning
 org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
 org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
-org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
 org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore
 org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore
 org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
 org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
-org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
-org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=warning
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=warning
 org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore
 org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
 org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
@@ -60,6 +62,7 @@
 org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
 org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled
 org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled
 org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
 org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
 org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled
@@ -68,9 +71,9 @@
 org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
 org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
 org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
-org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
 org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
-org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
 org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
 org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
 org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
@@ -78,13 +81,15 @@
 org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
 org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
 org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore
-org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameter=warning
 org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
 org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
 org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
 org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore
 org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
 org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.processAnnotations=enabled
 org.eclipse.jdt.core.compiler.source=1.7
 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
diff --git a/.watchmanconfig b/.watchmanconfig
new file mode 100644
index 0000000..b1869ba
--- /dev/null
+++ b/.watchmanconfig
@@ -0,0 +1,8 @@
+{
+  "ignore_dirs": [
+    "buck-out"
+  ],
+  "ignore_vcs": [
+    ".git"
+  ]
+}
diff --git a/Documentation/BUCK b/Documentation/BUCK
index f070d7e..48a6525 100644
--- a/Documentation/BUCK
+++ b/Documentation/BUCK
@@ -6,30 +6,25 @@
 MAIN = ['//gerrit-pgm:pgm', '//gerrit-gwtui:ui_module']
 SRCS = glob(['*.txt'], excludes = ['licenses.txt'])
 
-genrule(
+genasciidoc(
   name = 'html',
-  cmd = 'cd $TMP;' +
-    'mkdir -p %s/images;' % DOC_DIR +
-    'unzip -q $(location %s) -d %s/;'
-    % (':generate_html', DOC_DIR) +
-    'for s in $SRCS;do ln -s $s %s;done;' % DOC_DIR +
-    'mv %s/*.{jpg,png} %s/images;' % (DOC_DIR, DOC_DIR) +
-    'cp $(location %s) LICENSES.txt;' % ':licenses.txt' +
-    'zip -qr $OUT *',
-  srcs = glob([
-      'images/*.jpg',
-      'images/*.png',
-    ]) + ['doc.css'],
   out = 'html.zip',
+  docdir = DOC_DIR,
+  srcs = SRCS + [':licenses.txt'],
+  attributes = documentation_attributes(git_describe()),
+  backend = 'html5',
   visibility = ['PUBLIC'],
 )
 
 genasciidoc(
-  name = 'generate_html',
+  name = 'searchfree',
+  out = 'searchfree.zip',
+  docdir = DOC_DIR,
   srcs = SRCS + [':licenses.txt'],
   attributes = documentation_attributes(git_describe()),
   backend = 'html5',
-  out = 'only_html.zip',
+  searchbox = False,
+  visibility = ['PUBLIC'],
 )
 
 genrule(
@@ -39,6 +34,13 @@
   out = 'licenses.txt',
 )
 
+genrule(
+  name = 'doc.css',
+  srcs = ['doc.css.in'],
+  cmd = 'cp $SRCS $OUT',
+  out = 'doc.css',
+)
+
 python_binary(
   name = 'gen_licenses',
   main = 'gen_licenses.py',
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index fce3f76..ff252f7 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -210,8 +210,8 @@
 
 Permissions can be set on a single reference name to match one
 branch (e.g. `refs/heads/master`), or on a reference namespace
-(e.g. `refs/heads/*`) to match any branch starting with that
-prefix. So a permission with `refs/heads/*` will match
+(e.g. `+refs/heads/*+`) to match any branch starting with that
+prefix. So a permission with `+refs/heads/*+` will match
 `refs/heads/master` and `refs/heads/experimental`, etc.
 
 Reference names can also be described with a regular expression
@@ -227,7 +227,7 @@
 References can have the current user name automatically included,
 creating dynamic access controls that change to match the currently
 logged in user.  For example to provide a personal sandbox space
-to all developers, `refs/heads/sandbox/${username}/*` allowing
+to all developers, `+refs/heads/sandbox/${username}/*+` allowing
 the user 'joe' to use 'refs/heads/sandbox/joe/foo'.
 
 When evaluating a reference-level access right, Gerrit will use
@@ -405,19 +405,19 @@
 
 ==== refs/publish/*
 
-`refs/publish/*` is an alternative name to `refs/for/*` when pushing new changes
+`+refs/publish/*+` is an alternative name to `+refs/for/*+` when pushing new changes
 and patch sets.
 
 
 ==== refs/drafts/*
 
-Push to `refs/drafts/*` creates a change like push to `refs/for/*`, except the
+Push to `+refs/drafts/*+` creates a change like push to `+refs/for/*+`, except the
 resulting change remains hidden from public review.  You then have the option
 of adding individual reviewers before making the change public to all.  The
 change page will have a 'Publish' button which allows you to convert individual
 draft patch sets of a change into public patch sets for review.
 
-To block push permission to `refs/drafts/*` the following permission rule can
+To block push permission to `+refs/drafts/*+` the following permission rule can
 be configured:
 
 ====
@@ -464,18 +464,18 @@
 as well as bypass review for new commits on that branch.
 
 To push lightweight (non-annotated) tags, grant
-`Create Reference` for reference name `refs/tags/*`, as lightweight
+`Create Reference` for reference name `+refs/tags/*+`, as lightweight
 tags are implemented just like branches in Git.
 
 For example, to grant the possibility to create new branches under the
 namespace `foo`, you have to grant this permission on
-`refs/heads/foo/*` for the group that should have it.
+`+refs/heads/foo/*+` for the group that should have it.
 Finally, if you plan to grant each user a personal namespace in
 where they are free to create as many branches as they wish, you
 should grant the create reference permission so it's possible
 to create new branches. This is done by using the special
 `${username}` keyword in the reference pattern, e.g.
-`refs/heads/sandbox/${username}/*`. If you do, it's also recommended
+`+refs/heads/sandbox/${username}/*+`. If you do, it's also recommended
 you grant the users the push force permission to be able to clean up
 stale branches.
 
@@ -547,7 +547,7 @@
 Ownership over a particular branch subspace may be delegated by
 entering a branch pattern.  To delegate control over all branches
 that begin with `qa/` to the QA group, add `Owner` category
-for reference `refs/heads/qa/*`.  Members of the QA group can
+for reference `+refs/heads/qa/*+`.  Members of the QA group can
 further refine access, but only for references that begin with
 `refs/heads/qa/`. See <<project_owners,project owners>> to find
 out more about this role.
@@ -600,15 +600,15 @@
 have the `Read` access granted to upload a change.
 
 For an open source, public Gerrit installation, it is common to
-grant `Read` and `Push` for `refs/for/refs/heads/*`
+grant `Read` and `Push` for `+refs/for/refs/heads/*+`
 to `Registered Users` in the `All-Projects` ACL.  For more
 private installations, its common to simply grant `Read` and
-`Push` for `refs/for/refs/heads/*` to all users of a project.
+`Push` for `+refs/for/refs/heads/*+` to all users of a project.
 
 * Force option
 +
 The force option has no function when granted to a branch in the
-`refs/for/refs/heads/*` namespace.
+`+refs/for/refs/heads/*+` namespace.
 
 
 [[category_push_merge]]
@@ -661,11 +661,11 @@
 
 To push lightweight (non annotated) tags, grant
 <<category_create,`Create Reference`>> for reference name
-`refs/tags/*`, as lightweight tags are implemented just like
+`+refs/tags/*+`, as lightweight tags are implemented just like
 branches in Git.
 
 To delete or overwrite an existing tag, grant `Push` with the force
-option enabled for reference name `refs/tags/*`, as deleting a tag
+option enabled for reference name `+refs/tags/*+`, as deleting a tag
 requires the same permission as deleting a branch.
 
 
@@ -768,8 +768,7 @@
 [[category_submit]]
 === Submit
 
-This category permits users to push the `Submit Patch Set n` button
-on the web UI.
+This category permits users to submit changes.
 
 Submitting a change causes it to be merged into the destination
 branch as soon as possible, making it a permanent part of the
@@ -838,6 +837,17 @@
 edited on open changes.
 
 
+[[category_edit_hashtags]]
+=== Edit Hashtags
+
+This category permits users to add or remove hashtags on a change that
+is uploaded for review.
+
+The change owner, branch owners, project owners, and site administrators
+can always edit or remove hashtags (even without having the `Edit Hashtags`
+access right assigned).
+
+
 [[example_roles]]
 == Examples of typical roles in a project
 
@@ -897,7 +907,7 @@
 * xref:category_forge_author[`Forge Author Identity`] to 'refs/heads/*'
 * link:config-labels.html#label_Code-Review[`Label: Code-Review`] with range '-2' to '+2' for 'refs/heads/*'
 * link:config-labels.html#label_Verified[`Label: Verified`] with range '-1' to '+1' for 'refs/heads/*'
-* xref:category_submit[`Submit`]
+* xref:category_submit[`Submit`] on 'refs/heads/*'
 
 If the project is small or the developers are seasoned it might make
 sense to give them the freedom to push commits directly to a branch.
@@ -1016,7 +1026,7 @@
 
 == Enforcing site wide access policies
 
-By granting the <<category_owner,`Owner`>> access right on the `refs/*` to a
+By granting the <<category_owner,`Owner`>> access right on the `+refs/*+` to a
 group, Gerrit administrators can delegate the responsibility of maintaining
 access rights for that project to that group.
 
@@ -1152,7 +1162,8 @@
 [[capability_accessDatabase]]
 === Access Database
 
-Allow users to access the database using the `gsql` command.
+Allow users to access the database using the `gsql` command, and view code
+review metadata refs in repositories.
 
 
 [[capability_administrateServer]]
@@ -1164,6 +1175,20 @@
 capabilities granted to them automatically.
 
 
+[[capability_batchChangesLimit]]
+=== Batch Changes Limit
+
+Allow site administrators to configure the batch changes limit for
+users to override the system config
+link:config-gerrit.html#receive.maxBatchChanges['receive.maxBatchChanges'].
+
+Administrators can add a global block to `All-Projects` with group(s)
+that should have different limits.
+
+When applying a batch changes limit to a user the largest value
+granted by any of their groups is used. 0 means no limit.
+
+
 [[capability_createAccount]]
 === Create Account
 
@@ -1211,13 +1236,6 @@
 you need the <<capability_viewCaches,view caches capability>>.
 
 
-[[capability_generateHttpPassword]]
-=== Generate HTTP Password
-
-Allow the user to generate HTTP passwords for other users.  Typically this would
-be assigned to a non-interactive users group.
-
-
 [[capability_kill]]
 === Kill Task
 
@@ -1226,6 +1244,12 @@
 a replication task or a user initiated task such as an upload-pack or
 receive-pack.
 
+[[capability_modifyAccount]]
+=== Modify Account
+
+Allow to link:cmd-set-account.html[modify accounts over the ssh prompt].
+This capability allows the granted group members to modify any user account
+setting.
 
 [[capability_priority]]
 === Priority
diff --git a/Documentation/asciidoc.defs b/Documentation/asciidoc.defs
index 7adf265..2520c74 100644
--- a/Documentation/asciidoc.defs
+++ b/Documentation/asciidoc.defs
@@ -12,18 +12,20 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-def genasciidoc(
+def genasciidoc_htmlonly(
     name,
     out,
     srcs = [],
     attributes = [],
     backend = None,
+    searchbox = True,
     visibility = []):
-  EXPN = '.expn'
+  EXPN = '.' + name + '_expn'
 
   asciidoc = [
       '$(exe //lib/asciidoctor:asciidoc)',
       '-z', '$OUT',
+      '--base-dir', '$SRCDIR',
       '--tmp', '$TMP',
       '--in-ext', '".txt%s"' % EXPN,
       '--out-ext', '".html"',
@@ -33,7 +35,7 @@
   for attribute in attributes:
     asciidoc.extend(['-a', attribute])
   asciidoc.append('$SRCS')
-  newsrcs = ["doc.css"]
+  newsrcs = [":doc.css"]
   for src in srcs:
     fn = src
     # We have two cases: regular source files and generated files.
@@ -50,14 +52,16 @@
 
     genrule(
       name = ex,
-      cmd = '$(exe :replace_macros) --suffix=' + EXPN +
-            ' -s ' + passed_src +
-            ' -o $OUT',
+      cmd = '$(exe :replace_macros) --suffix="%s"' % EXPN +
+        ' -s ' + passed_src + ' -o $OUT' +
+        (' --searchbox' if searchbox else ' --no-searchbox'),
       srcs = srcs,
       out = ex,
     )
 
-    asciidoc.append('$(location :%s)' % ex)
+    # The new AsciiDoctor requires both the css file and include files are under
+    # the same directory. Luckily Buck allows us to use :target as SRCS now.
+    newsrcs.append(':%s' % ex)
 
   genrule(
     name = name,
@@ -66,3 +70,41 @@
     out = out,
     visibility = visibility,
   )
+
+def genasciidoc(
+    name,
+    out,
+    docdir,
+    srcs = [],
+    attributes = [],
+    backend = None,
+    searchbox = True,
+    visibility = []):
+  SUFFIX = '_htmlonly'
+
+  genasciidoc_htmlonly(
+    name = name + SUFFIX,
+    srcs = srcs,
+    attributes = attributes,
+    backend = backend,
+    searchbox = searchbox,
+    out = name + SUFFIX + '.zip',
+  )
+
+  genrule(
+    name = name,
+    cmd = 'cd $TMP;' +
+      'mkdir -p %s/images;' % docdir +
+      'unzip -q $(location %s) -d %s/;'
+      % (':' + name + SUFFIX, docdir) +
+      'for s in $SRCS;do ln -s $s %s;done;' % docdir +
+      'mv %s/*.{jpg,png} %s/images;' % (docdir, docdir) +
+      'cp $(location %s) LICENSES.txt;' % ':licenses.txt' +
+      'zip -qr $OUT *',
+    srcs = glob([
+        'images/*.jpg',
+        'images/*.png',
+      ]) + [':doc.css'],
+    out = out,
+    visibility = visibility,
+  )
diff --git a/Documentation/cmd-close-connection.txt b/Documentation/cmd-close-connection.txt
new file mode 100644
index 0000000..3314326
--- /dev/null
+++ b/Documentation/cmd-close-connection.txt
@@ -0,0 +1,38 @@
+= gerrit close-connection
+
+== NAME
+gerrit close-connection - Close the specified SSH connection
+
+== SYNOPSIS
+--
+'ssh' -p <port> <host> 'gerrit close-connection' <SESSION_ID>
+   [--wait]
+--
+
+== DESCRIPTION
+Close an SSH connection.
+
+The connection closing is done asynchronously by default. Use `--wait` option to
+wait for connection to close.
+
+An error message will be displayed if no connection with the specified session
+ID is found.
+
+== ACCESS
+Caller must be a member of the privileged 'Administrators' group.
+
+== SCRIPTING
+Intended for interactive use only.
+
+OPTIONS
+-------
+
+`--wait`
+:	Wait for connection to close before exiting.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt
index d747852..31b4538 100644
--- a/Documentation/cmd-create-project.txt
+++ b/Documentation/cmd-create-project.txt
@@ -15,6 +15,7 @@
   [--use-contributor-agreements | --ca]
   [--use-signed-off-by | --so]
   [--use-content-merge]
+  [--create-new-change-for-all-not-in-target]
   [--require-change-id | --id]
   [[--branch <REF> | -b <REF>] ...]
   [--empty-commit]
@@ -134,6 +135,15 @@
 	from either the author or the uploader in the commit message.
 	Disabled by default.
 
+--create-new-change-for-all-not-in-target::
+--ncfa:
+	If enabled, a new change is created for every commit that is not in
+	the target branch. If the pushed commit is a merge commit, this flag is
+	ignored for that push. To avoid accidental creation of a large number
+	of open changes, this option also does not accept merge commits in the
+	commit chain.
+	Disabled by default.
+
 --require-change-id::
 --id::
 	Require a valid link:user-changeid.html[Change-Id] footer
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index d4d6a579..7c664ac 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -19,7 +19,7 @@
 === [[client_commands]]Commands
 
 link:cmd-cherry-pick.html[gerrit-cherry-pick]::
-  Download and cherry-pick one or more changes (commits).
+	Download and cherry-pick one or more changes (commits).
 
 === [[client_hooks]]Hooks
 
@@ -28,7 +28,7 @@
 server.
 
 link:cmd-hook-commit-msg.html[commit-msg]::
-  Automatically generate `Change-Id: ` tags in commit messages.
+	Automatically generate `Change-Id: ` tags in commit messages.
 
 
 == Server
@@ -51,6 +51,9 @@
 link:cmd-ban-commit.html[gerrit ban-commit]::
 	Bans a commit from a project's repository.
 
+link:cmd-create-branch.html[gerrit create-branch]::
+	Create a new project branch.
+
 link:cmd-ls-groups.html[gerrit ls-groups]::
 	List groups visible to the caller.
 
@@ -60,57 +63,51 @@
 link:cmd-ls-projects.html[gerrit ls-projects]::
 	List projects visible to the caller.
 
-link:cmd-rename-group.html[gerrit rename-group]::
-	Rename an account group.
-
-link:cmd-set-reviewers.html[gerrit set-reviewers]::
-        Add or remove reviewers on a change.
-
 link:cmd-query.html[gerrit query]::
 	Query the change database.
 
 'gerrit receive-pack'::
 	'Deprecated alias for `git receive-pack`.'
 
+link:cmd-rename-group.html[gerrit rename-group]::
+	Rename an account group.
+
 link:cmd-review.html[gerrit review]::
 	Verify, approve and/or submit a patch set from the command line.
 
+link:cmd-set-reviewers.html[gerrit set-reviewers]::
+	Add or remove reviewers on a change.
+
 link:cmd-stream-events.html[gerrit stream-events]::
 	Monitor events occurring in real time.
 
 link:cmd-version.html[gerrit version]::
 	Show the currently executing version of Gerrit.
 
-git upload-pack::
-	Standard Git server side command for client side `git fetch`.
-
 link:cmd-receive-pack.html[git receive-pack]::
 	Standard Git server side command for client side `git push`.
 +
 Also implements the magic associated with uploading commits for
 review.  See link:user-upload.html#push_create[Creating Changes].
 
-link:cmd-create-branch.html[gerrit create-branch]::
-	Create a new project branch.
+git upload-pack::
+	Standard Git server side command for client side `git fetch`.
 
 [[admin_commands]]Administrator Commands
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
+link:cmd-close-connection.html[gerrit close-connection]::
+	Close the specified SSH connection.
+
 link:cmd-create-account.html[gerrit create-account]::
 	Create a new user account.
 
-link:cmd-set-account.html[gerrit set-account]::
-	Change an account's settings.
-
 link:cmd-create-group.html[gerrit create-group]::
 	Create a new account group.
 
 link:cmd-create-project.html[gerrit create-project]::
 	Create a new project and associated Git repository.
 
-link:cmd-set-project.html[gerrit set-project]::
-    Change a project's settings.
-
 link:cmd-flush-caches.html[gerrit flush-caches]::
 	Flush some/all server caches from memory.
 
@@ -120,15 +117,48 @@
 link:cmd-gsql.html[gerrit gsql]::
 	Administrative interface to active database.
 
-link:cmd-set-members.html[gerrit set-members]::
-	Set group members.
+link:cmd-logging-ls-level.html[gerrit logging ls-level]::
+	List loggers and their logging level.
 
-link:cmd-set-project-parent.html[gerrit set-project-parent]::
-	Change the project permissions are inherited from.
+link:cmd-logging-set-level.html[gerrit logging set-level]::
+	Set the logging level of loggers.
 
 link:cmd-ls-user-refs.html[gerrit ls-user-refs]::
 	Lists refs visible for a specified user.
 
+link:cmd-plugin-install.html[gerrit plugin add]::
+	Alias for 'gerrit plugin install'.
+
+link:cmd-plugin-enable.html[gerrit plugin enable]::
+	Enable plugins.
+
+link:cmd-plugin-install.html[gerrit plugin install]::
+	Install/Add a plugin.
+
+link:cmd-plugin-ls.html[gerrit plugin ls]::
+	List the installed plugins.
+
+link:cmd-plugin-reload.html[gerrit plugin reload]::
+	Reload/Restart plugins.
+
+link:cmd-plugin-remove.html[gerrit plugin remove]::
+	Disable plugins.
+
+link:cmd-plugin-remove.html[gerrit plugin rm]::
+	Alias for 'gerrit plugin remove'.
+
+link:cmd-set-account.html[gerrit set-account]::
+	Change an account's settings.
+
+link:cmd-set-members.html[gerrit set-members]::
+	Set group members.
+
+link:cmd-set-project.html[gerrit set-project]::
+	Change a project's settings.
+
+link:cmd-set-project-parent.html[gerrit set-project-parent]::
+	Change the project permissions are inherited from.
+
 link:cmd-show-caches.html[gerrit show-caches]::
 	Display current cache statistics.
 
@@ -138,27 +168,6 @@
 link:cmd-show-queue.html[gerrit show-queue]::
 	Display the background work queues, including replication.
 
-link:cmd-plugin-install.html[gerrit plugin add]::
-    Alias for 'gerrit plugin install'.
-
-link:cmd-plugin-enable.html[gerrit plugin enable]::
-    Enable plugins.
-
-link:cmd-plugin-install.html[gerrit plugin install]::
-    Install/Add a plugin.
-
-link:cmd-plugin-ls.html[gerrit plugin ls]::
-    List the installed plugins.
-
-link:cmd-plugin-reload.html[gerrit plugin reload]::
-    Reload/Restart plugins.
-
-link:cmd-plugin-remove.html[gerrit plugin remove]::
-    Disable plugins.
-
-link:cmd-plugin-remove.html[gerrit plugin rm]::
-    Alias for 'gerrit plugin remove'.
-
 link:cmd-test-submit-rule.html[gerrit test-submit rule]::
 	Test prolog submit rules.
 
diff --git a/Documentation/cmd-logging-ls-level.txt b/Documentation/cmd-logging-ls-level.txt
new file mode 100644
index 0000000..c59dc3f
--- /dev/null
+++ b/Documentation/cmd-logging-ls-level.txt
@@ -0,0 +1,43 @@
+= gerrit logging ls-level
+
+== NAME
+gerrit logging ls-level - view the logging level
+
+gerrit logging ls - view the logging level
+
+== SYNOPSIS
+--
+'ssh' -p <port> <host> 'gerrit logging ls-level | ls'
+  <NAME>
+--
+
+== DESCRIPTION
+View the logging level of specified loggers.
+
+== Options
+<NAME>::
+  Display the loggers which contain the input argument in their name. If this
+  argument is not provided, all loggers will be printed.
+
+== ACCESS
+Caller must have the ADMINISTRATE_SERVER capability.
+
+== Examples
+
+View the logging level of the loggers in the package com.google:
+=====
+    $ssh -p 29418 review.example.com gerrit logging ls-level \
+     com.google.
+=====
+
+View the logging level of every logger
+=====
+    $ssh -p 29418 review.example.com gerrit logging ls-level
+=====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/cmd-logging-set-level.txt b/Documentation/cmd-logging-set-level.txt
new file mode 100644
index 0000000..38062cb
--- /dev/null
+++ b/Documentation/cmd-logging-set-level.txt
@@ -0,0 +1,51 @@
+= gerrit logging set-level
+
+== NAME
+gerrit logging set-level - set the logging level
+
+gerrit logging set - set the logging level
+
+== SYNOPSIS
+--
+'ssh' -p <port> <host> 'gerrit logging set-level | set'
+  <LEVEL>
+  <NAME>
+--
+
+== DESCRIPTION
+Set the logging level of specified loggers.
+
+== Options
+<LEVEL>::
+  Required; logging level for which the loggers should be set.
+  'reset' can be used to revert all loggers back to their level
+  at deployment time.
+
+<NAME>::
+  Set the level of the loggers which contain the input argument in their name.
+  If this argument is not provided, all loggers will have their level changed.
+  Note that this argument has no effect if 'reset' is passed in LEVEL.
+
+== ACCESS
+Caller must have the ADMINISTRATE_SERVER capability.
+
+== Examples
+
+Change the logging level of the loggers in the package com.google to DEBUG.
+=====
+    $ssh -p 29418 review.example.com gerrit logging set-level \
+     debug com.google.
+=====
+
+Reset the logging level of every logger to what they were at deployment time.
+=====
+    $ssh -p 29418 review.example.com gerrit logging set-level \
+     reset
+=====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/cmd-plugin-enable.txt b/Documentation/cmd-plugin-enable.txt
index 585efd8..c8022ef 100644
--- a/Documentation/cmd-plugin-enable.txt
+++ b/Documentation/cmd-plugin-enable.txt
@@ -15,7 +15,9 @@
 `<plugin-jar-name>.disabled` to `<plugin-jar-name>`.
 
 == ACCESS
-Caller must be a member of the privileged 'Administrators' group.
+* Caller must be a member of the privileged 'Administrators' group.
+* link:config-gerrit.html#plugins.allowRemoteAdmin[plugins.allowRemoteAdmin]
+must be enabled in `$site_path/etc/gerrit.config`.
 
 == SCRIPTING
 This command is intended to be used in scripts.
diff --git a/Documentation/cmd-plugin-install.txt b/Documentation/cmd-plugin-install.txt
index daa6472..0ce6d7d 100644
--- a/Documentation/cmd-plugin-install.txt
+++ b/Documentation/cmd-plugin-install.txt
@@ -17,7 +17,9 @@
 `plugins` directory.
 
 == ACCESS
-Caller must be a member of the privileged 'Administrators' group.
+* Caller must be a member of the privileged 'Administrators' group.
+* link:config-gerrit.html#plugins.allowRemoteAdmin[plugins.allowRemoteAdmin]
+must be enabled in `$site_path/etc/gerrit.config`.
 
 == SCRIPTING
 This command is intended to be used in scripts.
diff --git a/Documentation/cmd-plugin-ls.txt b/Documentation/cmd-plugin-ls.txt
index d9c997e..905c9ab 100644
--- a/Documentation/cmd-plugin-ls.txt
+++ b/Documentation/cmd-plugin-ls.txt
@@ -14,7 +14,9 @@
 List the installed plugins and show their version and status.
 
 == ACCESS
-Caller must be a member of the privileged 'Administrators' group.
+* Caller must be a member of the privileged 'Administrators' group.
+* link:config-gerrit.html#plugins.allowRemoteAdmin[plugins.allowRemoteAdmin]
+must be enabled in `$site_path/etc/gerrit.config`.
 
 == SCRIPTING
 This command is intended to be used in scripts.
diff --git a/Documentation/cmd-plugin-reload.txt b/Documentation/cmd-plugin-reload.txt
index 8889307..88cb1f3 100644
--- a/Documentation/cmd-plugin-reload.txt
+++ b/Documentation/cmd-plugin-reload.txt
@@ -19,7 +19,9 @@
 make the new configuration data become active.
 
 == ACCESS
-Caller must be a member of the privileged 'Administrators' group.
+* Caller must be a member of the privileged 'Administrators' group.
+* link:config-gerrit.html#plugins.allowRemoteAdmin[plugins.allowRemoteAdmin]
+must be enabled in `$site_path/etc/gerrit.config`.
 
 == SCRIPTING
 This command is intended to be used in scripts.
diff --git a/Documentation/cmd-plugin-remove.txt b/Documentation/cmd-plugin-remove.txt
index 3197203..770df85 100644
--- a/Documentation/cmd-plugin-remove.txt
+++ b/Documentation/cmd-plugin-remove.txt
@@ -16,7 +16,9 @@
 jars in the site path's `plugins` directory to `<plugin-jar-name>.disabled`.
 
 == ACCESS
-Caller must be a member of the privileged 'Administrators' group.
+* Caller must be a member of the privileged 'Administrators' group.
+* link:config-gerrit.html#plugins.allowRemoteAdmin[plugins.allowRemoteAdmin]
+must be enabled in `$site_path/etc/gerrit.config`.
 
 == SCRIPTING
 This command is intended to be used in scripts.
diff --git a/Documentation/cmd-query.txt b/Documentation/cmd-query.txt
index 538b6ed..0ff59d4 100644
--- a/Documentation/cmd-query.txt
+++ b/Documentation/cmd-query.txt
@@ -116,7 +116,7 @@
 ====
   $ ssh -p 29418 review.example.com gerrit query --format=JSON status:open project:tools/gerrit limit:2
   {"project":"tools/gerrit", ...}
-  {"project":"tools/gerrit", ..., sortKey:"000e6aee00003e26", ...}
+  {"project":"tools/gerrit", ...}
   {"type":"stats","rowCount":2,"runningTimeMilliseconds:15}
 ====
 
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index 4c9962d..70a695e 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -13,7 +13,9 @@
   [--notify <NOTIFYHANDLING> | -n <NOTIFYHANDLING>]
   [--submit | -s]
   [--abandon | --restore]
+  [--rebase]
   [--publish]
+  [--json | -j]
   [--delete]
   [--verified <N>] [--code-review <N>]
   [--label Label-Name=<N>]
@@ -55,6 +57,15 @@
 -m::
 	Optional cover letter to include as part of the message
 	sent to reviewers when the approval states are updated.
+	(option is mutually exclusive with --json)
+
+--json::
+-j::
+	Read review input from JSON file. See
+	link:rest-api-changes.html#review-input[ReviewInput] entity for the
+	format.
+	(option is mutually exclusive with --submit, --restore, --publish, --delete,
+	--abandon, --message and --rebase)
 
 --notify::
 -n::
@@ -75,25 +86,32 @@
 
 --abandon::
 	Abandon the specified change(s).
-	(option is mutually exclusive with --submit, --restore, --publish and
-	--delete)
+	(option is mutually exclusive with --submit, --restore, --publish, --delete,
+	--rebase and --json)
 
 --restore::
 	Restore the specified abandoned change(s).
-	(option is mutually exclusive with --abandon)
+	(option is mutually exclusive with --abandon and --json)
+
+--rebase::
+	Rebase the specified change(s).
+	(option is mutually exclusive with --abandon, --submit, --delete and --json)
 
 --submit::
 -s::
 	Submit the specified patch set(s) for merging.
-	(option is mutually exclusive with --abandon, --publish and --delete)
+	(option is mutually exclusive with --abandon, --publish --delete, --rebase
+	and --json)
 
 --publish::
 	Publish the specified draft patch set(s).
-	(option is mutually exclusive with --submit, --restore, --abandon, and --delete)
+	(option is mutually exclusive with --submit, --restore, --abandon, --delete
+	and --json)
 
 --delete::
 	Delete the specified draft patch set(s).
-	(option is mutually exclusive with --submit, --restore, --abandon, and --publish)
+	(option is mutually exclusive with --submit, --restore, --abandon, --publish,
+	--rebase and --json)
 
 --code-review::
 --verified::
diff --git a/Documentation/cmd-set-account.txt b/Documentation/cmd-set-account.txt
index cec5a8e..f31f61b 100644
--- a/Documentation/cmd-set-account.txt
+++ b/Documentation/cmd-set-account.txt
@@ -7,9 +7,11 @@
 --
 set-account [--full-name <FULLNAME>] [--active|--inactive] \
             [--add-email <EMAIL>] [--delete-email <EMAIL> | ALL] \
+            [--preferred-email <EMAIL>] \
             [--add-ssh-key - | <KEY>] \
             [--delete-ssh-key - | <KEY> | ALL] \
-            [--http-password <PASSWORD>] <USER>
+            [--http-password <PASSWORD>] \
+            [--clear-http-password] <USER>
 --
 
 == DESCRIPTION
@@ -21,7 +23,15 @@
 verification step we force within the UI.
 
 == ACCESS
-Caller must be a member of the privileged 'Administrators' group.
+Caller must be a member of the privileged 'Administrators' group,
+or have been granted
+link:access-control.html#capability_modifyAccount[the 'Modify Account' global capability].
+For security reasons only the members of the privileged 'Administrators'
+group can add or delete SSH keys for a user.
+
+To set the HTTP password for the user account (option --http-password) or
+to clear the HTTP password (option --clear-http-password) caller must be
+a member of the privileged 'Administrators' group.
 
 == SCRIPTING
 This command is intended to be used in scripts.
@@ -58,6 +68,13 @@
     Maybe supplied more than once to remove multiple emails
     from an account in a single command execution.
 
+--preferred-email::
+    Sets the preferred email address for the user's account.
+    The email address must already have been registered
+    with the user's account before it can be set.
+    May be supplied with the delete-email option as long as
+    the emails are not the same.
+
 --add-ssh-key::
     Content of the public SSH key to add to the account's
     keyring.  If `-` the key is read from stdin, rather than
@@ -77,6 +94,9 @@
 --http-password::
     Set the HTTP password for the user account.
 
+--clear-http-password::
+    Clear the HTTP password for the user account.
+
 == EXAMPLES
 Add an email and SSH key to `watcher`'s account:
 
diff --git a/Documentation/cmd-set-reviewers.txt b/Documentation/cmd-set-reviewers.txt
index 4cb7bbd..79f7651 100644
--- a/Documentation/cmd-set-reviewers.txt
+++ b/Documentation/cmd-set-reviewers.txt
@@ -27,7 +27,7 @@
 --project::
 -p::
 	Name of the project the intended change is contained within.  This
-	option must be supplied before Change-ID in order to take effect.
+	option must be supplied before Change-Id in order to take effect.
 
 --add::
 -a::
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index 68e796a..c754f35 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -37,35 +37,16 @@
 == SCHEMA
 The JSON messages consist of nested objects referencing the *change*,
 *patchSet*, *account* involved, and other attributes as appropriate.
-The currently supported message types are *patchset-created*,
-*draft-published*, *change-abandoned*, *change-restored*,
-*change-merged*, *merge-failed*, *comment-added*, *ref-updated* and
-*reviewer-added*.
 
 Note that any field may be missing in the JSON messages, so consumers of
 this JSON stream should deal with that appropriately.
 
 [[events]]
-=== Events
-==== Patchset Created
-type:: "patchset-created"
+== EVENTS
+=== Change Abandoned
 
-change:: link:json.html#change[change attribute]
+Sent when a change has been abandoned.
 
-patchSet:: link:json.html#patchSet[patchSet attribute]
-
-uploader:: link:json.html#account[account attribute]
-
-==== Draft Published
-type:: "draft-published"
-
-change:: link:json.html#change[change attribute]
-
-patchSet:: link:json.html#patchSet[patchSet attribute]
-
-uploader:: link:json.html#account[account attribute]
-
-==== Change Abandoned
 type:: "change-abandoned"
 
 change:: link:json.html#change[change attribute]
@@ -76,7 +57,30 @@
 
 reason:: Reason for abandoning the change.
 
-==== Change Restored
+eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
+created.
+
+=== Change Merged
+
+Sent when a change has been merged into the git repository.
+
+type:: "change-merged"
+
+change:: link:json.html#change[change attribute]
+
+patchSet:: link:json.html#patchSet[patchSet attribute]
+
+submitter:: link:json.html#account[account attribute]
+
+newRev:: The resulting revision of the merge.
+
+eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
+created.
+
+=== Change Restored
+
+Sent when an abandoned change has been restored.
+
 type:: "change-restored"
 
 change:: link:json.html#change[change attribute]
@@ -87,27 +91,13 @@
 
 reason:: Reason for restoring the change.
 
-==== Change Merged
-type:: "change-merged"
+eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
+created.
 
-change:: link:json.html#change[change attribute]
+=== Comment Added
 
-patchSet:: link:json.html#patchSet[patchSet attribute]
+Sent when a review comment has been posted on a change.
 
-submitter:: link:json.html#account[account attribute]
-
-==== Merge Failed
-type:: "merge-failed"
-
-change:: link:json.html#change[change attribute]
-
-patchSet:: link:json.html#patchSet[patchSet attribute]
-
-submitter:: link:json.html#account[account attribute]
-
-reason:: Reason that the merge failed.
-
-==== Comment Added
 type:: "comment-added"
 
 change:: link:json.html#change[change attribute]
@@ -118,16 +108,106 @@
 
 approvals:: All link:json.html#approval[approval attributes] granted.
 
-comment:: Comment text author had written
+comment:: Review comment cover message.
 
-==== Ref Updated
+eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
+created.
+
+=== Draft Published
+
+Sent when a draft change has been published.
+
+type:: "draft-published"
+
+change:: link:json.html#change[change attribute]
+
+patchSet:: link:json.html#patchSet[patchSet attribute]
+
+uploader:: link:json.html#account[account attribute]
+
+eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
+created.
+
+=== Dropped Output
+
+Sent to notify a client that events have been dropped.
+
+type:: "dropped-output"
+
+=== Hashtags Changed
+
+Sent when the hashtags have been added to or removed from a change.
+
+type:: "hashtags-changed"
+
+change:: link:json.html#change[change attribute]
+
+editor:: link:json.html#account[account attribute]
+
+added:: List of hashtags added to the change
+
+removed:: List of hashtags removed from the change
+
+hashtags:: List of hashtags on the change after tags were added or removed
+
+eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
+created.
+
+=== Merge Failed
+
+Sent when a change has failed to be merged into the git repository.
+
+type:: "merge-failed"
+
+change:: link:json.html#change[change attribute]
+
+patchSet:: link:json.html#patchSet[patchSet attribute]
+
+submitter:: link:json.html#account[account attribute]
+
+reason:: Reason that the merge failed.
+
+eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
+created.
+
+=== Patchset Created
+
+Sent when a new change has been uploaded, or a new patch set has been uploaded
+to an existing change.
+
+Note that this event is also sent for changes or patch sets uploaded as draft,
+but is only visible to the change owner, any existing reviewers, and users who
+belong to a group that is granted the
+link:access-control.html#category_view_drafts[View Drafts] capability.
+
+type:: "patchset-created"
+
+change:: link:json.html#change[change attribute]
+
+patchSet:: link:json.html#patchSet[patchSet attribute]
+
+uploader:: link:json.html#account[account attribute]
+
+eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
+created.
+
+=== Ref Updated
+
+Sent when a reference is updated in a git repository.
+
 type:: "ref-updated"
 
 submitter:: link:json.html#account[account attribute]
 
 refUpdate:: link:json.html#refUpdate[refUpdate attribute]
 
-==== Reviewer Added
+eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
+created.
+
+=== Reviewer Added
+
+Sent when a reviewer is added to a change.
+
 type:: "reviewer-added"
 
 change:: link:json.html#change[change attribute]
@@ -136,7 +216,13 @@
 
 reviewer:: link:json.html#account[account attribute]
 
-==== Topic Changed
+eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
+created.
+
+=== Topic Changed
+
+Sent when the topic of a change has been changed.
+
 type:: "topic-changed"
 
 change:: link:json.html#change[change attribute]
@@ -145,6 +231,9 @@
 
 oldTopic:: Topic name before it was changed.
 
+eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
+created.
+
 == SEE ALSO
 
 * link:json.html[JSON Data Formats]
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 5b1817a..53645e4 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -283,6 +283,20 @@
 and disables the ability to manually modify or register other e-mails
 from the contact information page.
 
+[[auth.httpExternalIdHeader]]auth.httpExternalIdHeader::
++
+HTTP header to retrieve the user's external identification token.
+Only used if `auth.type` is set to `HTTP`.
++
+If set, Gerrit adds the value contained in the HTTP header to the
+user's identity. Typical use is with a federated identity token from
+an external system (e.g. GitHub OAuth 2.0 authentication) where
+the user's auth token exchanged during authentication handshake
+needs to be used for authenticated communication to the external
+system later on.
++
+Example: `auth.httpExternalIdHeader: X-GitHub-OTP`
+
 [[auth.loginUrl]]auth.loginUrl::
 +
 URL to redirect a browser to after the end-user has clicked on the
@@ -488,15 +502,17 @@
 * y, year, years (`1 year` is treated as `365 days`)
 
 +
+--
 If a unit suffix is not specified, `seconds` is assumed.  If 0 is
 supplied, the maximum age is infinite and items are never purged
 except when the cache is full.
-+
+
 Default is `0`, meaning store forever with no expire, except:
-+
+
 * `"adv_bases"`: default is `10 minutes`
 * `"ldap_groups"`: default is `1 hour`
 * `"web_sessions"`: default is `12 hours`
+--
 
 [[cache.name.memoryLimit]]cache.<name>.memoryLimit::
 +
@@ -721,9 +737,11 @@
 * h, hr, hour, hours
 
 +
+--
 If a unit suffix is not specified, `milliseconds` is assumed.
-+
+
 Default is 5 seconds.
+--
 
 [[cache.diff_intraline.timeout]]cache.diff_intraline.timeout::
 +
@@ -744,9 +762,11 @@
 * h, hr, hour, hours
 
 +
+--
 If a unit suffix is not specified, `milliseconds` is assumed.
-+
+
 Default is 5 seconds.
+--
 
 [[cache.diff_intraline.enabled]]cache.diff_intraline.enabled::
 +
@@ -775,6 +795,26 @@
 +
 Default is 5 minutes.
 
+[[cache.projects.loadOnStartup]]cache.projects.loadOnStartup::
++
+If the project cache should be loaded during server startup.
++
+The cache is loaded concurrently. Admins should ensure that the cache
+size set under <<cache.name.memoryLimit,cache.projects.memoryLimit>>
+is not smaller than the number of repos.
++
+Default is false, disabled.
+
+[[cache.projects.loadThreads]]cache.projects.loadThreads::
++
+Only relevant if <<cache.projects.loadOnStartup,cache.projects.loadOnStartup>>
+is true.
++
+The number of threads to allocate for loading the cache at startup. These
+threads will die out after the cache is loaded.
++
+Default is the number of CPUs.
+
 [[change]]
 === Section change
 
@@ -830,6 +870,21 @@
 +
 Default is "Submit patch set ${patchSet} into ${branch}".
 
+[[change.replyLabel]]change.replyLabel::
++
+Label name for the reply button. In the user interface an ellipsis (…)
+is appended.
++
+Default is "Reply". In the user interface it becomes "Reply…".
+
+[[change.replyTooltip]]change.replyTooltip::
++
+Tooltip for the reply button. In the user interface a note about the
+keyboard shortcut is appended.
++
+Default is "Reply and score". In the user interface it becomes "Reply
+and score (Shortcut: a)".
+
 [[changeMerge]]
 === Section changeMerge
 
@@ -845,19 +900,22 @@
 
 [[changeMerge.threadPoolSize]]changeMerge.threadPoolSize::
 +
-Maximum size of the thread pool in which the mergeability flag of open
-changes is updated.
+_Deprecated:_ Formerly used to control thread pool size for background
+mergeability checks. These checks were moved to the indexing threadpool,
+so this value is now used for
+link:#index.batchThreads[index.batchThreads], only if that value is not
+provided.
 +
-Default is 1.
+This option may be removed in a future version.
 
 [[changeMerge.interactiveThreadPoolSize]]changeMerge.interactiveThreadPoolSize::
 +
-Maximum size of the thread pool in which the mergeability flag of open
-changes is updated, when processing interactive user requests (e.g.
-pushes to refs/for/*). Set to 0 or negative to share the pool for
-background mergeability checks.
+_Deprecated:_ Formerly used to control thread pool size for interactive
+mergeability checks. These checks were moved to the indexing threadpool,
+so this value is now used for link:#index.threads[index.threads], only
+if that value is not provided.
 +
-Default is 1.
+This option may be removed in a future version.
 
 [[commentlink]]
 === Section commentlink
@@ -997,6 +1055,15 @@
   javaOptions = -Dlog4j.configuration=file:///home/gerrit/site/etc/log4j.properties
 ----
 
+[[container.daemonOpt]]container.daemonOpt::
++
+Additional options to pass to the daemon (e.g. '--enable-httpd'). If
+multiple values are configured, they are passed in that order to the command
+line, separated by spaces.
++
+Execute `java -jar gerrit.war daemon --help` to see all possible
+options.
+
 [[container.slave]]container.slave::
 +
 Used on Gerrit slave installations. If set to true the Gerrit JVM is
@@ -1260,12 +1327,14 @@
 * h, hr, hour, hours
 
 +
+--
 If a unit suffix is not specified, `milliseconds` is assumed.
-+
+
 Default is `30 seconds`.
-+
+
 This setting only applies if
 <<database.connectionPool,database.connectionPool>> is true.
+--
 
 [[database.dataSourceInterceptorClass]]database.dataSourceInterceptorClass::
 
@@ -1488,10 +1557,10 @@
 +
 Optional command to install the `commit-msg` hook. Typically of the
 form:
++
 ----
 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
@@ -1510,21 +1579,34 @@
 
 [[gerrit.reportBugUrl]]gerrit.reportBugUrl::
 +
-URL to direct users to when they need to report a bug about the
-Gerrit service. By default this links to the upstream Gerrit
-Code Review's own bug tracker but could be directed to the system
-administrator's ticket queue.
+URL to direct users to when they need to report a bug.
++
+By default unset, meaning no bug report URL will be displayed. Administrators
+should set this to the URL of their issue tracker, if necessary.
 
 [[gerrit.reportBugText]]gerrit.reportBugText::
 +
 Text to be displayed in the link to the bug report URL.
 +
+Only used when `gerrit.reportBugUrl` is set.
++
 Defaults to "Report Bug".
 
-[[gerrit.changeScreen]]gerrit.changeScreen::
+[[gerrit.disableReverseDnsLookup]]gerrit.disableReverseDnsLookup::
 +
-Default change screen UI to direct users to. Valid values are
-`OLD_UI` and `CHANGE_SCREEN2`. Default is `CHANGE_SCREEN2`.
+Disables reverse DNS lookup during computing ref log entry for identified user.
++
+Defaults to false.
+
+[[gerrit.secureStoreClass]]gerrit.secureStoreClass::
++
+Use the secure store implementation from a specified class.
++
+If specified, must be the fully qualified class name of a class that implements
+the `com.google.gerrit.server.securestore.SecureStore` interface, and the jar
+file containing the class must be placed in the `$site_path/lib` folder.
++
+If not specified, the default no-op implementation is used.
 
 [[gitweb]]
 === Section gitweb
@@ -1677,41 +1759,61 @@
 +
 Optional path to hooks, if not specified then `'$site_path'/hooks` will be used.
 
-[[hooks.patchsetCreatedHook]]hooks.patchsetCreatedHook::
+[[hooks.syncHookTimeout]]hooks.syncHookTimeout::
 +
-Optional filename for the patchset created hook, if not specified then
-`patchset-created` will be used.
-
-[[hooks.draftPublishedHook]]hooks.draftPublishedHook::
-+
-Optional filename for the draft published hook, if not specified then
-`draft-published` will be used.
-
-[[hooks.commentAddedHook]]hooks.commentAddedHook::
-+
-Optional filename for the comment added hook, if not specified then
-`comment-added` will be used.
-
-[[hooks.changeMergedHook]]hooks.changeMergedHook::
-+
-Optional filename for the change merged hook, if not specified then
-`change-merged` will be used.
-
-[[hooks.mergeFailedHook]]hooks.mergeFailedHook::
-+
-Optional filename for the merge failed hook, if not specified then
-`merge-failed` will be used.
+Optional timeout value in seconds for synchronous hooks, if not specified
+then 30 seconds will be used.
 
 [[hooks.changeAbandonedHook]]hooks.changeAbandonedHook::
 +
 Optional filename for the change abandoned hook, if not specified then
 `change-abandoned` will be used.
 
+[[hooks.changeMergedHook]]hooks.changeMergedHook::
++
+Optional filename for the change merged hook, if not specified then
+`change-merged` will be used.
+
 [[hooks.changeRestoredHook]]hooks.changeRestoredHook::
 +
 Optional filename for the change restored hook, if not specified then
 `change-restored` will be used.
 
+[[hooks.claSignedHook]]hooks.claSignedHook::
++
+Optional filename for the CLA signed hook, if not specified then
+`cla-signed` will be used.
+
+[[hooks.commentAddedHook]]hooks.commentAddedHook::
++
+Optional filename for the comment added hook, if not specified then
+`comment-added` will be used.
+
+[[hooks.draftPublishedHook]]hooks.draftPublishedHook::
++
+Optional filename for the draft published hook, if not specified then
+`draft-published` will be used.
+
+[[hooks.hashtagsChangedHook]]hooks.hashtagsChangedHook::
++
+Optional filename for the hashtags changed hook, if not specified then
+`hashtags-changed` will be used.
+
+[[hooks.mergeFailedHook]]hooks.mergeFailedHook::
++
+Optional filename for the merge failed hook, if not specified then
+`merge-failed` will be used.
+
+[[hooks.patchsetCreatedHook]]hooks.patchsetCreatedHook::
++
+Optional filename for the patchset created hook, if not specified then
+`patchset-created` will be used.
+
+[[hooks.refUpdateHook]]hooks.refUpdateHook::
++
+Optional filename for the ref update hook, if not specified then
+`ref-update` will be used.
+
 [[hooks.refUpdatedHook]]hooks.refUpdatedHook::
 +
 Optional filename for the ref updated hook, if not specified then
@@ -1727,21 +1829,6 @@
 Optional filename for the topic changed hook, if not specified then
 `topic-changed` will be used.
 
-[[hooks.claSignedHook]]hooks.claSignedHook::
-+
-Optional filename for the CLA signed hook, if not specified then
-`cla-signed` will be used.
-
-[[hooks.refUpdateHook]]hooks.refUpdateHook::
-+
-Optional filename for the ref update hook, if not specified then
-`ref-update` will be used.
-
-[[hooks.syncHookTimeout]]hooks.syncHookTimeout::
-+
-Optional timeout value in seconds for synchronous hooks, if not specified
-then 30 seconds will be used.
-
 [[http]]
 === Section http
 
@@ -1763,6 +1850,24 @@
 This property is honored only if the password does not
 appear in the http.proxy property above.
 
+[[http.addUserAsRequestAttribute]]http.addUserAsRequestAttribute::
++
+If true, 'User' attribute will be added to the request attributes so it
+can be accessed outside the request scope (will be set to username or id
+if username not configured).
++
+This attribute can be used by the servlet container to log user in the
+http access log.
++
+When running the embedded servlet container, this attribute is used to
+print user in the httpd_log.
++
+* `%{User}r`
++
+Pattern to print user in Tomcat AccessLog.
+
++
+Default value is true.
 
 [[httpd]]
 === Section httpd
@@ -1817,10 +1922,12 @@
 'https://' is the proper URL back to the server.
 
 +
+--
 If multiple values are supplied, the daemon will listen on all
 of them.
-+
+
 By default, http://*:8080.
+--
 
 [[httpd.reuseAddress]]httpd.reuseAddress::
 +
@@ -1948,11 +2055,13 @@
 * y, year, years (`1 year` is treated as `365 days`)
 
 +
+--
 If a unit suffix is not specified, `minutes` is assumed.  If 0
 is supplied, the maximum age is infinite and connections will not
 abort until the client disconnects.
-+
+
 By default, 5 minutes.
+--
 
 [[httpd.filterClass]]httpd.filterClass::
 +
@@ -1983,7 +2092,7 @@
 	type = HTTP
 	httpHeader = TRUSTED_USER
 
-[http]
+[httpd]
 	filterClass = org.anyorg.MySecureFilter
 ----
 
@@ -2031,9 +2140,20 @@
 
 [[index.threads]]index.threads::
 +
-Determines the number of threads to use for indexing.
+Number of threads to use for indexing in normal interactive operations.
 +
-Defaults to 1 if not set, or set to a negative value.
+Defaults to 1 if not set, or set to a negative value (unless
+link:#changeMerge.interactiveThreadPoolSize[changeMerge.interactiveThreadPoolSize]
+is iset).
+
+[[index.batchThreads]]index.batchThreads::
++
+Number of threads to use for indexing in background operations, such as
+online schema upgrades.
++
+If not set or set to a negative value, defaults to using the same
+thread pool as interactive operations (unless
+link:#changeMerge.threadPoolSize[changeMerge.threadPoolSize] is set).
 
 ==== Lucene configuration
 
@@ -2161,6 +2281,13 @@
 +
 By default, true, requiring the certificate to be verified.
 
+[[ldap.groupsVisibleToAll]]ldap.groupsVisibleToAll::
++
+If true, LDAP groups are visible to all registered users.
++
+By default, false, LDAP groups are visible only to administrators and
+group members.
+
 [[ldap.username]]ldap.username::
 +
 _(Optional)_ Username to bind to the LDAP server with.  If not set,
@@ -2461,8 +2588,8 @@
 If set to true, files with the MIME type `<name>` will be sent as
 direct downloads to the user's browser, rather than being wrapped up
 inside of zipped archives.  The type name may be a complete type
-name, e.g. `image/gif`, a generic media type, e.g. `image/*`,
-or the wildcard `*/*` to match all types.
+name, e.g. `image/gif`, a generic media type, e.g. `+image/*+`,
+or the wildcard `+*/*+` to match all types.
 +
 By default, false for all MIME types.
 
@@ -2601,6 +2728,21 @@
 +
 Common unit suffixes of 'k', 'm', or 'g' are supported.
 
+[[receive.maxBatchChanges]]receive.maxBatchChanges::
++
+The maximum number of changes that Gerrit allows to be pushed
+in a batch for review. When this number is exceeded Gerrit rejects
+the push with an error message.
++
+May be overridden for certain groups by specifying a limit in the
+link:access-control.html#capability_batchChangesLimit['Batch Changes Limit']
+global capability.
++
+This setting can be used to prevent users from uploading large
+number of changes for review by mistake.
++
+Default is zero, no limit.
+
 [[receive.threadPoolSize]]receive.threadPoolSize::
 +
 Maximum size of the thread pool in which the change data in received packs is
@@ -2674,6 +2816,31 @@
 +
 Default is true, to execute project specific rules.
 
+[[rules.reductionLimit]]rules.reductionLimit::
++
+Maximum number of Prolog reductions that can be performed when
+evaluating rules for a single change. Each function call made
+in user rule code, internal Gerrit Prolog code, or the Prolog
+interpreter counts against this limit.
++
+Sites using very complex rules that need many reductions should
+compile Prolog to Java bytecode with link:pgm-rulec.html[rulec].
+This eliminates the dynamic Prolog interpreter from charging its
+own reductions against the limit, enabling more logic to execute
+within the same bounds.
++
+A reductionLimit of 0 is nearly infinite, implemented by setting
+the internal limit to 2^31-1.
++
+Default is 100,000 reductions (about 14 ms on Intel Core i7 CPU).
+
+[[rules.compileReductionLimit]]rules.compileReductionLimit::
++
+Maximum number of Prolog reductions that can be performed when
+compiling source code to internal Prolog machine code.
++
+Default is 10x reductionLimit (1,000,000).
+
 [[execution]]
 === Section execution
 
@@ -2842,12 +3009,6 @@
 updated versions. If false, a server restart is required to change
 any of these resources. Default is true, allowing automatic reloads.
 
-[[site.enableDeprecatedQuery]]site.enableDeprecatedQuery::
-+
-If true the deprecated `/query` URL is available to return JSON
-and text results for changes. If false, the URL is disabled and
-returns 404 to clients. Default is true, enabling `/query`.
-
 [[ssh-alias]]
 === Section ssh-alias
 
@@ -2881,15 +3042,17 @@
 * 'hostname':'port' (for example `review.example.com:29418`)
 * 'IPv4':'port' (for example `10.0.0.1:29418`)
 * ['IPv6']:'port' (for example `[ff02::1]:29418`)
-* *:'port' (for example `*:29418`)
+* +*:'port'+ (for example `+*:29418+`)
 
 +
+--
 If multiple values are supplied, the daemon will listen on all
 of them.
-+
+
 To disable the internal SSHD, set listenAddress to `off`.
-+
+
 By default, *:29418.
+--
 
 [[sshd.advertisedAddress]]sshd.advertisedAddress::
 +
@@ -2898,16 +3061,18 @@
 redirector is being used, making Gerrit appear to answer on port
 22. The following forms may be used to specify an address.  In any
 form, `:'port'` may be omitted to use the default SSH port of 22.
-+
+
 * 'hostname':'port' (for example `review.example.com:22`)
 * 'IPv4':'port' (for example `10.0.0.1:29418`)
 * ['IPv6']:'port' (for example `[ff02::1]:29418`)
 
 +
+--
 If multiple values are supplied, the daemon will advertise all
 of them.
-+
+
 By default, sshd.listenAddress.
+--
 
 [[sshd.tcpKeepAlive]]sshd.tcpKeepAlive::
 +
@@ -3110,6 +3275,16 @@
 New configurations should prefer the boolean value for this field
 and an enum value for `accounts.visibility`.
 
+[[suggest.maxSuggestedReviewers]]suggest.maxSuggestedReviewers::
++
+The maximum numbers of reviewers suggested.
++
+By default 10.
+
+[[suggest.fullTextSearch]]suggest.fullTextSearch::
++
+If 'true' the reviewer completion suggestions will be based on a full text search.
+
 [[suggest.from]]suggest.from::
 +
 The number of characters that a user must have typed before suggestions
@@ -3117,6 +3292,15 @@
 +
 By default 0.
 
+[[suggest.fullTextSearchMaxMatches]]suggest.fullTextSearchMaxMatches::
++
+The maximum number of matches evaluated for change access when using full text search.
++
+Making this number too high could have a negative impact on performance.
++
+By default 100.
+
+
 [[theme]]
 === Section theme
 
@@ -3220,7 +3404,7 @@
 
 Tagged footer lines containing references to external
 tracking systems, parsed out of the commit message and
-saved in Gerrit's database.
+saved in Gerrit's secondary index.
 
 After making changes to this section, existing changes
 must be reindexed with link:pgm-reindex.html[reindex].
@@ -3231,6 +3415,7 @@
 ----
 [trackingid "jira-bug"]
   footer = Bugfix:
+  footer = Bug:
   match = JRA\\d{2,8}
   system = JIRA
 
@@ -3242,11 +3427,15 @@
 
 [[trackingid.name.footer]]trackingid.<name>.footer::
 +
-A prefix tag that identify the footer line to parse for tracking ids.
-Several trackingid entries can have the same footer tag. A single
-trackingid entry can have multiple footer tags. If multiple footer
-tags are specified, each tag will be parsed separately.
-(the trailing ":" is optional)
+A prefix tag that identifies the footer line to parse for tracking ids.
++
+Several trackingid entries can have the same footer tag, and a single trackingid
+entry can have multiple footer tags.
++
+If multiple footer tags are specified, each tag will be parsed separately and
+duplicates will be ignored.
++
+The trailing ":" is optional.
 
 [[trackingid.name.match]]trackingid.<name>.match::
 +
diff --git a/Documentation/config-gitweb.txt b/Documentation/config-gitweb.txt
index d1cee57..2311184 100644
--- a/Documentation/config-gitweb.txt
+++ b/Documentation/config-gitweb.txt
@@ -215,7 +215,7 @@
 
 The CGI's `$projectroot` should be the same directory as
 gerrit.basePath, or a fairly current replica.  If a replica is
-being used, ensure it uses a full mirror, so the `refs/changes/*`
+being used, ensure it uses a full mirror, so the `+refs/changes/*+`
 namespace is available.
 
 ----
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index 8d58d36..5666dff 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -50,8 +50,9 @@
 
   TRIVIAL_REBASE;; Conflict-free merge between the new parent and the prior patch set.
 
-  NO_CODE_CHANGE;; No code changed; same tree and same parents.
+  NO_CODE_CHANGE;; No code changed; same tree and same parent tree.
 
+  NO_CHANGE;; No changes; same commit message, same tree and same parent tree.
 
 === draft-published
 
@@ -125,6 +126,25 @@
   topic-changed --change <change id> --change-owner <change owner> --project <project name> --branch <branch> --changer <changer> --old-topic <old topic> --new-topic <new topic>
 ====
 
+=== hashtags-changed
+
+Called whenever hashtags are added to or removed from a change from the Web UI
+or via the REST API.
+
+====
+  hashtags-changed --change <change id>  --change-owner <change owner> --project <project name> --branch <branch> --editor <editor> --added <hashtag> --removed <hashtag> --hashtag <hashtag>
+====
+
+The `--added` parameter may be passed multiple times, once for each
+hashtag that was added to the change.
+
+The `--removed` parameter may be passed multiple times, once for each
+hashtag that was removed from the change.
+
+The `--hashtag` parameter may be passed multiple times, once for each
+hashtag remaining on the change after the add or remove operation has
+been performed.
+
 === cla-signed
 
 Called whenever a user signs a contributor license agreement.
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index aaeb834..ce49d31 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -222,7 +222,8 @@
 === `label.Label-Name.copyMinScore`
 
 If true, the lowest possible negative value for the label is copied
-forward when a new patch set is uploaded.
+forward when a new patch set is uploaded. Defaults to false, except
+for All-Projects which has it true by default.
 
 [[label_copyMaxScore]]
 === `label.Label-Name.copyMaxScore`
@@ -230,7 +231,7 @@
 If true, the highest possible positive value for the label is copied
 forward when a new patch set is uploaded. This can be used to enable
 sticky approvals, reducing turn-around for trivial cleanups prior to
-submitting a change.
+submitting a change. Defaults to false.
 
 [[label_copyAllScoresOnTrivialRebase]]
 === `label.Label-Name.copyAllScoresOnTrivialRebase`
@@ -241,19 +242,33 @@
 patch set and if it has the same code delta as the previous patch set.
 This is the case if the change was rebased onto a different parent.
 This can be used to enable sticky approvals, reducing turn-around for
-trivial rebases prior to submitting a change. Defaults to false.
+trivial rebases prior to submitting a change.
+It is recommended to enable this for the Code-Review label.
+Defaults to false.
 
 [[label_copyAllScoresIfNoCodeChange]]
 === `label.Label-Name.copyAllScoresIfNoCodeChange`
 
 If true, all scores for the label are copied forward when a new patch
-set is uploaded that has the same parent commit as the previous patch
+set is uploaded that has the same parent tree as the previous patch
 set and the same code delta as the previous patch set. This means only
 the commit message is different. This can be used to enable sticky
 approvals on labels that only depend on the code, reducing turn-around
 if only the commit message is changed prior to submitting a change.
+It is recommended to enable this for the Verified label if enabled.
 Defaults to false.
 
+[[label_copyAllScoresIfNoChange]]
+=== `label.Label-Name.copyAllScoresIfNoChange`
+
+If true, all scores for the label are copied forward when a new patch
+set is uploaded that has the same parent tree, code delta, and commit
+message as the previous patch set. This means that only the patch
+set SHA1 is different. This can be used to enable sticky
+approvals, reducing turn-around for this special case.
+It is recommended to leave this enabled for both Verified and
+Code-Review labels. Defaults to true.
+
 [[label_canOverride]]
 === `label.Label-Name.canOverride`
 
@@ -325,6 +340,7 @@
 ====
 
 Upon clicking the Reply button:
+
 * Administrators have all scores (-3..+3) available, -3 is set as the default.
 * Project Owners have limited scores (-2..+2) available, -2 is set as the default.
 * Registered Users have limited scores (-1..+1) available, -1 is set as the default.
diff --git a/Documentation/config-plugins.txt b/Documentation/config-plugins.txt
new file mode 100644
index 0000000..608a3ac
--- /dev/null
+++ b/Documentation/config-plugins.txt
@@ -0,0 +1,565 @@
+= Plugins
+
+The Gerrit server functionality can be extended by installing plugins.
+
+[[installation]]
+== Plugin Installation
+Plugin installation is as easy as dropping the plugin jar into the
+`$site_path/plugins/` folder. It may take
+link:config-gerrit.html#plugins.checkFrequency[a few minutes] until
+the server picks up new and updated plugins.
+
+Plugins can also be installed via
+link:rest-api-plugins.html#install-plugin[REST] and
+link:cmd-plugin-install.html[SSH].
+
+[[development]]
+== Plugin Development
+
+How to develop plugins is described in the link:dev-plugins.html[
+Plugin Development Guide].
+
+If you want to share your plugin under the link:licenses.html#Apache2_0[
+Apache License 2.0] you can host your plugin development on the
+link:https://gerrit-review.googlesource.com[gerrit-review] Gerrit
+Server. You can request the creation of a new Project by email
+to the link:https://groups.google.com/forum/#!forum/repo-discuss[Gerrit
+mailing list]. You would be assigned as project owner of the new plugin
+project so that you can submit changes on your own. It is the
+responsibility of the project owner to maintain the plugin, e.g. to
+make sure that it works with new Gerrit versions and to create stable
+branches for old releases.
+
+[[core-plugins]]
+== Core Plugins
+
+Core plugins are packaged within the Gerrit war file and can easily be
+installed during the link:pgm-init.html[Gerrit initialization].
+
+The core plugins are developed and maintained by the Gerrit maintainers
+and the Gerrit community.
+
+[[commit-message-length-validator]]
+=== commit-message-length-validator
+
+This plugin checks the length of a commit’s commit message subject and
+message body, and reports warnings or errors to the git client if the
+lengths are exceeded.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/commit-message-length-validator[
+Project] |
+link:https://gerrit.googlesource.com/plugins/commit-message-length-validator/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/commit-message-length-validator/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[cookbook-plugin]]
+=== cookbook-plugin
+
+Sample plugin to demonstrate features of Gerrit's plugin API.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/cookbook-plugin[
+Project] |
+link:https://gerrit.googlesource.com/plugins/cookbook-plugin/+doc/master/src/main/resources/Documentation/about.md[
+Documentation]
+
+[[download-commands]]
+=== download-commands
+
+This plugin defines commands for downloading changes in different
+download schemes (for downloading via different network protocols).
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/download-commands[
+Project] |
+link:https://gerrit.googlesource.com/plugins/download-commands/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/download-commands/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[replication]]
+=== replication
+
+This plugin can automatically push any changes Gerrit Code Review makes
+to its managed Git repositories to another system. Usually this would
+be configured to provide mirroring of changes, for warm-standby
+backups, or a load-balanced public mirror farm.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/replication[
+Project] |
+link:https://gerrit.googlesource.com/plugins/replication/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/replication/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[reviewnotes]]
+=== reviewnotes
+
+Stores review information for Gerrit changes in the `refs/notes/review`
+branch.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/reviewnotes[
+Project] |
+link:https://gerrit.googlesource.com/plugins/reviewnotes/+doc/master/src/main/resources/Documentation/about.md[
+Documentation]
+
+[[singleusergroup]]
+=== singleusergroup
+
+This plugin provides a group per user. This is useful to assign access
+rights directly to a single user, since in Gerrit access rights can
+only be assigned to groups.
+
+[[other-plugins]]
+== Other Plugins
+
+Besides core plugins there are many other Gerrit plugins available.
+These plugins are developed and maintained by different parties.
+The Gerrit Project doesn't guarantee proper functionality of any of
+these plugins.
+
+The Gerrit Project doesn't provide binaries for these plugins, but
+there are some public services, like the
+link:https://ci.gerritforge.com/[CI Server from GerritForge], that
+offer the download of ready plugin jars.
+
+The following list gives an overview about available plugins, but the
+list may not be complete. You may discover more plugins on
+link:https://gerrit-review.googlesource.com/#/admin/projects/?filter=plugins%252F[
+gerrit-review].
+
+[[admin-console]]
+=== admin-console
+
+Plugin to provide administrator-only functionality, intended to
+simplify common administrative tasks. Currently providing user-level
+information. Also providing access control information by project or
+project/account.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/admin-console[
+Project] |
+link:https://gerrit.googlesource.com/plugins/admin-console/+doc/master/src/main/resources/Documentation/about.md[
+Documentation]
+
+[[avatars-external]]
+=== avatars/external
+
+This plugin allows to use an external url to load the avatar images
+from.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/avatars/external[
+Project] |
+link:https://gerrit.googlesource.com/plugins/avatars/external/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/avatars/external/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[avatars-gravatar]]
+=== avatars/gravatar
+
+Plugin to display user icons from Gravatar.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/avatars/gravatar[
+Project]
+
+[[branch-network]]
+=== branch-network
+
+This plugin allows the rendering of Git repository branch network in a
+graphical HTML5 Canvas. It is mainly intended to be used as a
+"project link" in a GitWeb configuration or by other Gerrit GWT UI
+plugins to be plugged elsewhere in Gerrit.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/branch-network[
+Project] |
+link:https://gerrit.googlesource.com/plugins/branch-network/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/branch-network/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[changemessage]]
+=== changemessage
+
+This plugin allows to display a static info message on the change screen.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/changemessage[
+Project] |
+link:https://gerrit.googlesource.com/plugins/changemessage/+doc/master/src/main/resources/Documentation/about.md[
+Plugin Documenatation] |
+link:https://gerrit.googlesource.com/plugins/changemessage/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[codenvy]]
+=== codenvy
+
+Plugin to allow to edit code on-line on either an existing branch or an
+active change using the link:http://codenvy.com[Codenvy] cloud
+development platform.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/codenvy[
+Project]
+
+[[delete-project]]
+=== delete-project
+
+Provides the ability to delete a project.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/delete-project[
+Project] |
+link:https://gerrit.googlesource.com/plugins/delete-project/+doc/master/src/main/resources/Documentation/about.md[
+Documentation]
+
+[[egit]]
+=== egit
+
+This plugin provides extensions for easier usage with EGit.
+
+The plugin adds a download command for EGit that allows to copy only
+the change ref into the clipboard. The change ref is needed for
+downloading a Gerrit change from within EGit.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/egit[
+Project] |
+link:https://gerrit.googlesource.com/plugins/egit/+doc/master/src/main/resources/Documentation/about.md[
+Documentation]
+
+[[force-draft]]
+=== force-draft
+
+Provides an ssh command to force a change or patch set to draft status.
+This is useful for administrators to be able to easily completely
+delete a change or patch set from the server.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/force-draft[
+Project]
+
+[[gitblit]]
+=== gitblit
+
+GitBlit code-viewer plugin with SSO and Security Access Control.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/gitblit[
+Project]
+
+[[github]]
+=== github
+
+Plugin to integrate with GitHub: replication, pull-request to Change-Sets
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/github[
+Project]
+
+[[gitiles]]
+=== gitiles
+
+Plugin running Gitiles alongside a Gerrit server.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/gitiles[
+Project]
+
+[[imagare]]
+=== imagare
+
+The imagare plugin allows Gerrit users to upload and share images.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/imagare[
+Project] |
+link:https://gerrit.googlesource.com/plugins/imagare/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/imagare/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[importer]]
+=== importer
+
+The importer plugin allows to import projects from one Gerrit server
+into another Gerrit server.
+
+Projects can be imported while both source and target Gerrit server
+are online. There is no downtime required.
+
+The git repository and all changes of the project, including approvals
+and review comments, are imported. Historic timestamps are preserved.
+
+Project imports can be resumed. This means a project team can continue
+to work in the source system while the import to the target system is
+done. By resuming the import the project in the target system can be
+updated with the missing delta.
+
+The importer plugin can also be used to copy a project within one Gerrit
+server, and in combination with the link:#delete-project[delete-project]
+plugin it can be used to rename a project.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/importer[
+Project] |
+link:https://gerrit.googlesource.com/plugins/importer/+doc/master/src/main/resources/Documentation/about.md[
+Documentation]
+
+[[its-plugins]]
+=== Issue Tracker System Plugins
+
+Plugins to integrate with issue tracker systems (ITS), that (based
+on events in Gerrit) allows to take actions in the ITS. For example,
+they can add comments to bugs, or change status of bugs.
+
+All its-plugins have a common base implementation which is stored in
+the `its-base` project. `its-base` is not a plugin, but just a
+framework for the ITS plugins which is packaged within each ITS plugin.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/its-base[
+its-base Project] |
+link:https://gerrit.googlesource.com/plugins/its-base/+doc/master/src/main/resources/Documentation/about.md[
+its-base Documentation] |
+link:https://gerrit.googlesource.com/plugins/its-base/+doc/master/src/main/resources/Documentation/config.md[
+its-base Configuration]
+
+[[its-bugzilla]]
+==== its-bugzilla
+
+Plugin to integrate with Bugzilla.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/its-bugzilla[
+Project] |
+link:https://gerrit.googlesource.com/plugins/its-bugzilla/+doc/master/src/main/resources/Documentation/about.md[
+Documentation]
+
+[[its-jira]]
+==== its-jira
+
+Plugin to integrate with Jira.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/its-jira[
+Project] |
+link:https://gerrit.googlesource.com/plugins/its-jira/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[its-rtc]]
+==== its-rtc
+
+Plugin to integrate with IBM Rational Team Concert (RTC).
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/its-rtc[
+Project] |
+link:https://gerrit.googlesource.com/plugins/its-rtc/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[javamelody]]
+=== javamelody
+
+This plugin allows to monitor the Gerrit server.
+
+This plugin integrates JavaMelody in Gerrit in order to retrieve live
+instrumentation data from Gerrit.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/javamelody[
+Project] |
+link:https://gerrit.googlesource.com/plugins/javamelody/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+https://gerrit.googlesource.com/plugins/javamelody/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[menuextender]]
+=== menuextender
+
+The menuextender plugin allows Gerrit administrators to configure
+additional menu entries from the WebUI.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/menuextender[
+Project] |
+link:https://gerrit.googlesource.com/plugins/menuextender/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/menuextender/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[motd]]
+=== motd
+
+This plugin can output messages to clients when pulling/fetching/cloning
+code from Gerrit Code Review. If the client (and transport mechanism)
+can support sending the message to the client, it will be displayed to
+the user (usually prefixed by “remote: ”), but will be silently
+discarded otherwise.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/motd[
+Project] |
+link:https://gerrit.googlesource.com/plugins/motd/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/motd/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[project-download-commands]]
+=== project-download-commands
+
+This plugin adds support for project specific download commands.
+
+Project specific download commands that are defined on a parent project
+are inherited by the child projects. Child projects can overwrite the
+inherited download command or remove it by assigning no value to it.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/project-download-commands[
+Project] |
+link:https://gerrit.googlesource.com/plugins/project-download-commands/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/project-download-commands/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[quota]]
+=== quota
+
+This plugin allows to enforce quotas in Gerrit.
+
+To protect a Gerrit installation it makes sense to limit the resources
+that a project or group can consume. To do this a Gerrit administrator
+can use this plugin to define quotas on project namespaces.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/quota[
+Project] |
+link:https://gerrit.googlesource.com/plugins/quota/+doc/master/src/main/resources/Documentation/about.md[
+Documentation]
+link:https://gerrit.googlesource.com/plugins/quota/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[reviewers]]
+=== reviewers
+
+A plugin that allows adding default reviewers to a change.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/reviewers[
+Project] |
+link:https://gerrit.googlesource.com/plugins/reviewers/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/reviewers/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[reviewers-by-blame]]
+=== reviewers-by-blame
+
+A plugin that allows automatically adding reviewers to a change from
+the git blame computation on the changed files. It will add the users
+that authored most of the lines touched by the change, since these
+users should be familiar with the code and can mostly review the
+change.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/reviewers-by-blame[
+Project] |
+link:https://gerrit.googlesource.com/plugins/reviewers-by-blame/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/reviewers-by-blame/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[groovy-provider]]
+=== scripting/groovy-provider
+
+This plugin provides a Groovy runtime environment for Gerrit plugins in Groovy.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/scripting/groovy-provider[
+Project] |
+link:https://gerrit.googlesource.com/plugins/scripting/groovy-provider/+doc/master/src/main/resources/Documentation/about.md[
+Documentation]
+
+[[scala-provider]]
+=== scripting/scala-provider
+
+This plugin provides a Scala runtime environment for Gerrit plugins in Scala.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/scripting/scala-provider[
+Project] |
+link:https://gerrit.googlesource.com/plugins/scripting/scala-provider/+doc/master/src/main/resources/Documentation/about.md[
+Documentation]
+
+[[server-config]]
+=== server-config
+
+This plugin enables access (download and upload) to the server config
+files.  It may be used to change Gerrit config files (like
+`etc/gerrit.config`) in cases where direct access to the file system
+where Gerrit's config files are stored is difficult or impossible to
+get.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/scripting/server-config[
+Project]
+
+[[serviceuser]]
+=== serviceuser
+
+This plugin allows to create service users in Gerrit.
+
+A service user is a user that is used by another service to communicate
+with Gerrit. E.g. a service user is needed to run the Gerrit Trigger
+Plugin in Jenkins. A service user is not able to login into the Gerrit
+WebUI and it cannot push commits or tags.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/scripting/serviceuser[
+Project] |
+link:https://gerrit.googlesource.com/plugins/serviceuser/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/serviceuser/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[uploadvalidator]]
+=== uploadvalidator
+
+This plugin allows to configure upload validations per project.
+
+Project owners can configure blocked file extensions, required footers
+and a maximum allowed path length. Pushes of commits that violate these
+settings are rejected by Gerrit.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/scripting/uploadvalidator[
+Project] |
+link:https://gerrit.googlesource.com/plugins/uploadvalidator/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/uploadvalidator/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[websession-flatfile]]
+=== websession-flatfile
+
+This plugin replaces the built-in Gerrit H2 based websession cache with
+a flatfile based implementation. This implemantation is shareable
+amongst multiple Gerrit servers, making it useful for multi-master
+Gerrit installations.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/scripting/websession-flatfile[
+Project] |
+link:https://gerrit.googlesource.com/plugins/websession-flatfile/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/websession-flatfile/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[wip]]
+=== wip
+
+This plugin adds a new button that allows a change owner to set a
+change to Work In Progress, and a button to change from WIP back to a
+"Ready For Review" state.
+
+Any change in the WIP state will not show up in anyone's Review
+Requests. Pushing a new patchset will reset the change to Review In
+Progress.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/scripting/wip[
+Project] |
+link:https://gerrit.googlesource.com/plugins/wip/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/wip/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+[[x-docs]]
+=== x-docs
+
+This plugin serves project documentation as HTML pages.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/scripting/x-docs[
+Project] |
+link:https://gerrit.googlesource.com/plugins/x-docs/+doc/master/src/main/resources/Documentation/about.md[
+Documentation] |
+link:https://gerrit.googlesource.com/plugins/x-docs/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 43ede06..276117b 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -46,10 +46,17 @@
        description = Rights inherited by all other projects
 [access "refs/*"]
        read = group Administrators
+[access "refs/heads/*"]
+        label-Your-Label-Here = -1..+1 group Administrators
 [capability]
        administrateServer = group Administrators
 [receive]
        requireContributorAgreement = false
+[label "Your-Label-Here"]
+        function = MaxWithBlock
+        value = -1 Your -1 Description
+        value =  0 Your No score Description
+        value = +1 Your +1 Description
 ----
 
 As you can see, there are several sections.
@@ -57,7 +64,7 @@
 The link:#project-section[+project+ section] appears once per project.
 
 The link:#access-section[+access+ section] appears once per reference pattern,
-such as `refs/*` or `refs/heads/*`.  Only one access section per pattern is
+such as `+refs/*+` or `+refs/heads/*+`.  Only one access section per pattern is
 allowed.  You will find examples of keys and values in each category section
 <<access_category,below>>.
 
@@ -70,6 +77,9 @@
 on a global level.  You can find examples of these
 <<capability_category,below>>.
 
+The link:#label-section[+label+] section can appear multiple times. You can
+also redefine the text and behavior of the built in label types `Code-Review`
+and `Verified`.
 
 [[project-section]]
 === Project section
@@ -197,6 +207,10 @@
 link:access-control.html#global_capabilities[Global Capabilities]
 documentation for a full list of available capabilities.
 
+[[label-section]]
+=== Label section
+
+Please refer to link:config-labels.html#label_custom[Custom Labels] documentation.
 
 [[branchOrder-section]]
 === branchOrder section
@@ -247,6 +261,7 @@
 #
 3d6da7dc4e99e6f6e5b5196e21b6f504fc530bba       Administrators
 global:Anonymous-Users                         Anonymous Users
+global:Change-Owner                            Change Owner
 global:Project-Owners                          Project Owners
 global:Registered-Users                        Registered Users
 ----
diff --git a/Documentation/config-sso.txt b/Documentation/config-sso.txt
index e4cf20e..561309b 100644
--- a/Documentation/config-sso.txt
+++ b/Documentation/config-sso.txt
@@ -51,12 +51,42 @@
 === Database Schema
 
 User identities obtained from OpenID providers are stored into the
-`account_external_ids` table.  Users may link more than one OpenID
-identity to the same Gerrit account (use Settings, Web Identities
-to manage this linking), making it easier for their browser to sign
-in to Gerrit if they are frequently switching between different
-unique OpenID accounts.
+`account_external_ids` table.
 
+=== Multiple Identities
+
+Users may link more than one OpenID identity to the same Gerrit account, making
+it easier for their browser to sign in to Gerrit if they are frequently switching
+between different unique OpenID accounts.
+
+[WARNING]
+Users wishing to link an alternative identity should *NOT* log in separately
+with that identity. Doing so will result in a new account being created, and
+subsequent attempts to link that account with the existing account will fail.
+In cases where this happens, the administrator will need to manually merge the
+accounts.  See link:https://code.google.com/p/gerrit/wiki/SqlMergeUserAccounts[
+Merging Gerrit User Accounts] on the Gerrit Wiki for details.
+
+Linking another identity is also useful for users whose primary OpenID provider
+shuts down. For example Google will
+link:https://developers.google.com/+/api/auth-migration[shut down their OpenID
+service on 20th April 2015]. Users must add an alternative identity, using another
+OpenID provider, before that shutdown date. User who fail to add an alternative
+identity before that date, and end up with their account only having a disabled
+Google identity, will need to create a separate account with an alternative
+provider and then ask the administrator to merge the accounts using the previously
+mentioned method.
+
+To link another identity to an existing account:
+
+* Login with the existing account
+* Select menu Settings -> Identities
+* Click the 'Link Another Identity' button
+* Select the OpenID provider for the other identity
+* Authenticate with the other identity
+
+Login using the other identity can only be performed after the linking is
+successful.
 
 == HTTP Basic/Digest Authentication
 
diff --git a/Documentation/config-validation.txt b/Documentation/config-validation.txt
index 5d23c79..27b39eb 100644
--- a/Documentation/config-validation.txt
+++ b/Documentation/config-validation.txt
@@ -1,7 +1,7 @@
-= Gerrit Code Review - Commit Validation
+= Gerrit Code Review - Plugin-based Validation
 
-Gerrit supports link:dev-plugins.html[plugin-based] validation of
-commits.
+Gerrit provides interfaces to allow link:dev-plugins.html[plugins] to
+perform validation on certain operations.
 
 [[new-commit-validation]]
 == New commit validation
@@ -21,6 +21,17 @@
 Out of the box, Gerrit includes a plugin that checks the length of the
 subject and body lines of commit messages on uploaded commits.
 
+[[ref-operation-validation]]
+== Ref operation validation
+
+
+Plugins implementing the `RefOperationValidationListener` interface can
+perform additional validation checks against ref creation/deletion operation
+before it is applied to the git repository.
+
+If the ref operation fails the validation, the plugin can throw an exception
+which will cause the operation to fail.
+
 [[pre-merge-validation]]
 == Pre-merge validation
 
@@ -67,6 +78,21 @@
 E.g. a plugin could use this to enforce a certain name scheme for
 group names.
 
+[[hashtag-validation]]
+== Hashtag validation
+
+
+Plugins implementing the `HashtagValidationListener` interface can perform
+validation of hashtags before they are added to or removed from changes.
+
+[[outgoing-email-validation]]
+== Outgoing e-mail validation
+
+
+This interface provides a low-level e-mail filtering API for plugins.
+Plugins implementing the `OutgoingEmailValidationListener` interface can perform
+filtering of outgoing e-mails just before they are sent.
+
 
 GERRIT
 ------
diff --git a/Documentation/config.defs b/Documentation/config.defs
index 642b915..8d67173 100644
--- a/Documentation/config.defs
+++ b/Documentation/config.defs
@@ -16,5 +16,6 @@
     'last-update-label!',
     'source-highlighter=prettify',
     'stylesheet=doc.css',
+    'linkcss=true',
     'revnumber="%s"' % revision,
   ]
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt
index ce40a01..2eb478c 100644
--- a/Documentation/dev-buck.txt
+++ b/Documentation/dev-buck.txt
@@ -3,6 +3,8 @@
 
 == Installation
 
+Note that you need to use Java 7 for building gerrit.
+
 There is currently no binary distribution of Buck, so it has to be manually
 built and installed.  Apache Ant is required.  Currently only Linux and Mac
 OS are supported.  Buck requires Python version 2.7 to be installed.
@@ -10,8 +12,9 @@
 Clone the git and build it:
 
 ----
-  git clone https://gerrit.googlesource.com/buck
+  git clone https://github.com/facebook/buck
   cd buck
+  git checkout $(cat ../gerrit/.buckversion)
   ant
 ----
 
@@ -30,10 +33,11 @@
 Note that the buck executable needs to be available in all shell sessions,
 so also make sure it is appended to the path globally.
 
-Add a symbolic link in `~/bin` to the buck executable:
+Add a symbolic link in `~/bin` to the buck and buckd executables:
 
 ----
   ln -s `pwd`/bin/buck ~/bin/
+  ln -s `pwd`/bin/buckd ~/bin/
 ----
 
 Verify that `buck` is accessible:
@@ -43,8 +47,8 @@
 ----
 
 To enable autocompletion of buck commands, install the autocompletion
-script from `./scripts/bash_completion` in the buck project.  Refer to
-the script's header comments for installation instructions.
+script from `./scripts/buck_completion.bash` in the buck project.  Refer
+to the script's header comments for installation instructions.
 
 
 [[eclipse]]
@@ -196,22 +200,23 @@
 [[documentation]]
 === Documentation
 
-To build only the documentation:
+To build only the documentation for testing or static hosting:
 
 ----
   buck build docs
 ----
 
-The generated html files will be placed in:
+The generated html files will NOT come with the search box, and will be
+placed in:
 
 ----
-  buck-out/gen/Documentation/html__tmp/Documentation
+  buck-out/gen/Documentation/searchfree__tmp/Documentation
 ----
 
-The html files will also be bundled into `html.zip` in this location:
+The html files will also be bundled into `searchfree.zip` in this location:
 
 ----
-  buck-out/gen/Documentation/html.zip
+  buck-out/gen/Documentation/searchfree.zip
 ----
 
 To build the executable WAR with the documentation included:
@@ -226,6 +231,26 @@
   buck-out/gen/withdocs.war
 ----
 
+[[soyc]]
+=== GWT Compile Report
+
+The GWT compiler can output a compile report (or "story of your compile"),
+describing the size of the JavaScript and which source classes contributed
+to the overall download size.
+
+----
+  buck build soyc
+----
+
+The report will be written as an HTML page to the extras directory, and
+can be opened and viewed in any web browser:
+
+----
+  extras/gerrit_ui/soycReport/compile-report/index.html
+----
+
+Only the "Split Point Report" is created, "Compiler Metrics" are not output.
+
 [[release]]
 === Gerrit Release WAR File
 
@@ -275,6 +300,7 @@
 The following groups of tests are currently supported:
 
 * api
+* edit
 * git
 * pgm
 * rest
@@ -288,6 +314,13 @@
   buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git:HttpPushForReviewIT
 ----
 
+To create test coverage report:
+
+----
+  buck test --code-coverage --code-coverage-format html --no-results-cache
+----
+
+The HTML report is created in `buck-out/gen/jacoco/code-coverage/index.html`.
 
 == Dependencies
 
@@ -378,6 +411,21 @@
   /home/<user>/projects/jgit/org.eclipse.jgit/target/org.eclipse.jgit-3.3.0-SNAPSHOT.jar
 ----
 
+After `buck clean` and `buck build lib/jgit:jgit` the symbolic link that was
+created the first time is lost due to Buck's caching mechanism. This means that
+when a new version of the local artifact is deployed (by running `mvn package`
+in the JGit project in the example above), Buck is not aware of it, because it
+still has a stale version of it in its cache.
+
+To solve this problem and re-create the symbolic link, you don't need to wipe out
+the entire Buck cache. Just rebuilding the target with the `--no-cache` option
+does the job:
+
+----
+  buck clean
+  buck build --no-cache lib/jgit:jgit
+----
+
 == Building against artifacts from custom Maven repositories
 
 To build against custom Maven repositories, two modes of operations are
@@ -437,6 +485,25 @@
   EOF
 ----
 
+[[clean-cache]]
+=== Cleaning The Buck Cache
+
+The cache for the Gerrit Code Review project is located in
+`~/.gerritcodereview/buck-cache/cache`.
+
+The Buck cache should never need to be manually deleted. If you find yourself
+deleting the Buck cache regularly, then it is likely that there is something
+wrong with your environment or your workflow.
+
+If you really do need to clean the cache manually, then:
+
+----
+ rm -rf ~/.gerritcodereview/buck-cache/cache
+----
+
+Note that the root `buck-cache` folder should not be deleted as this is where
+downloaded artifacts are stored.
+
 [[buck-daemon]]
 === Using Buck daemon
 
@@ -456,6 +523,15 @@
 To use `buckd` the additional
 link:https://facebook.github.io/watchman[watchman] program must be installed.
 
+To disable `buckd`, the environment variable `NO_BUCKD` must be set. It's not
+recommended to put it in the shell config, as it can be forgotten about it and
+then assumed Buck was working as it should when it should be using buckd.
+Prepend the variable to Buck invocation instead:
+
+----
+  NO_BUCKD=1 buck build gerrit
+----
+
 [[watchman]]
 === Installing watchman
 
@@ -517,13 +593,13 @@
 needs to be repeated, the unit test cache for that test must be removed first:
 
 ----
-  $ rm -rf buck-out/bin/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/.AddRemoveGroupMembersIT/
+  rm -rf buck-out/bin/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/.AddRemoveGroupMembersIT/
 ----
 
 After clearing the cache, the test can be run again:
 
 ----
-  $ buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group:AddRemoveGroupMembersIT
+  buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group:AddRemoveGroupMembersIT
   TESTING //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group:AddRemoveGroupMembersIT
   PASS  14,9s  8 Passed   0 Failed   com.google.gerrit.acceptance.rest.group.AddRemoveGroupMembersIT
   TESTS PASSED
@@ -557,6 +633,23 @@
 buck test --no-results-cache
 ----
 
+== Troubleshooting Buck
+
+In some cases problems with Buck itself need to be investigated. See for example
+link:https://gerrit-review.googlesource.com/62411[this attempt to upgrade Buck]
+and link:https://github.com/facebook/buck/pull/227[the fix that was needed] to
+make the update possible.
+
+To build Gerrit with a custom version of Buck, the following steps are necessary:
+
+1. In the Buck git apply any necessary changes from pull requests
+2. Compile Buck with `ant`
+3. In the root of the Gerrit project create a `.nobuckcheck` file to prevent Buck
+from updating itself
+4. Replace the sha1 in Gerrit's `.buckversion` file with the required version from
+the custom Buck build
+5. Build Gerrit as usual
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 95a5554..72d7ddf 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -1,33 +1,36 @@
 = Gerrit Code Review - Contributing
 
-Gerrit is developed as a self-hosting open source project and
-very much welcomes contributions from anyone with a contributor's
+== Introduction
+Gerrit is developed as a
+link:https://gerrit-review.googlesource.com/[self-hosting open source project]
+and very much welcomes contributions from anyone with a contributor's
 agreement on file with the project.
 
-* https://gerrit-review.googlesource.com/
-
+== Contributor License Agreement
 A Contributor License Agreement must be completed before contributions
 are accepted.  To view and accept the agreements do the following:
 
-* Click "Sign In" at the top right corner of https://gerrit-review.googlesource.com/
+* Click 'Sign In' at the top right corner of https://gerrit-review.googlesource.com/
 * Sign In with your Google account
-* After signing in, go to https://gerrit-review.googlesource.com/#/settings/agreements
-* Click "New Contributor Agreement" and follow the instructions
+* After signing in, go to the
+link:https://gerrit-review.googlesource.com/#/settings/agreements[Agreements]
+tab on the settings page
+* Click 'New Contributor Agreement' and follow the instructions
 
 For reference, the actual agreements are linked below
 
-* https://gerrit-review.googlesource.com/static/cla_individual.html
-* https://gerrit-review.googlesource.com/static/cla_corporate.html
+* link:https://cla.developers.google.com/about/android-individual[Individual Agreement]
+* link:https://source.android.com/source/cla-corporate.pdf[Corporate Agreement]
 
+== Code Review
 As Gerrit is a code review tool, naturally contributions will
 be reviewed before they will get submitted to the code base.  To
 start your contribution, please make a git commit and upload it
 for review to the main Gerrit review server.  To help speed up the
 review of your change, review these guidelines before submitting
 your change.  You can view the pending Gerrit contributions and
-their statuses here:
-
-* https://gerrit-review.googlesource.com/#/q/status:open+project:gerrit
+their statuses
+link:https://gerrit-review.googlesource.com/#/q/status:open+project:gerrit[here].
 
 Depending on the size of that list it might take a while for
 your change to get reviewed.  Naturally there are fewer
@@ -64,7 +67,7 @@
 
 
 [[commit-message]]
-== Commit Message
+=== Commit Message
 
 It is essential to have a good commit message if you want your
 change to be reviewed.
@@ -75,8 +78,22 @@
   * Followed by one or more explanatory paragraphs
   * Use the present tense (fix instead of fixed)
   * Use the past tense when describing the status before this commit
-  * Include a Bug: Issue <#> line if fixing a Gerrit issue
-  * Include a Change-Id line
+  * Include a `Bug: Issue <#>` line if fixing a Gerrit issue
+  * Include a `Change-Id` line
+
+=== Setting up Vim for Git commit message
+
+Git uses Vim as the default commit message editor. Put this into your
+`$HOME/.vimrc` file to configure Vim for Git commit message formatting
+and writing:
+
+====
+  " Enable spell checking, which is not on by default for commit messages.
+  au FileType gitcommit setlocal spell
+
+  " Reset textwidth if you've previously overridden it.
+  au FileType gitcommit setlocal textwidth=72
+====
 
 
 === A sample good Gerrit commit message:
@@ -97,8 +114,8 @@
   Change-Id: Ic4a7c07eeb98cdeaf44e9d231a65a51f3fceae52
 ====
 
-The Change-Id is, as usual, created by a local git hook.  To install it, simply
-copy it from the checkout and make it executable:
+The `Change-Id` line is, as usual, created by a local git hook.  To install it,
+simply copy it from the checkout and make it executable:
 
 ====
   cp ./gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg .git/hooks/
@@ -121,12 +138,12 @@
 ====
 
 The HTTPS access requires proper username and password; this can be obtained
-by clicking the "Obtain Password" link on the
+by clicking the 'Obtain Password' link on the
 link:https://gerrit-review.googlesource.com/#/settings/http-password[HTTP
 Password tab of the user settings page].
 
 
-== Style
+=== Style
 
 The basic coding style is covered by the tools/GoogleFormat.xml
 doc, see the link:dev-eclipse.html#Formatting[Eclipse Setup]
@@ -159,8 +176,28 @@
   * Put a blank line between external import sources, but not
     between internal ones.
 
+When to use `final` modifier and when not (in new code):
 
-== Code Organization
+Always:
+
+  * final fields: marking fields as final forces them to be
+  initialised in the constructor or at declaration
+  * final static fields: clearly communicates the intent
+  * to use final variables in inner anonymous classes
+
+Optional:
+
+  * final classes: use when appropriate, e.g. API restriction
+  * final methods: similar to final classes
+
+Never:
+
+  * local variables: it clutters the code, and make the code less
+  readable. When copying old code to new location, finals should
+  be removed
+  * method parameters: similar to local variables
+
+=== Code Organization
 
 Do your best to organize classes and methods in a logical way.
 Here are some guidelines that Gerrit uses:
@@ -179,14 +216,18 @@
     might appear near the instance methods which they help (but may
     also appear at the top).
   * Getters and setters for the same instance field should usually
-    be near each other baring a good reason not to.
+    be near each other barring a good reason not to.
   * If you are using assisted injection, the factory for your class
     should be before the instance members.
-  * Annotations should go before language keywords (final, private...) +
-    Example: @Assisted @Nullable final type varName
+  * Annotations should go before language keywords (`final`, `private`, etc) +
+    Example: `@Assisted @Nullable final type varName`
+  * The `@Inject`-ed constructor arguments should be listed one per line.
   * Imports should be mostly alphabetical (uppercase sorts before
     all lowercase, which means classes come before packages at the
     same level).
+  * Prefer to open multiple AutoCloseable resources in the same
+    try-with-resources block instead of nesting the try-with-resources
+    blocks and increasing the indentation level more than necessary.
 
 Wow that's a lot!  But don't worry, you'll get the habit and most
 of the code is organized this way already; so if you pay attention
@@ -195,7 +236,7 @@
 back and consult this section when creating them.
 
 
-== Design
+=== Design
 
 Here are some design level objectives that you should keep in mind
 when coding:
@@ -211,7 +252,7 @@
     mitigating this longer load by using a second RPC to fill in
     this data after the page is displayed (or alternatively it might
     be worth proposing caching this data).
-  * @Inject should be used on constructors, not on fields.  The
+  * `@Inject` should be used on constructors, not on fields.  The
     current exceptions are the ssh commands, these were implemented
     earlier in Gerrit's development.  To stay consistent, new ssh
     commands should follow this older pattern; but eventually these
@@ -227,12 +268,12 @@
   * ...and so is Guava (previously known as Google Collections).
 
 
-== Tests
+=== Tests
 
   * Tests for new code will greatly help your change get approved.
 
 
-== Change Size/Number of Files Touched
+=== Change Size/Number of Files Touched
 
 And finally, I probably cannot say enough about change sizes.
 Generally, smaller is better, hopefully within reason.  Do try to
@@ -265,9 +306,9 @@
   * Do only what the commit message describes.  In other words, things which
     are not strictly related to the commit message shouldn't be part of
     a change, even trivial things like externalizing a string somewhere
-    or fixing a typo.  This help keep "git blame" more useful in the future
-    and it also makes "git revert" more useful.
-  * Use topic branches to link your separate changes together.
+    or fixing a typo.  This helps keep `git blame` more useful in the future
+    and it also makes `git revert` more useful.
+  * Use topics to link your separate changes together.
 
 [[process]]
 == Process
@@ -283,6 +324,45 @@
 for review on that release's stable branch.  It will then be included in
 the master branch when the stable branch is merged back.
 
+=== Updating to new version of GWT
+
+When updating to a new version of GWT, there are several things that also need
+to be updated or at least checked.
+
+* Update common and plugin dependencies in `tools/gwt-constants.defs`.
+* Update to the same GWT version in the cookbook plugin and optionally in other
+plugins that have a dependency on GWT.
+* Update the GWT version in the archetype metadata in the
+`gerrit-plugin-gwt-archetype`.
+* Update to the same GWT version in the `gwtjsonrpc` project, and release a
+new version.
+
+=== Updating to new version of CodeMirror
+
+* Clone the git from https://github.com/codemirror/CodeMirror
+* Checkout the version needed
+* If the needed version is not a tagged version, use `git describe` to determine
+the version number:
++
+----
+ git describe --tags
+----
+
+* Create the release zip file:
++
+----
+ git archive --format=zip --prefix=codemirror-4.10.0-6-gd0a2dda/ d0a2dda > codemirror-4.10.0-6-gd0a2dda.zip
+----
+
+* Determine the sha1 hash of the zip file:
++
+----
+ openssl sha1 4.10.0-6-gd0a2dda.zip
+----
+
+* Upload the zip file to the
+link:https://console.developers.google.com/project/164060093628/storage/gerrit-maven/[
+gerrit-maven] storage bucket
 
 GERRIT
 ------
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index 384bb74..b3525a1 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -31,15 +31,13 @@
 
 Duplicate the existing launch configuration:
 
-* Run -> Debug Configurations ...
+* In Eclipse select Run -> Debug Configurations ...
 * Java Application -> `gerrit_daemon`
 * Right click, Duplicate
-
 * Modify the name to be unique.
-
 * Switch to Arguments tab.
 * Edit the `-d` program argument flag to match the path used during
-  'init'.  The template launch configuration resolves to ../gerrit_testsite
+  'init'.  The template launch configuration resolves to `../gerrit_testsite`
   since that is what the documentation recommends.
 
 * Switch to Common tab.
@@ -47,50 +45,39 @@
 * Close the Debug Configurations dialog and save the changes when prompted.
 
 
-=== Running Hosted Mode
+=== Running GWT Debug Mode
 
-Duplicate the existing launch configuration:
+The `gerrit_gwt_debug` launch configuration uses GWT's
+link:http://www.gwtproject.org/articles/superdevmode.html[Super Dev Mode].
 
-* Run -> Debug Configurations ...
-* Java Application -> `buck_gwt_debug`
-* Right click, Duplicate
+* Make a local copy of the `gerrit_gwt_debug` configuration, using the
+process described for `gerrit_daemon` above.
+* Launch the local copy of `gerrit_gwt_debug` from the Eclipse debug menu.
+* If debugging GWT for the first time:
 
-* Modify the name to be unique.
+** Open the link:http://localhost:9876/[codeserver URL] and add the `Dev Mode On`
+and `Dev Mode Off` bookmarklet to your bookmark bar.
 
-* Switch to Arguments tab.
-* Edit the `-Dgerrit.site_path=` VM argument to match the path
-  used during 'init'.  The template launch configuration resolves
-  to ../gerrit_testsite since that is what the documentation recommends.
+** Activate the source maps feature in your browser. Refer to the
+link:https://developer.chrome.com/devtools/docs/javascript-debugging#source-maps[
+Chrome] and
+link:https://developer.mozilla.org/en-US/docs/Tools/Debugger#Use_a_source_map[
+Firefox] developer documentation.
 
-* Switch to Common tab.
-* Change Save as to be Local file.
-* Close the Debug Configurations dialog and save the changes when prompted.
+* Load the link:http://localhost:8080[Gerrit page].
+* Open the source tab in developer tools.
+* Click the `Dev Mode On` bookmark to incrementally recompile changed files.
+* Select the `gerrit_ui` module to compile (the `Compile` button can also be used
+as a bookmarklet).
+* In the developer tools source tab, open a file and set a breakpoint.
+* Navigate to the UI and confirm that the breakpoint is hit.
+* To end the debugging session, click the `Dev Mode Off` bookmark.
 
+.After changing the client side code:
 
-[[known-problems]]
-== Known problems
-
-* OpenID authentication won't work in hosted mode, so you need to change
-the link:config-gerrit.html#auth.type[auth.type] configuration parameter
-to `DEVELOPMENT_BECOME_ANY_ACCOUNT` to disable OpenID and allow you to
-impersonate whatever account you otherwise would've used.
-
-* Error "Cannot create ReviewDb" occurs if the test site is already running.
-Stop the test site with `gerrit.sh stop` before attempting to run hosted mode
-debugging.
-
-* Gerrit site doesn't appear, only directory listing is shown. Web toolkit
-developer browser plugin is missing. If there is no warning, that browser
-plugin is missing with the suggestion to install it, you can install the
-right extension for your browser from the following locations:
-+
-https://dl.google.com/dl/gwt/plugins/chrome/gwt-dev-plugin.crx[Chrome]
-+
-link:https://dl.google.com/dl/gwt/plugins/firefox/gwt-dev-plugin.xpi[Firefox]
-+
-link:http://dl.google.com/dl/gwt/plugins/ie/1.0.7263.20091208111100/gwt-dev-plugin.msi[IE]
-+
-https://dl.google.com/dl/gwt/plugins/safari/gwt-dev-plugin.dmg[Safari]
+* Hitting `F5` in the browser only reloads the last compile output, without
+recompiling.
+* To reflect your changes in the debug session, click `Dev Mode On` then `Compile`.
 
 GERRIT
 ------
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index e619923..f9b66cb 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -274,7 +274,7 @@
 During their initialization plugins may get access to the
 `project.config` file of the `All-Projects` project and they are able
 to store configuration parameters in it. For this a plugin `InitStep`
-can get `com.google.gerrit.pgm.init.AllProjectsConfig` injected:
+can get `com.google.gerrit.pgm.init.api.AllProjectsConfig` injected:
 
 [source,java]
 ----
@@ -299,7 +299,7 @@
       ui.message("\n");
       ui.header(pluginName + " Integration");
       boolean enabled = ui.yesno(true, "By default enabled for all projects");
-      Config cfg = allProjectsConfig.load();
+      Config cfg = allProjectsConfig.load().getConfig();
       if (enabled) {
         cfg.setBoolean("plugin", pluginName, "enabled", enabled);
       } else {
@@ -378,9 +378,9 @@
 notifications of these events by implementing the corresponding
 listeners.
 
-* `com.google.gerrit.common.ChangeListener`:
+* `com.google.gerrit.common.EventListener`:
 +
-Allows to listen to change events. These are the same
+Allows to listen to events. These are the same
 link:cmd-stream-events.html#events[events] that are also streamed by
 the link:cmd-stream-events.html[gerrit stream-events] command.
 
@@ -412,7 +412,15 @@
 
 To send an event, the plugin must invoke one of the `postEvent`
 methods in the `ChangeHookRunner` class, passing an instance of
-its own custom event class derived from `ChangeEvent`.
+its own custom event class derived from
+`com.google.gerrit.server.events.Event`.
+
+Plugins which define new Events should register them via the
+`com.google.gerrit.server.events.EventTypes.registerClass()`
+method. This will make the EventType known to the system.
+Deserialzing events with the
+`com.google.gerrit.server.events.EventDeserializer` class requires
+that the event be registered in EventTypes.
 
 [[validation]]
 == Validation Listeners
@@ -655,9 +663,6 @@
                         .getStringList("branch", "refs/heads/master", "reviewer");
 ----
 
-The plugin configuration is loaded only once and is then cached.
-Similar to changes in 'gerrit.config', changes to the plugin
-configuration file will only become effective after a Gerrit restart.
 
 [[simple-project-specific-configuration]]
 == Simple Project Specific Configuration in `project.config`
@@ -1258,6 +1263,26 @@
 }
 ----
 
+`MenuItems` that are bound for the `MenuEntry` with the name
+`GerritTopMenu.PROJECTS` can contain a `${projectName}` placeholder
+which is automatically replaced by the actual project name.
+
+E.g. plugins may register an link:#http[HTTP Servlet] to handle project
+specific requests and add an menu item for this:
+
+[source,java]
+---
+  new MenuItem("My Screen", "/plugins/myplugin/project/${projectName}");
+---
+
+This also enables plugins to provide menu items for project aware
+screens:
+
+[source,java]
+---
+  new MenuItem("My Screen", "/x/my-screen/for/${projectName}");
+---
+
 If no Guice modules are declared in the manifest, the top menu extension may use
 auto-registration by providing an `@Listen` annotation:
 
@@ -1667,7 +1692,7 @@
 ----
 
 The auto registration only works for standard servlet mappings like
-`/foo` or `/foo/*`. Regex style bindings must use a Guice ServletModule
+`/foo` or `+/foo/*+`. Regex style bindings must use a Guice ServletModule
 to register the HTTP servlets and declare it explicitly in the manifest
 with the `Gerrit-HttpModule` attribute:
 
@@ -1706,6 +1731,34 @@
 }
 ----
 
+[[secure-store]]
+== SecureStore
+
+SecureStore allows to change the way Gerrit stores sensitive data like
+passwords.
+
+In order to replace the default SecureStore (no-op) implementation,
+a class that extends `com.google.gerrit.server.securestore.SecureStore`
+needs to be provided (with dependencies) in a separate jar file. Then
+link:pgm-SwitchSecureStore.html[SwitchSecureStore] must be run to
+switch implementations.
+
+The SecureStore implementation is instantiated using a Guice injector
+which binds the `File` annotated with the `@SitePath` annotation.
+This means that a SecureStore implementation class can get access to
+the `site_path` like in the following example:
+
+[source,java]
+----
+@Inject
+MySecureStore(@SitePath java.io.File sitePath) {
+  // your code
+}
+----
+
+No Guice bindings or modules are required. Gerrit will automatically
+discover and bind the implementation.
+
 [[download-commands]]
 == Download Commands
 
@@ -1732,34 +1785,41 @@
 ----
 import com.google.gerrit.extensions.annotations.Listen;
 import com.google.gerrit.extensions.webui.PatchSetWebLink;;
+import com.google.gerrit.extensions.webui.WebLinkTarget;
 
 @Listen
 public class MyWeblinkPlugin implements PatchSetWebLink {
 
   private String name = "MyLink";
   private String placeHolderUrlProjectCommit = "http://my.tool.com/project=%s/commit=%s";
+  private String imageUrl = "http://placehold.it/16x16.gif";
 
   @Override
-  public String getLinkName() {
-    return name ;
+  public WebLinkInfo getPatchSetWebLink(String projectName, String commit) {
+    return new WebLinkInfo(name,
+        imageUrl,
+        String.format(placeHolderUrlProjectCommit, project, commit),
+        WebLinkTarget.BLANK);
   }
-
-  @Override
-  public String getPatchSetUrl(String project, String commit) {
-    return String.format(placeHolderUrlProjectCommit, project, commit);
-  }
-
 }
 ----
 
+FileWebLinks will appear in the side-by-side diff screen on the right
+side of the patch selection on each side.
+
+DiffWebLinks will appear in the side-by-side and unified diff screen in
+the header next to the navigation icons.
+
 ProjectWebLinks will appear in the project list in the
 `Repository Browser` column.
 
+BranchWebLinks will appear in the branch list in the last column.
+
 [[documentation]]
 == Documentation
 
 If a plugin does not register a filter or servlet to handle URLs
-`/Documentation/*` or `/static/*`, the core Gerrit server will
+`+/Documentation/*+` or `+/static/*+`, the core Gerrit server will
 automatically export these resources over HTTP from the plugin JAR.
 
 Static resources under the `static/` directory in the JAR will be
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt
index da1ca70..bcf4a588 100644
--- a/Documentation/dev-readme.txt
+++ b/Documentation/dev-readme.txt
@@ -188,31 +188,6 @@
   http://localhost:8080/?dbg=1
 ----
 
-To use the GWT DETAILED style the package must be recompiled and
-`?dbg=1` must be omitted from the URL:
-
-----
-  mvn package -Dgwt.style=DETAILED
-----
-
-
-== Release Builds
-
-To create a release build for a production server, or deployment
-through the download site:
-
-----
-  ./tools/release.sh
-----
-
-If AsciiDoc isn't installed or is otherwise unavailable, the WAR
-can still be built without the embedded documentation by passing
-an additional flag:
-
-----
-  ./tools/release.sh --without-documentation
-----
-
 
 == Client-Server RPC
 
diff --git a/Documentation/dev-release-deploy-config.txt b/Documentation/dev-release-deploy-config.txt
index 364e61d..f3397a4 100644
--- a/Documentation/dev-release-deploy-config.txt
+++ b/Documentation/dev-release-deploy-config.txt
@@ -40,8 +40,8 @@
 * Generate and publish a PGP key
 +
 Generate and publish a PGP key as described in
-link:https://docs.sonatype.org/display/Repository/How+To+Generate+PGP+Signatures+With+Maven[
-How To Generate PGP Signatures With Maven].
+link:http://central.sonatype.org/pages/working-with-pgp-signatures.html[
+Working with PGP Signatures].
 +
 Please be aware that after publishing your public key it may take a
 while until it is visible to the Sonatype server.
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index 6cf9edf..3d3104c 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -122,23 +122,35 @@
 [[update-versions]]
 === Update Versions and Create Release Tag
 
-Before doing the release build the `GERRIT_VERSION` in the `VERSION`
-file must be updated, e.g. change if from `2.5-SNAPSHOT` to `2.5`.
+Before doing the release build, the `GERRIT_VERSION` in the `VERSION`
+file must be updated, e.g. change it from `2.5-SNAPSHOT` to `2.5`.
 
 In addition the version must be updated in a number of pom.xml files.
-To do this run the `./tools/version.sh` script and provide the new
+
+To do this run the `./tools/version.py` script and provide the new
 version as parameter, e.g.:
 
 ----
-  ./tools/version.sh 2.5
+  ./tools/version.py 2.5
 ----
 
+Also check and update the referenced `archetypeVersion` and the
+`archetypeRepository` in the `Documentation/dev-plugins.txt` file.
+If the referenced `archetypeVersion` will be available in the Maven central,
+delete the line with the `archetypeRepository`.
+
 Commit the changes and create the release tag on the new commit:
 
 ----
   git tag -a v2.5
 ----
 
+Tag the plugins:
+
+----
+  git submodule foreach git tag -a v2.5
+----
+
 [[build-gerrit]]
 === Build Gerrit
 
@@ -307,37 +319,50 @@
 [[push-tag]]
 ==== Push the Release Tag
 
-* Push the new Release Tag
-+
-For an `RC`:
-+
+Push the new Release Tag:
+
 ----
-  git push gerrit-review refs/tags/v2.5-rc0:refs/tags/v2.5-rc0
+  git push gerrit-review tag v2.5
 ----
-+
-For a final `stable` release:
-+
+
+Push the new Release Tag on the plugins:
+
 ----
-  git push gerrit-review refs/tags/v2.5:refs/tags/v2.5
+  git submodule foreach git push gerrit-review tag v2.5
 ----
 
 
 [[upload-documentation]]
 ==== Upload the Documentation
 
-Build the release notes:
-
+* Build the release notes:
++
 ----
   make -C ReleaseNotes
 ----
 
-* Upload html files to the storage bucket via `https://cloud.google.com/console` (manual via web browser)
-** Documentation html files must be extracted from `buck-out/gen/Documentation/html.zip`
-* Update Google Code project links
-** Go to http://code.google.com/p/gerrit/admin
-** Update the documentation link in the `Resources` section of the
+* Build the documentation:
++
+----
+  buck build docs
+----
+
+* Extract the documentation html files from the generated zip file
+`buck-out/gen/Documentation/searchfree.zip`.
+
+* Upload the html files manually via web browser to the
+link:https://console.developers.google.com/project/164060093628/storage/gerrit-documentation/[
+gerrit-documentation] storage bucket. The `gerrit-documentation`
+storage bucket is accessible via the
+link:https://cloud.google.com/console[Google Developers Console].
+
+[[update-links]]
+==== Update Google Code project links
+
+* Go to http://code.google.com/p/gerrit/admin
+* Update the documentation link in the `Resources` section of the
 Description text, and in the `Links` section.
-** Add a link to the new release notes in the `News` section of the
+* Add a link to the new release notes in the `News` section of the
 Description text
 
 [[update-issues]]
@@ -369,31 +394,6 @@
 ** A link to the docs
 ** Describe the type of release (stable, bug fix, RC)
 
-----
-To: Repo and Gerrit Discussion <repo-discuss@googlegroups.com>
-Subject: Announce: Gerrit 2.2.2.1  (Stable bug fix update)
-
-I am pleased to announce Gerrit Code Review 2.2.2.1.
-
-Download:
-
-  http://code.google.com/p/gerrit/downloads/list
-
-
-This release is a stable bug fix release with some
-documentation updates including a new "Contributing to
-Gerrit" doc:
-
-  http://gerrit-documentation.googlecode.com/svn/Documentation/2.2.2/dev-contributing.html
-
-
-To read more about the bug fixes:
-
-  http://gerrit-documentation.googlecode.com/svn/ReleaseNotes/ReleaseNotes-2.2.2.1.html
-
--Martin
-----
-
 * Add an entry to the `NEWS` section of the main Gerrit project web page
 ** Go to: http://code.google.com/p/gerrit/admin
 ** Add entry like:
@@ -404,9 +404,8 @@
 * Update the new discussion group announcement to be sticky
 ** Go to: http://groups.google.com/group/repo-discuss/topics
 ** Click on the announcement thread
-** Near the top right, click on options
-** Under options, click the "Display this top first" checkbox
-** and Save
+** Near the top right, click on actions
+** Under actions, click the "Display this top first" checkbox
 
 * Update the previous discussion group announcement to no longer be sticky
 ** See above (unclick checkbox)
@@ -415,11 +414,19 @@
 [[increase-version]]
 === Increase Gerrit Version for Current Development
 
-All new development that is done in the `master` branch will be
-included in the next Gerrit release. Update the Gerrit version in the
-`VERSION` file, and plugin archetypes' `pom.xml` files. Push the change
-for review and get it merged.
+All new development that is done in the `master` branch will be included in the
+next Gerrit release. The Gerrit version should be set to the snapshot version
+for the next release.
 
+Use the `version` tool to set the version in the `VERSION` file and plugin
+archetypes' `pom.xml` files:
+
+----
+ ./tools/version.py 2.11-SNAPSHOT
+----
+
+Verify that the changes made by the tool are sane, then commit them, push
+the change for review on the master branch, and get it merged.
 
 [[merge-stable]]
 === Merge `stable` into `master`
diff --git a/Documentation/dev-rest-api.txt b/Documentation/dev-rest-api.txt
index ec4b666..308d4bd 100644
--- a/Documentation/dev-rest-api.txt
+++ b/Documentation/dev-rest-api.txt
@@ -47,7 +47,7 @@
 Example to set a Gerrit project's link:rest-api-projects.html#set-project-description[description]:
 
 ----
- curl -X PUT --digest --user john:2LlAB3K9B0PF --data-binary @project-desc.txt --header "Content-Type: application/json;charset=UTF-8" http://localhost:8080/a/projects/myproject/description
+ curl -X PUT --digest --user john:2LlAB3K9B0PF --data-binary @project-desc.txt --header "Content-Type: application/json; charset=UTF-8" http://localhost:8080/a/projects/myproject/description
 ----
 
 === Authentication
diff --git a/Documentation/doc.css b/Documentation/doc.css.in
similarity index 93%
rename from Documentation/doc.css
rename to Documentation/doc.css.in
index c49b596..6be89f6 100644
--- a/Documentation/doc.css
+++ b/Documentation/doc.css.in
@@ -24,6 +24,7 @@
   margin: 0.2em 0 0.2em 0;
 }
 
+#license > .content,
 .listingblock > .content {
   border: 2px solid silver;
   background: #ebebeb;
@@ -33,6 +34,7 @@
   overflow: auto;
 }
 
+#license > .content pre,
 .listingblock > .content pre {
   background: none;
   border: 0 solid silver;
diff --git a/Documentation/error-change-does-not-belong-to-project.txt b/Documentation/error-change-does-not-belong-to-project.txt
index ce9c23a1e..21596b1 100644
--- a/Documentation/error-change-does-not-belong-to-project.txt
+++ b/Documentation/error-change-does-not-belong-to-project.txt
@@ -6,7 +6,7 @@
 This error message means that the user explicitly pushed a commit to
 a change that belongs to another project by specifying it as target
 ref. This way of adding a new patch set to a change is deprecated as
-explained link:user-upload.html#manual_replacement_mapping[here]. It is recommended to only rely on Change-IDs for
+explained link:user-upload.html#manual_replacement_mapping[here]. It is recommended to only rely on Change-Ids for
 link:user-upload.html#push_replace[replacing changes].
 
 
diff --git a/Documentation/error-change-not-found.txt b/Documentation/error-change-not-found.txt
index 98969b0..df99388 100644
--- a/Documentation/error-change-not-found.txt
+++ b/Documentation/error-change-not-found.txt
@@ -6,7 +6,7 @@
 This error message means that the user explicitly pushed a commit to
 a non-existing change by specifying it as target ref. This way of
 adding a new patch set to a change is deprecated as explained link:user-upload.html#manual_replacement_mapping[here].
-It is recommended to only rely on Change-IDs for link:user-upload.html#push_replace[replacing changes].
+It is recommended to only rely on Change-Ids for link:user-upload.html#push_replace[replacing changes].
 
 
 GERRIT
diff --git a/Documentation/error-has-duplicates.txt b/Documentation/error-has-duplicates.txt
index c34bf52..0ec6eda 100644
--- a/Documentation/error-has-duplicates.txt
+++ b/Documentation/error-has-duplicates.txt
@@ -1,20 +1,20 @@
 = \... has duplicates
 
 With this error message Gerrit rejects to push a commit if its commit
-message contains a Change-ID for which multiple changes can be found
+message contains a Change-Id for which multiple changes can be found
 in the project.
 
 This error means that there is an inconsistency in Gerrit since for
-one project there are multiple changes that have the same Change-ID.
-Every change is expected to have an unique Change-ID.
+one project there are multiple changes that have the same Change-Id.
+Every change is expected to have an unique Change-Id.
 
 Since this error should never occur in practice, you should inform
 your Gerrit administrator if you hit this problem and/or
 link:http://code.google.com/p/gerrit/issues/list[open a Gerrit issue].
 
 In any case to not be blocked with your work, you can simply create a
-new Change-ID for your commit and then push it as new change to
-Gerrit. How to exchange the Change-ID in the commit message of your
+new Change-Id for your commit and then push it as new change to
+Gerrit. How to exchange the Change-Id in the commit message of your
 commit is explained link:error-push-fails-due-to-commit-message.html[here].
 
 
diff --git a/Documentation/error-missing-changeid.txt b/Documentation/error-missing-changeid.txt
index 2a93760..9cddd85 100644
--- a/Documentation/error-missing-changeid.txt
+++ b/Documentation/error-missing-changeid.txt
@@ -45,7 +45,7 @@
 
 If the Change-Id is contained in the commit message but not in its
 last paragraph you have to update the commit message and move the
-Change-ID into the last paragraph. How to update the commit message
+Change-Id into the last paragraph. How to update the commit message
 is explained link:error-push-fails-due-to-commit-message.html[here].
 
 
diff --git a/Documentation/error-prohibited-by-gerrit.txt b/Documentation/error-prohibited-by-gerrit.txt
index 4c7bf22..3d9bbad 100644
--- a/Documentation/error-prohibited-by-gerrit.txt
+++ b/Documentation/error-prohibited-by-gerrit.txt
@@ -9,32 +9,32 @@
 1. if you push a commit for code review to a branch for which you
    don't have upload permissions (access right
    link:access-control.html#category_push_review['Push'] on
-   `refs/for/refs/heads/*`)
+   `+refs/for/refs/heads/*+`)
 2. if you bypass code review without
    link:access-control.html#category_push_direct['Push'] access right
-   on `refs/heads/*`
+   on `+refs/heads/*+`
 3. if you bypass code review pushing to a non-existing branch without
    link:access-control.html#category_create['Create Reference'] access
-   right on `refs/heads/*`
+   right on `+refs/heads/*+`
 4. if you push an annotated tag without
    link:access-control.html#category_push_annotated['Push Annotated Tag']
-   access right on 'refs/tags/*'
+   access right on `+refs/tags/*+`
 5. if you push a signed tag without
    link:access-control.html#category_push_signed['Push Signed Tag']
-   access right on 'refs/tags/*'
+   access right on `+refs/tags/*+`
 6. if you push a lightweight tag without the access right link:access-control.html#category_create['Create
-   Reference'] for the reference name 'refs/tags/*'
+   Reference'] for the reference name `+refs/tags/*+`
 7. if you push a tag with somebody else as tagger and you don't have the
    link:access-control.html#category_forge_committer['Forge Committer']
-   access right for the reference name 'refs/tags/*'
+   access right for the reference name `+refs/tags/*+`
 8. if you push to a project that is in state 'Read Only'
 
 For new users it often happens that they accidentally try to bypass
 code review. The push then fails with the error message 'prohibited
 by Gerrit' because the project didn't allow to bypass code review.
-Bypassing the code review is done by pushing directly to refs/heads/*
-(e.g. refs/heads/master) instead of pushing to refs/for/* (e.g.
-refs/for/master). Details about how to push commits for code review
+Bypassing the code review is done by pushing directly to `+refs/heads/*+`
+(e.g. `refs/heads/master`) instead of pushing to `+refs/for/*+` (e.g.
+`refs/for/master`). Details about how to push commits for code review
 are explained link:user-upload.html#push_create[here].
 
 
diff --git a/Documentation/error-squash-commits-first.txt b/Documentation/error-squash-commits-first.txt
index b3b9d56..4069d5b 100644
--- a/Documentation/error-squash-commits-first.txt
+++ b/Documentation/error-squash-commits-first.txt
@@ -1,7 +1,7 @@
 = squash commits first
 
 With this error message Gerrit rejects to push a commit if it
-contains the same Change-ID as a predecessor commit.
+contains the same Change-Id as a predecessor commit.
 
 The reason for rejecting such a commit is that it would introduce, for
 the corresponding change in Gerrit, a dependency upon itself. Gerrit
@@ -14,7 +14,7 @@
 review comments and creates a new commit instead of amending the
 existing commit. Another possibility for this error, although less
 likely, is that the user tried to create a patch series with multiple
-changes to be reviewed and accidentally included the same Change-ID
+changes to be reviewed and accidentally included the same Change-Id
 into the different commit messages.
 
 
@@ -22,8 +22,8 @@
 
 Here an example about how the push is failing. Please note that the
 two commits 'one commit' and 'another commit' both have the same
-Change-ID (of course in real life it can happen that there are more
-than two commits that have the same Change-ID).
+Change-Id (of course in real life it can happen that there are more
+than two commits that have the same Change-Id).
 
 ----
   $ git log
@@ -54,11 +54,12 @@
   error: failed to push some refs to 'ssh://JohnDoe@host:29418/myProject'
 ----
 
-If it was the intention to rework on a change and to push a new patch
-set the problem can be fixed by squashing the commits that contain the
-same Change-ID. The squashed commit can then be pushed to Gerrit.
-To squash the commits use git rebase to do an interactive rebase. For
-the example above where the last two commits have the same Change-ID
+If it was the intention to rework a change and push a new patch
+set, the problem can be fixed by squashing the commits that contain the
+same Change-Id. The squashed commit can then be pushed to Gerrit.
+
+To squash the commits, use `git rebase -i` to do an interactive rebase. For
+the example above where the last two commits have the same Change-Id,
 this means an interactive rebase for the last two commits should be
 done. For further details about the git rebase command please check
 the link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[Git documentation for rebase].
@@ -92,11 +93,11 @@
 
 If it was the intention to create a patch series with multiple
 changes to be reviewed, each commit message should contain the
-Change-ID of the corresponding change in Gerrit.  If a change in
-Gerrit does not exist yet, the Change-ID should be generated (either
-by using a link:cmd-hook-commit-msg.html[commit hook] or by using EGit) or the Change-ID could be
+Change-Id of the corresponding change in Gerrit.  If a change in
+Gerrit does not exist yet, the Change-Id should be generated (either
+by using a link:cmd-hook-commit-msg.html[commit hook] or by using EGit) or the Change-Id could be
 removed (not recommended since then amending this commit to create
-subsequent patch sets is more error prone). To change the Change-ID
+subsequent patch sets is more error prone). To change the Change-Id
 of an existing commit do an interactive link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[git rebase] and fix the
 affected commit messages.
 
diff --git a/Documentation/gen_licenses.py b/Documentation/gen_licenses.py
index 61d2e24..af7cd88 100755
--- a/Documentation/gen_licenses.py
+++ b/Documentation/gen_licenses.py
@@ -134,10 +134,12 @@
       p = d[d.index(':')+1:].lower()
     print('* ' + p)
   print()
-  print('----')
+  print('[[license]]')
+  print('[verse]')
+  print('--')
   with open(n[2:].replace(':', '/')) as fd:
     copyfileobj(fd, stdout)
-  print('----')
+  print('--')
 
 print("""
 GERRIT
diff --git a/Documentation/images/inline-edit-add-file-suggestion.png b/Documentation/images/inline-edit-add-file-suggestion.png
new file mode 100644
index 0000000..25a33f8
--- /dev/null
+++ b/Documentation/images/inline-edit-add-file-suggestion.png
Binary files differ
diff --git a/Documentation/images/inline-edit-confirm-unsaved-edits.png b/Documentation/images/inline-edit-confirm-unsaved-edits.png
new file mode 100644
index 0000000..87e4a32
--- /dev/null
+++ b/Documentation/images/inline-edit-confirm-unsaved-edits.png
Binary files differ
diff --git a/Documentation/images/inline-edit-create-change-project-screen-dialog.png b/Documentation/images/inline-edit-create-change-project-screen-dialog.png
new file mode 100644
index 0000000..ea5daa9
--- /dev/null
+++ b/Documentation/images/inline-edit-create-change-project-screen-dialog.png
Binary files differ
diff --git a/Documentation/images/inline-edit-create-change-project-screen.png b/Documentation/images/inline-edit-create-change-project-screen.png
new file mode 100644
index 0000000..e9c7033
--- /dev/null
+++ b/Documentation/images/inline-edit-create-change-project-screen.png
Binary files differ
diff --git a/Documentation/images/inline-edit-create-follow-up-change.png b/Documentation/images/inline-edit-create-follow-up-change.png
new file mode 100644
index 0000000..3e81eee
--- /dev/null
+++ b/Documentation/images/inline-edit-create-follow-up-change.png
Binary files differ
diff --git a/Documentation/images/inline-edit-edit-in-diff-screen-patch-list.png b/Documentation/images/inline-edit-edit-in-diff-screen-patch-list.png
new file mode 100644
index 0000000..bdbc59d
--- /dev/null
+++ b/Documentation/images/inline-edit-edit-in-diff-screen-patch-list.png
Binary files differ
diff --git a/Documentation/images/inline-edit-edit-in-patch-list.png b/Documentation/images/inline-edit-edit-in-patch-list.png
new file mode 100644
index 0000000..9a31e02
--- /dev/null
+++ b/Documentation/images/inline-edit-edit-in-patch-list.png
Binary files differ
diff --git a/Documentation/images/inline-edit-enter-edit-mode-from-diff.png b/Documentation/images/inline-edit-enter-edit-mode-from-diff.png
new file mode 100644
index 0000000..46dd0ff
--- /dev/null
+++ b/Documentation/images/inline-edit-enter-edit-mode-from-diff.png
Binary files differ
diff --git a/Documentation/images/inline-edit-enter-edit-mode-from-file-list.png b/Documentation/images/inline-edit-enter-edit-mode-from-file-list.png
new file mode 100644
index 0000000..b8c52c9
--- /dev/null
+++ b/Documentation/images/inline-edit-enter-edit-mode-from-file-list.png
Binary files differ
diff --git a/Documentation/images/inline-edit-file-list-in-edit-mode.png b/Documentation/images/inline-edit-file-list-in-edit-mode.png
new file mode 100644
index 0000000..8f355335
--- /dev/null
+++ b/Documentation/images/inline-edit-file-list-in-edit-mode.png
Binary files differ
diff --git a/Documentation/images/inline-edit-full-screen-editor.png b/Documentation/images/inline-edit-full-screen-editor.png
new file mode 100644
index 0000000..474fae5
--- /dev/null
+++ b/Documentation/images/inline-edit-full-screen-editor.png
Binary files differ
diff --git a/Documentation/images/user-review-ui-change-screen-edit-commit-message.png b/Documentation/images/user-review-ui-change-screen-edit-commit-message.png
deleted file mode 100644
index 615e9a7..0000000
--- a/Documentation/images/user-review-ui-change-screen-edit-commit-message.png
+++ /dev/null
Binary files differ
diff --git a/Documentation/images/user-review-ui-change-view-preference.png b/Documentation/images/user-review-ui-change-view-preference.png
deleted file mode 100644
index 825b7f6..0000000
--- a/Documentation/images/user-review-ui-change-view-preference.png
+++ /dev/null
Binary files differ
diff --git a/Documentation/images/user-review-ui-side-by-side-diff-screen-preferences-popup.png b/Documentation/images/user-review-ui-side-by-side-diff-screen-preferences-popup.png
index 35e29a3..043c1ff 100644
--- a/Documentation/images/user-review-ui-side-by-side-diff-screen-preferences-popup.png
+++ b/Documentation/images/user-review-ui-side-by-side-diff-screen-preferences-popup.png
Binary files differ
diff --git a/Documentation/index.txt b/Documentation/index.txt
index e0a823b..89661fe 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -9,6 +9,7 @@
 .. Registering a new Gerrit account
 .. link:user-review-ui.html[Reviewing Changes]
 .. link:user-search.html[Searching Changes]
+.. link:user-inline-edit.html[Manipulating Changes in Browser]
 .. link:user-notify.html[Subscribing to Email Notifications]
 . SSH
 .. SSH connection details
@@ -57,26 +58,26 @@
 .. How to read stats from the JVM
 . High availability
 . Replication
-. link:https://gerrit-review.googlesource.com/#/admin/projects/?filter=plugins%252F[Plugins]
-. link:dev-design.html[System Design]
+. link:config-plugins.html[Plugins]
 . link:config-contact.html[User Contact Information]
 . link:config-reverseproxy.html[Reverse Proxy]
 . link:config-auto-site-initialization.html[Automatic Site Initialization on Startup]
 . link:pgm-index.html[Server Side Administrative Tools]
 
 == Developer
-. link:dev-readme.html[Developer Setup]
-. link:dev-buck.html[Building with Buck]
-. link:dev-build-plugins.html[Building Gerrit plugins]
-. link:dev-eclipse.html[Eclipse Setup]
-. link:dev-contributing.html[Contributing to Gerrit]
+. Getting Started
+.. link:dev-readme.html[Developer Setup]
+.. link:dev-eclipse.html[Eclipse Setup]
+.. link:dev-buck.html[Building with Buck]
+.. link:dev-contributing.html[Contributing to Gerrit]
+. Plugin Development
+.. link:dev-plugins.html[Developing Plugins]
+.. link:dev-build-plugins.html[Building Gerrit plugins]
+.. link:js-api.html[JavaScript Plugin API]
+.. link:config-validation.html[Validation Interfaces]
 . Documentation formatting guide for contributions
 . link:dev-design.html[System Design]
 . link:i18n-readme.html[i18n Support]
-. Plugin development
-.. link:dev-plugins.html[Developing Plugins]
-.. link:js-api.html[JavaScript Plugin API]
-.. link:config-validation.html[Commit Validation]
 
 == Maintainer
 . link:dev-release.html[Developer Release]
diff --git a/Documentation/intro-project-owner.txt b/Documentation/intro-project-owner.txt
index c55a43c..461aa91 100644
--- a/Documentation/intro-project-owner.txt
+++ b/Documentation/intro-project-owner.txt
@@ -10,7 +10,7 @@
 Being project owner means that you own a project in Gerrit.
 Technically this is expressed by having the
 link:access-control.html#category_owner[Owner] access right on
-`refs/*` on that project. As project owner you have the permission to
+`+refs/*+` on that project. As project owner you have the permission to
 edit the access control list and the project settings of the project.
 It also means that you should get familiar with these settings so that
 you can adapt them to the needs of your project.
@@ -127,12 +127,12 @@
 `refs/heads/master` but also on ref patterns and regular expressions
 for ref names.
 
-A ref pattern ends with `/*` and describes a complete ref name
-namespace, e.g. access rights assigned on `refs/heads/*` apply to all
+A ref pattern ends with `+/*+` and describes a complete ref name
+namespace, e.g. access rights assigned on `+refs/heads/*+` apply to all
 branches.
 
 Regular expressions must start with `^`, e.g. access rights assigned
-on `^refs/heads/rel-.*` would apply to all `rel-*` branches.
+on `+^refs/heads/rel-.*+` would apply to all `+rel-*+` branches.
 
 [[groups]]
 === Groups
diff --git a/Documentation/intro-quick.txt b/Documentation/intro-quick.txt
index 9ff6058..bb80134 100644
--- a/Documentation/intro-quick.txt
+++ b/Documentation/intro-quick.txt
@@ -251,7 +251,7 @@
 link:user-changeid.html[Change-Id commit-msg hook]
 before we uploaded the change, re-working it is easy. All we need
 to do to upload a re-worked change is to push another commit that has
-the same Change-Id in the message. Since the hook added a Change-ID in
+the same Change-Id in the message. Since the hook added a Change-Id in
 our initial commit we can simply checkout and then amend that commit.
 Then push it to Gerrit in the same way as we did to create the review. E.g.
 
diff --git a/Documentation/js-api.txt b/Documentation/js-api.txt
index 883198a..43e4336 100644
--- a/Documentation/js-api.txt
+++ b/Documentation/js-api.txt
@@ -60,6 +60,11 @@
   a string, otherwise the result is a JavaScript object or array,
   as described in the relevant REST API documentation.
 
+[[self_getCurrentUser]]
+=== self.getCurrentUser()
+Returns the currently signed in user's AccountInfo data; empty account
+data if no user is currently signed in.
+
 [[self_getPluginName]]
 === self.getPluginName()
 Returns the name this plugin was installed as by the server
@@ -168,7 +173,7 @@
 self.onAction(type, view_name, callback);
 ----
 
-* type: `'change'`, `'revision'`, `'project'`, or `'branch'`
+* type: `'change'`, `'edit'`, `'revision'`, `'project'`, or `'branch'`
   indicating which type of resource the `UiAction` was bound to
   in the server.
 
@@ -625,6 +630,11 @@
 });
 ----
 
+[[Gerrit_getCurrentUser]]
+=== Gerrit.getCurrentUser()
+Returns the currently signed in user's AccountInfo data; empty account
+data if no user is currently signed in.
+
 [[Gerrit_getPluginName]]
 === Gerrit.getPluginName()
 Returns the name this plugin was installed as by the server
@@ -838,8 +848,8 @@
 Gerrit.onAction(type, view_name, callback);
 ----
 
-* type: `'change'`, `'revision'`, `'project'` or `'branch'` indicating
-  what sort of resource the `UiAction` was bound to in the server.
+* type: `'change'`, `'edit'`, `'revision'`, `'project'` or `'branch'`
+  indicating what sort of resource the `UiAction` was bound to in the server.
 
 * view_name: string appearing in URLs to name the view. This is the
   second argument of the `get()`, `post()`, `put()`, and `delete()`
@@ -877,6 +887,10 @@
 === Gerrit.refreshMenuBar()
 Refreshes Gerrit's menu bar.
 
+[[Gerrit_isSignedIn]]
+=== Gerrit.isSignedIn()
+Checks if user is signed in.
+
 [[Gerrit_url]]
 === Gerrit.url()
 Returns the URL of the Gerrit Code Review server. If invoked with
diff --git a/Documentation/json.txt b/Documentation/json.txt
index b45f404..feef1a1 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -35,8 +35,6 @@
 lastUpdated:: Time in seconds since the UNIX epoch when this change
 was last updated.
 
-sortKey:: Internal key used to sort changes, based on lastUpdated.
-
 open:: Boolean indicating if the change is still open for review.
 
 status:: Current state of this change.
@@ -121,7 +119,9 @@
 
   TRIVIAL_REBASE;; Conflict-free merge between the new parent and the prior patch set.
 
-  NO_CODE_CHANGE;; No code changed; same tree and same parents.
+  NO_CODE_CHANGE;; No code changed; same tree and same parent tree.
+
+  NO_CHANGE;; No changes; same commit message, same tree and same parent tree.
 
 approvals:: The <<approval,approval attribute>> granted.
 
diff --git a/Documentation/pgm-SwitchSecureStore.txt b/Documentation/pgm-SwitchSecureStore.txt
new file mode 100644
index 0000000..f9b2aa4
--- /dev/null
+++ b/Documentation/pgm-SwitchSecureStore.txt
@@ -0,0 +1,39 @@
+= SwitchSecureStore
+
+== NAME
+SwitchSecureStore - Changes the currently used SecureStore implementation
+
+== SYNOPSIS
+--
+'java' -jar gerrit.war 'SwitchSecureStore' [<OPTIONS>]
+--
+
+== DESCRIPTION
+Changes the SecureStore implementation used by Gerrit. It migrates all data
+stored in the old implementation, removes the old implementation jar file
+from `$site_path/lib` and puts the new one there. As a final step
+the link:config-gerrit.html#gerrit.secureStoreClass[gerrit.secureStoreClass]
+property of `gerrit.config` will be updated.
+
+All dependencies not provided by Gerrit should be put the in `$site_path/lib`
+directory manually, before running the `SwitchSecureStore` program.
+
+After this operation there is no automatic way back the to standard Gerrit no-op
+secure store implementation, however there is a manual procedure:
+* stop Gerrit,
+* remove SecureStore jar file from `$site_path/lib`,
+* put plain text passwords into `$site_path/etc/secure.conf` file,
+* start Gerrit.
+
+== OPTIONS
+
+--new-secure-store-lib::
+	Path to jar file with new SecureStore implementation. Jar dependencies must be
+	put in `$site_path/lib` directory.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/pgm-daemon.txt b/Documentation/pgm-daemon.txt
index 3d7dd45..bcf2b1b 100644
--- a/Documentation/pgm-daemon.txt
+++ b/Documentation/pgm-daemon.txt
@@ -37,7 +37,7 @@
 --enable-httpd::
 --disable-httpd::
 	Enable (or disable) the internal HTTP daemon, answering
-	web requests.  Enabled by default.
+	web requests. Enabled by default when --slave is not used.
 
 --enable-sshd::
 --disable-sshd::
@@ -51,7 +51,7 @@
     or updates existing ones) or link:cmd-review.html[review]
     (sets approve marks) are disabled.
 +
-This option automatically implies '--disable-httpd --enable-sshd'.
+This option automatically implies '--enable-sshd'.
 
 --console-log::
 	Send log messages to the console, instead of to the standard
diff --git a/Documentation/pgm-index.txt b/Documentation/pgm-index.txt
index 3bb6182..bf6dc57 100644
--- a/Documentation/pgm-index.txt
+++ b/Documentation/pgm-index.txt
@@ -24,6 +24,9 @@
 link:pgm-reindex.html[reindex]::
 	Rebuild the secondary index.
 
+link:pgm-SwitchSecureStore.html[SwitchSecureStore]::
+	Change used SecureStore implementation.
+
 link:pgm-rulec.html[rulec]::
 	Compile project-specific Prolog rules to JARs.
 
diff --git a/Documentation/project-configuration.txt b/Documentation/project-configuration.txt
index 4b755f3..94af04f 100644
--- a/Documentation/project-configuration.txt
+++ b/Documentation/project-configuration.txt
@@ -58,7 +58,7 @@
 [[fast_forward_only]]
 * Fast Forward Only
 +
-This method produces a strictly linear history.  All merges must
+With this method no merge commits are produced. All merges must
 be handled on the client, prior to uploading to Gerrit for review.
 +
 To submit a change, the change must be a strict superset of the
@@ -150,6 +150,25 @@
 are not able to see the project even if they have read permissions
 granted on the project.
 
+=== Use target branch when determining new changes to open
+
+The `create-new-change-for-all-not-in-target` option provides a
+convenience for selecting link:user-upload.html#base[the merge base]
+by setting it automatically to the target branch's tip so you can
+create new changes for all commits not in the target branch.
+
+This option is disabled if the tip of the push is a merge commit.
+
+This option also only works if there are no merge commits in the
+commit chain, in such cases it fails warning the user that such
+pushes can only be performed by manually specifying
+link:user-upload.html#base[bases]
+
+This option is useful if you want to push a change to your personal
+branch first and for review to another branch for example. Or in cases
+where a commit is already merged into a branch and you want to create
+a new open change for that commit on another branch.
+
 === Require Change-Id
 
 The `Require Change-Id in commit message` option defines whether a
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
index 5bff6bf..43caa83 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -2,11 +2,11 @@
 
 [[SubmitRule]]
 == Submit Rule
-A 'Submit Rule' in Gerrit is logic that defines when a change is submittable.
+A _Submit Rule_ in Gerrit is logic that defines when a change is submittable.
 By default, a change is submittable when it gets at least one
 highest vote in each voting category and has no lowest vote (aka veto vote) in
-any category.  Typically, this means that a change needs 'Code-Review+2',
-'Verified+1' and has neither 'Code-Review-2' nor 'Verified-1' to become
+any category.  Typically, this means that a change needs `Code-Review+2`,
+`Verified+1` and has neither `Code-Review-2` nor `Verified-1` to become
 submittable.
 
 While this rule is a good default, there are projects which need more
@@ -29,7 +29,7 @@
 
 [[SubmitType]]
 == Submit Type
-A 'Submit Type' is a strategy that is used on submit to integrate the
+A _Submit Type_ is a strategy that is used on submit to integrate the
 change into the destination branch. Supported submit types are:
 
 * `Fast Forward Only`
@@ -38,7 +38,7 @@
 * `Cherry Pick`
 * `Rebase If Necessary`
 
-'Submit Type' is a project global setting. This means that the same submit type
+_Submit Type_ is a project global setting. This means that the same submit type
 is used for all changes of one project.
 
 Projects which need more flexibility in choosing, or enforcing, a submit type
@@ -51,14 +51,15 @@
 == Prolog Language
 This document is not a complete Prolog tutorial.
 link:http://en.wikipedia.org/wiki/Prolog[This Wikipedia page on Prolog] is a
-good starting point for learning the Prolog language. This document will only explain
-some elements of Prolog that are necessary to understand the provided examples.
+good starting point for learning the Prolog language. This document will only
+explain some elements of Prolog that are necessary to understand the provided
+examples.
 
 == Prolog in Gerrit
 Gerrit uses its own link:https://code.google.com/p/prolog-cafe/[fork] of the
 original link:http://kaminari.istc.kobe-u.ac.jp/PrologCafe/[prolog-cafe]
-project. Gerrit embeds the prolog-cafe library and can interpret Prolog programs at
-runtime.
+project. Gerrit embeds the prolog-cafe library and can interpret Prolog programs
+at runtime.
 
 == Interactive Prolog Cafe Shell
 For interactive testing and playing with Prolog, Gerrit provides the
@@ -66,7 +67,8 @@
 Prolog interpreter shell.
 
 NOTE: The interactive shell is just a prolog shell, it does not load
-a gerrit server environment and thus is not intended for xref:TestingSubmitRules[testing submit rules].
+a gerrit server environment and thus is not intended for
+xref:TestingSubmitRules[testing submit rules].
 
 == SWI-Prolog
 Instead of using the link:pgm-prolog-shell.html[prolog-shell] program one can
@@ -94,8 +96,8 @@
 
 [[HowToWriteSubmitRules]]
 == How to write submit rules
-Whenever Gerrit needs to evaluate submit rules for a change `C` from project `P` it
-will first initialize the embedded Prolog interpreter by:
+Whenever Gerrit needs to evaluate submit rules for a change `C` from project `P`
+it will first initialize the embedded Prolog interpreter by:
 
 * consulting a set of facts about the change `C`
 * consulting the `rules.pl` from the project `P`
@@ -126,9 +128,9 @@
 By default, Gerrit will search for a `submit_rule/1` predicate in the `rules.pl`
 file, evaluate the `submit_rule(X)` and then inspect the value of `X` in order
 to decide whether the change is submittable or not and also to find the set of
-needed criteria for the change to become submittable. This means that Gerrit has an
-expectation on the format and value of the result of the `submit_rule` predicate
-which is expected to be a `submit` term of the following format:
+needed criteria for the change to become submittable. This means that Gerrit has
+an expectation on the format and value of the result of the `submit_rule`
+predicate which is expected to be a `submit` term of the following format:
 
 ====
   submit(label(label-name, status) [, label(label-name, status)]*)
@@ -138,19 +140,19 @@
 be any other string (see examples below). The `status` is one of:
 
 * `ok(user(ID))` or just `ok(_)` if user info is not important. This status is
-   used to tell that this label/category has been met.
+  used to tell that this label/category has been met.
 * `need(_)` is used to tell that this label/category is needed for change to
-   become submittable
-* `reject(user(ID))` or just `reject(_)`. This status is used to tell that label/category
-   is blocking change submission
-* `impossible(_)` is used when the logic knows that the change cannot be submitted as-is.
-   Administrative intervention is probably required. This is meant for cases
-   where the logic requires members of "FooEng" to score "Code-Review +2" on a
-   change, but nobody is in group "FooEng". It is to hint at permissions
-   misconfigurations.
+  become submittable
+* `reject(user(ID))` or just `reject(_)`. This status is used to tell that
+  label/category is blocking change submission
+* `impossible(_)` is used when the logic knows that the change cannot be
+  submitted as-is. Administrative intervention is probably required. This is
+  meant for cases where the logic requires members of "FooEng" to score
+  `Code-Review +2` on a change, but nobody is in group "FooEng". It is to hint
+  at permissions misconfigurations.
 * `may(_)` allows expression of approval categories that are optional, i.e.
-   could either be set or unset without ever influencing whether the change
-   could be submitted.
+  could either be set or unset without ever influencing whether the change
+  could be submitted.
 
 NOTE: For a change to be submittable all `label` terms contained in the returned
 `submit` term must have either `ok` or `may` status.
@@ -176,10 +178,11 @@
 <2> label `'Verified'` is rejected. Change is not submittable.
 <3> label `'Author-is-John-Doe'` is needed for the change to become submittable.
     Note that this tells nothing about how this criteria will be met. It is up
-    to the implementer of the `submit_rule` to return `label('Author-is-John-Doe',
-    ok(_))` when this criteria is met.  Most likely, it will have to match
-    against `gerrit:commit_author` in order to check if this criteria is met.
-    This will become clear through the examples below.
+    to the implementer of the `submit_rule` to return
+    `label('Author-is-John-Doe', ok(_))` when this criteria is met.  Most
+    likely, it will have to match against `gerrit:commit_author` in order to
+    check if this criteria is met. This will become clear through the examples
+    below.
 
 Of course, when implementing the `submit_rule` we will use the facts about the
 change that are already provided by Gerrit.
@@ -190,9 +193,9 @@
 `'ABC'` is link:config-labels.html[defined for the project] then voting for the
 label `'ABC'` will be displayed. Otherwise, it is not displayed. Note that the
 project doesn't need a defined label for each label contained in the result of
-`submit_rule` predicate.  For example, the decision whether `'Author-is-John-Doe'`
-label is met will probably not be made by explicit voting but, instead, by
-inspecting the facts about the change.
+`submit_rule` predicate.  For example, the decision whether
+`'Author-is-John-Doe'` label is met will probably not be made by explicit voting
+but, instead, by inspecting the facts about the change.
 
 [[SubmitFilter]]
 == Submit Filter
@@ -202,7 +205,7 @@
 searched for in the `rules.pl` of all parent projects of the current project,
 but not in the `rules.pl` of the current project. The search will start from the
 immediate parent of the current project, then in the parent project of that
-project and so on until, and including, the 'All-Projects' project.
+project and so on until, and including, the `'All-Projects'` project.
 
 The purpose of the submit filter is, as its name says, to filter the results
 of the `submit_rule`. Therefore, the `submit_filter` predicate has two
@@ -264,7 +267,7 @@
 
 [[HowToWriteSubmitType]]
 == How to write submit type
-Writing custom submit type logic in Prolog is the similar top
+Writing custom submit type logic in Prolog is similar to
 xref:HowToWriteSubmitRules[writing submit rules]. The only difference is that
 one has to implement a `submit_type` predicate (instead of the `submit_rule`)
 and that the return result of the `submit_type` has to be an atom that
@@ -290,14 +293,15 @@
 
 [[TestingSubmitRules]]
 == Testing submit rules
-The prolog environment running the `submit_rule` is loaded with state describing the
-change that is being evaluated. The easiest way to load this state is to test your
-`submit_rule` against a real change on a running gerrit instance. The command
-link:cmd-test-submit-rule.html[test-submit rule] loads a specific change and executes
-the `submit_rule`. It optionally reads the rule from from `stdin` to facilitate easy testing.
+The prolog environment running the `submit_rule` is loaded with state describing
+the change that is being evaluated. The easiest way to load this state is to
+test your `submit_rule` against a real change on a running gerrit instance. The
+command link:cmd-test-submit-rule.html[test-submit rule] loads a specific change
+and executes the `submit_rule`. It optionally reads the rule from from `stdin`
+to facilitate easy testing.
 
 ====
-  cat rules.pl | ssh gerrit_srv gerrit test-submit rule I45e080b105a50a625cc8e1fb5b357c0bfabe6d68 -s
+  $ cat rules.pl | ssh gerrit_srv gerrit test-submit rule I45e080b105a50a625cc8e1fb5b357c0bfabe6d68 -s
 ====
 
 == Prolog vs Gerrit plugin for project specific submit rules
@@ -318,9 +322,9 @@
 can make use of the plugin provided predicates when writing Prolog based rules.
 
 == Examples - Submit Rule
-The following examples should serve as a cookbook for developing own submit rules.
-Some of them are too trivial to be used in production and their only purpose is
-to provide step by step introduction and understanding.
+The following examples should serve as a cookbook for developing own submit
+rules. Some of them are too trivial to be used in production and their only
+purpose is to provide step by step introduction and understanding.
 
 Some of the examples will implement the `submit_rule` and some will implement
 the `submit_filter` just to show both possibilities.  Remember that
@@ -329,49 +333,49 @@
 whether to implement `submit_rule` or `submit_filter`.
 
 === Example 1: Make every change submittable
-Let's start with a most trivial example where we would make every change submittable
-regardless of the votes it has:
+Let's start with a most trivial example where we would make every change
+submittable regardless of the votes it has:
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(submit(W)) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(W)) :-
     W = label('Any-Label-Name', ok(_)).
-====
+----
 
-In this case we make no use of facts about the change. We don't need it as we are simply
-making every change submittable. Note that, in this case, the Gerrit UI will not show
-the UI for voting for the standard `'Code-Review'` and `'Verified'` categories as labels
-with these names are not part of the return result. The `'Any-Label-Name'` could really
-be any string.
+In this case we make no use of facts about the change. We don't need it as we
+are simply making every change submittable. Note that, in this case, the Gerrit
+UI will not show the UI for voting for the standard `'Code-Review'` and
+`'Verified'` categories as labels with these names are not part of the return
+result. The `'Any-Label-Name'` could really be any string.
 
 === Example 2: Every change submittable and voting in the standard categories possible
 This is continuation of the previous example where, in addition, to making
 every change submittable we want to enable voting in the standard
 `'Code-Review'` and `'Verified'` categories.
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(submit(CR, V)) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(CR, V)) :-
     CR = label('Code-Review', ok(_)),
     V = label('Verified', ok(_)).
-====
+----
 
-Since for every change all label statuses are `'ok'` every change will be submittable.
-Voting in the standard labels will be shown in the UI as the standard label names are
-included in the return result.
+Since for every change all label statuses are `'ok'` every change will be
+submittable. Voting in the standard labels will be shown in the UI as the
+standard label names are included in the return result.
 
 === Example 3: Nothing is submittable
 This example shows how to make all changes non-submittable regardless of the
 votes they have.
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(submit(R)) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(R)) :-
     R = label('Any-Label-Name', reject(_)).
-====
+----
 
 Since for any change we return only one label with status `reject`, no change
 will be submittable. The UI will, however, not indicate what is needed for a
@@ -381,67 +385,66 @@
 In this example no change is submittable but here we show how to present 'Need
 <label>' information to the user in the UI.
 
-.rules.pl
-[caption=""]
-====
-  % In the UI this will show: Need Any-Label-Name
-  submit_rule(submit(N)) :-
+`rules.pl`
+[source,prolog]
+----
+% In the UI this will show: Need Any-Label-Name
+submit_rule(submit(N)) :-
     N = label('Any-Label-Name', need(_)).
 
-  % We could define more "need" labels by adding more rules
-  submit_rule(submit(N)) :-
+% We could define more "need" labels by adding more rules
+submit_rule(submit(N)) :-
     N = label('Another-Label-Name', need(_)).
 
-  % or by providing more than one need label in the same rule
-  submit_rule(submit(NX, NY)) :-
+% or by providing more than one need label in the same rule
+submit_rule(submit(NX, NY)) :-
     NX = label('X-Label-Name', need(_)),
     NY = label('Y-Label-Name', need(_)).
-====
+----
 
 In the UI this will show:
-****
-* Need Any-Label-Name
-* Need Another-Label-Name
-* Need X-Label-Name
-* Need Y-Label-Name
-****
+
+* `Need Any-Label-Name`
+* `Need Another-Label-Name`
+* `Need X-Label-Name`
+* `Need Y-Label-Name`
 
 From the example above we can see a few more things:
 
 * comment in Prolog starts with the `%` character
-* there could be multiple `submit_rule` predicates. Since Prolog, by default, tries to find
-  all solutions for a query, the result will be union of all solutions.
-  Therefore, we see all 4 `need` labels in the UI.
+* there could be multiple `submit_rule` predicates. Since Prolog, by default,
+  tries to find all solutions for a query, the result will be union of all
+  solutions. Therefore, we see all 4 `need` labels in the UI.
 
 === Example 5: The 'Need ...' labels not shown when change is submittable
-This example shows that, when there is a solution for `submit_rule(X)` where all labels
-have status `ok` then Gerrit will not show any labels with the `need` status from
-any of the previous `submit_rule(X)` solutions.
+This example shows that, when there is a solution for `submit_rule(X)` where all
+labels have status `ok` then Gerrit will not show any labels with the `need`
+status from any of the previous `submit_rule(X)` solutions.
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(submit(N)) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(N)) :-
     N = label('Some-Condition', need(_)).
 
-  submit_rule(submit(OK)) :-
+submit_rule(submit(OK)) :-
     OK = label('Another-Condition', ok(_)).
-====
+----
 
-The 'Need Some-Condition' will not be show in the UI because of the result of
+The `'Need Some-Condition'` will not be shown in the UI because of the result of
 the second rule.
 
 The same is valid if the two rules are swapped:
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(submit(OK)) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(OK)) :-
     OK = label('Another-Condition', ok(_)).
 
-  submit_rule(submit(N)) :-
+submit_rule(submit(N)) :-
     N = label('Some-Condition', need(_)).
-====
+----
 
 The result of the first rule will stop search for any further solutions.
 
@@ -449,81 +452,80 @@
 This is the first example where we will use the Prolog facts about a change that
 are automatically exposed by Gerrit. Our goal is to make any change submittable
 when the commit author is named `'John Doe'`. In the very first
-step let's make sure Gerrit UI shows 'Need Author-is-John-Doe' in
+step let's make sure Gerrit UI shows `'Need Author-is-John-Doe'` in
 the UI to clearly indicate to the user what is needed for a change to become
 submittable:
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(submit(Author)) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(Author)) :-
     Author = label('Author-is-John-Doe', need(_)).
-====
+----
 
 This will show:
-****
-* Need Author-is-John-Doe
-****
+
+* `Need Author-is-John-Doe`
 
 in the UI but no change will be submittable yet. Let's add another rule:
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(submit(Author)) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(Author)) :-
     Author = label('Author-is-John-Doe', need(_)).
 
-  submit_rule(submit(Author)) :-
+submit_rule(submit(Author)) :-
     gerrit:commit_author(_, 'John Doe', _),
     Author = label('Author-is-John-Doe', ok(_)).
-====
+----
 
 In the second rule we return `ok` status for the `'Author-is-John-Doe'` label
 if there is a `commit_author` fact where the full name is `'John Doe'`. If
 author of a change is `'John Doe'` then the second rule will return a solution
 where all labels have `ok` status and the change will become submittable. If
 author of a change is not `'John Doe'` then only the first rule will produce a
-solution. The UI will show 'Need Author-is-John-Doe' but, as expected, the
+solution. The UI will show `'Need Author-is-John-Doe'` but, as expected, the
 change will not be submittable.
 
 Instead of checking by full name we could also check by the email address:
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(submit(Author)) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(Author)) :-
     Author = label('Author-is-John-Doe', need(_)).
 
-  submit_rule(submit(Author)) :-
+submit_rule(submit(Author)) :-
     gerrit:commit_author(_, _, 'john.doe@example.com'),
     Author = label('Author-is-John-Doe', ok(_)).
-====
+----
 
-or by user id (assuming it is 1000000):
+or by user id (assuming it is `1000000`):
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(submit(Author)) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(Author)) :-
     Author = label('Author-is-John-Doe', need(_)).
 
-  submit_rule(submit(Author)) :-
+submit_rule(submit(Author)) :-
     gerrit:commit_author(user(1000000), _, _),
     Author = label('Author-is-John-Doe', ok(_)).
-====
+----
 
 or by a combination of these 3 attributes:
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(submit(Author)) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(Author)) :-
     Author = label('Author-is-John-Doe', need(_)).
 
-  submit_rule(submit(Author)) :-
+submit_rule(submit(Author)) :-
     gerrit:commit_author(_, 'John Doe', 'john.doe@example.com'),
     Author = label('Author-is-John-Doe', ok(_)).
-====
+----
 
 === Example 7: Make change submittable if commit message starts with "Fix "
 Besides showing how to make use of the commit message text the purpose of this
@@ -540,19 +542,19 @@
 
 Let's implement both options:
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(submit(Fix)) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(Fix)) :-
     Fix = label('Commit-Message-starts-with-Fix', need(_)).
 
-  submit_rule(submit(Fix)) :-
+submit_rule(submit(Fix)) :-
     gerrit:commit_message(M), name(M, L), starts_with(L, "Fix "),
     Fix = label('Commit-Message-starts-with-Fix', ok(_)).
 
-  starts_with(L, []).
-  starts_with([H|T1], [H|T2]) :- starts_with(T1, T2).
-====
+starts_with(L, []).
+starts_with([H|T1], [H|T2]) :- starts_with(T1, T2).
+----
 
 NOTE: The `name/2` embedded predicate is used to convert a string symbol into a
 list of characters. A string `abc` is converted into a list of characters `[97,
@@ -564,32 +566,33 @@
 
 Using the `gerrit:commit_message_matches` predicate is probably more efficient:
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(submit(Fix)) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(Fix)) :-
     Fix = label('Commit-Message-starts-with-Fix', need(_)).
 
-  submit_rule(submit(Fix)) :-
+submit_rule(submit(Fix)) :-
     gerrit:commit_message_matches('^Fix '),
     Fix = label('Commit-Message-starts-with-Fix', ok(_)).
-====
+----
 
 The previous example could also be written so that it first checks if the commit
 message starts with 'Fix '. If true then it sets OK for that category and stops
 further backtracking by using the cut `!` operator:
-.rules.pl
-[caption=""]
-====
-  submit_rule(submit(Fix)) :-
+
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(Fix)) :-
     gerrit:commit_message_matches('^Fix '),
     Fix = label('Commit-Message-starts-with-Fix', ok(_)),
     !.
 
-  % Message does not start with 'Fix ' so Fix is needed to submit
-  submit_rule(submit(Fix)) :-
+% Message does not start with 'Fix ' so Fix is needed to submit
+submit_rule(submit(Fix)) :-
     Fix = label('Commit-Message-starts-with-Fix', need(_)).
-====
+----
 
 == The default submit policy
 All examples until now concentrate on one particular aspect of change data.
@@ -606,44 +609,46 @@
 The default submit rule with the two default categories, `Code-Review` and
 `Verified`, can be implemented as:
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(submit(V, CR)) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(V, CR)) :-
     gerrit:max_with_block(-2, 2, 'Code-Review', CR),
     gerrit:max_with_block(-1, 1, 'Verified', V).
-====
+----
 
 Once this implementation is understood it can be customized to implement
 project specific submit rules. Note, that this implementation hardcodes
 the two default categories. Introducing a new category in the database would
 require introducing the same category here or a `submit_filter` in a parent
 project would have to care about including the new category in the result of
-this `submit_rule`.  On the other side, this example is easy to read and
+this `submit_rule`. On the other side, this example is easy to read and
 understand.
 
 === Reusing the default submit policy
 To get results of Gerrit's default submit policy we use the
 `gerrit:default_submit` predicate.  The `gerrit:default_submit(X)` includes all
-categories from the database.  This means that if we write a submit rule like:
+categories from the database.  This means that if we write a submit rule like
+this:
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(X) :- gerrit:default_submit(X).
-====
-then this is equivalent to not using `rules.pl` at all. We just delegate to
+`rules.pl`
+[source,prolog]
+----
+submit_rule(X) :- gerrit:default_submit(X).
+----
+
+it is equivalent to not using `rules.pl` at all. We just delegate to
 default logic. However, once we invoke the `gerrit:default_submit(X)` we can
 perform further actions on the return result `X` and apply our specific
 logic. The following pattern illustrates this technique:
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(S) :- gerrit:default_submit(R), project_specific_policy(R, S).
+`rules.pl`
+[source,prolog]
+----
+submit_rule(S) :- gerrit:default_submit(R), project_specific_policy(R, S).
 
-  project_specific_policy(R, S) :- ...
-====
+project_specific_policy(R, S) :- ...
+----
 
 In the following examples both styles will be shown.
 
@@ -659,26 +664,26 @@
 `Non-Author-Code-Review` label is added with status `ok` if such an approval
 exists or with status `need` if it doesn't exist.
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(S) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(S) :-
     gerrit:default_submit(X),
     X =.. [submit | Ls],
     add_non_author_approval(Ls, R),
     S =.. [submit | R].
 
-  add_non_author_approval(S1, S2) :-
+add_non_author_approval(S1, S2) :-
     gerrit:commit_author(A),
     gerrit:commit_label(label('Code-Review', 2), R),
     R \= A, !,
     S2 = [label('Non-Author-Code-Review', ok(R)) | S1].
-  add_non_author_approval(S1, [label('Non-Author-Code-Review', need(_)) | S1]).
-====
+add_non_author_approval(S1, [label('Non-Author-Code-Review', need(_)) | S1]).
+----
 
 This example uses the `univ` operator `=..` to "unpack" the result of the
 default_submit, which is a structure of the form `submit(label('Code-Review',
-ok(_)), label('Verified', need(_)) ...)` into a list like `[submit,
+ok(_)), label('Verified', need(_)), ...)` into a list like `[submit,
 label('Code-Review', ok(_)), label('Verified', need(_)), ...]`.  Then we
 process the tail of the list (the list of labels) as a Prolog list, which is
 much easier than processing a structure. In the end we use the same `univ`
@@ -697,24 +702,24 @@
 Let's implement the same submit rule the other way, without reusing the
 `gerrit:default_submit`:
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(submit(CR, V)) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(CR, V)) :-
     base(CR, V),
     CR = label(_, ok(Reviewer)),
     gerrit:commit_author(Author),
     Author \= Reviewer,
     !.
 
-  submit_rule(submit(CR, V, N)) :-
+submit_rule(submit(CR, V, N)) :-
     base(CR, V),
     N = label('Non-Author-Code-Review', need(_)).
 
-  base(CR, V) :-
+base(CR, V) :-
     gerrit:max_with_block(-2, 2, 'Code-Review', CR),
     gerrit:max_with_block(-1, 1, 'Verified', V).
-====
+----
 
 The latter implementation is probably easier to understand and the code looks
 cleaner. Note, however, that the latter implementation will always return the
@@ -730,53 +735,52 @@
 Which of these two behaviors is desired will always depend on how a particular
 Gerrit server is managed.
 
-Example 9: Remove the `Verified` category
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+==== Example 9: Remove the `Verified` category
 A project has no build and test. It consists of only text files and needs only
 code review.  We want to remove the `Verified` category from this project so
 that `Code-Review+2` is the only criteria for a change to become submittable.
 We also want the UI to not show the `Verified` category in the table with
 votes and on the voting screen.
 
-This is quite simple without reusing the 'gerrit:default_submit`:
+This is quite simple without reusing the `gerrit:default_submit`:
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(submit(CR)) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(CR)) :-
     gerrit:max_with_block(-2, 2, 'Code-Review', CR).
-====
+----
 
 Implementing the same rule by reusing `gerrit:default_submit` is a bit more complex:
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(S) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(S) :-
     gerrit:default_submit(X),
     X =.. [submit | Ls],
     remove_verified_category(Ls, R),
     S =.. [submit | R].
 
-  remove_verified_category([], []).
-  remove_verified_category([label('Verified', _) | T], R) :- remove_verified_category(T, R), !.
-  remove_verified_category([H|T], [H|R]) :- remove_verified_category(T, R).
-====
+remove_verified_category([], []).
+remove_verified_category([label('Verified', _) | T], R) :- remove_verified_category(T, R), !.
+remove_verified_category([H|T], [H|R]) :- remove_verified_category(T, R).
+----
 
 === Example 10: Combine examples 8 and 9
 In this example we want to both remove the verified and have the four eyes
 principle.  This means we want a combination of examples 7 and 8.
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(S) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(S) :-
     gerrit:default_submit(X),
     X =.. [submit | Ls],
     remove_verified_category(Ls, R1),
     add_non_author_approval(R1, R),
     S =.. [submit | R].
-====
+----
 
 The `remove_verified_category` and `add_non_author_approval` predicates are the
 same as defined in the previous two examples.
@@ -784,23 +788,23 @@
 Without reusing the `gerrit:default_submit` the same example may be implemented
 as:
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(submit(CR)) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(CR)) :-
     base(CR),
     CR = label(_, ok(Reviewer)),
     gerrit:commit_author(Author),
     Author \= Reviewer,
     !.
 
-  submit_rule(submit(CR, N)) :-
+submit_rule(submit(CR, N)) :-
     base(CR),
     N = label('Non-Author-Code-Review', need(_)).
 
-  base(CR) :-
+base(CR) :-
     gerrit:max_with_block(-2, 2, 'Code-Review', CR),
-====
+----
 
 === Example 11: Remove the `Verified` category from all projects
 Example 9, implements `submit_rule` that removes the `Verified` category from
@@ -808,34 +812,34 @@
 category from all projects. This means we have to implement `submit_filter` and
 we have to do that in the `rules.pl` of the `All-Projects` project.
 
-.rules.pl
-[caption=""]
-====
-  submit_filter(In, Out) :-
+`rules.pl`
+[source,prolog]
+----
+submit_filter(In, Out) :-
     In =.. [submit | Ls],
     remove_verified_category(Ls, R),
     Out =.. [submit | R].
 
-  remove_verified_category([], []).
-  remove_verified_category([label('Verified', _) | T], R) :-
-    remove_verified_category(T, R), !.
-  remove_verified_category([H|T], [H|R]) :- remove_verified_category(T, R).
-====
+remove_verified_category([], []).
+remove_verified_category([label('Verified', _) | T], R) :- remove_verified_category(T, R), !.
+remove_verified_category([H|T], [H|R]) :- remove_verified_category(T, R).
+----
 
 === Example 12: On release branches require DrNo in addition to project rules
 A new category 'DrNo' is added to the database and is required for release
-branches. To mark a branch as a release branch we use `drno('refs/heads/branch')`.
+branches. To mark a branch as a release branch we use
+`drno('refs/heads/branch')`.
 
-.rules.pl
-[caption=""]
-====
-  drno('refs/heads/master').
-  drno('refs/heads/stable-2.3').
-  drno('refs/heads/stable-2.4').
-  drno('refs/heads/stable-2.5').
-  drno('refs/heads/stable-2.5').
+`rules.pl`
+[source,prolog]
+----
+drno('refs/heads/master').
+drno('refs/heads/stable-2.3').
+drno('refs/heads/stable-2.4').
+drno('refs/heads/stable-2.5').
+drno('refs/heads/stable-2.5').
 
-  submit_filter(In, Out) :-
+submit_filter(In, Out) :-
     gerrit:change_branch(Branch),
     drno(Branch),
     !,
@@ -843,129 +847,131 @@
     gerrit:max_with_block(-1, 1, 'DrNo', DrNo),
     Out =.. [submit, DrNo | I].
 
-  submit_filter(In, Out) :- In = Out.
-====
+submit_filter(In, Out) :- In = Out.
+----
 
 === Example 13: 1+1=2 Code-Review
 In this example we introduce accumulative voting to determine if a change is
-submittable or not. We modify the standard Code-Review to be accumulative, and make the
-change submittable if the total score is 2 or higher.
+submittable or not. We modify the standard `Code-Review` to be accumulative, and
+make the change submittable if the total score is `2` or higher.
 
-The code in this example is very similar to Example 8, with the addition of findall/3
-and gerrit:remove_label.
-The findall/3 embedded predicate is used to form a list of all objects that satisfy a
-specified Goal. In this example it is used to get a list of all the 'Code-Review' scores.
-gerrit:remove_label is a built-in helper that is implemented similarly to the
-'remove_verified_category' as seen in the previous example.
+The code in this example is very similar to Example 8, with the addition of
+`findall/3` and `gerrit:remove_label`.
 
-.rules.pl
-[caption=""]
-====
-  sum_list([], 0).
-  sum_list([H | Rest], Sum) :- sum_list(Rest,Tmp), Sum is H + Tmp.
+The `findall/3` embedded predicate is used to form a list of all objects that
+satisfy a specified Goal. In this example it is used to get a list of all the
+`Code-Review` scores. `gerrit:remove_label` is a built-in helper that is
+implemented similarly to the `remove_verified_category` as seen in the previous
+example.
 
-  add_category_min_score(In, Category, Min,  P) :-
+`rules.pl`
+[source,prolog]
+----
+sum_list([], 0).
+sum_list([H | Rest], Sum) :- sum_list(Rest,Tmp), Sum is H + Tmp.
+
+add_category_min_score(In, Category, Min,  P) :-
     findall(X, gerrit:commit_label(label(Category,X),R),Z),
     sum_list(Z, Sum),
     Sum >= Min, !,
     P = [label(Category,ok(R)) | In].
 
-  add_category_min_score(In, Category,Min,P) :-
+add_category_min_score(In, Category,Min,P) :-
     P = [label(Category,need(Min)) | In].
 
-  submit_rule(S) :-
+submit_rule(S) :-
     gerrit:default_submit(X),
     X =.. [submit | Ls],
     gerrit:remove_label(Ls,label('Code-Review',_),NoCR),
     add_category_min_score(NoCR,'Code-Review', 2, Labels),
     S =.. [submit | Labels].
-====
+----
 
 Implementing the same example without using `gerrit:default_submit`:
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(submit(CR, V)) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(submit(CR, V)) :-
     sum(2, 'Code-Review', CR),
     gerrit:max_with_block(-1, 1, 'Verified', V).
 
-  % Sum the votes in a category. Uses a helper function score/2
-  % to select out only the score values the given category.
-  sum(VotesNeeded, Category, label(Category, ok(_))) :-
+% Sum the votes in a category. Uses a helper function score/2
+% to select out only the score values the given category.
+sum(VotesNeeded, Category, label(Category, ok(_))) :-
     findall(Score, score(Category, Score), All),
     sum_list(All, Sum),
     Sum >= VotesNeeded,
     !.
-  sum(VotesNeeded, Category, label(Category, need(VotesNeeded))).
+sum(VotesNeeded, Category, label(Category, need(VotesNeeded))).
 
-  score(Category, Score) :-
+score(Category, Score) :-
     gerrit:commit_label(label(Category, Score), User).
 
-  % Simple Prolog routine to sum a list of integers.
-  sum_list(List, Sum)   :- sum_list(List, 0, Sum).
-  sum_list([X|T], Y, S) :- Z is X + Y, sum_list(T, Z, S).
-  sum_list([], S, S).
-====
+% Simple Prolog routine to sum a list of integers.
+sum_list(List, Sum)   :- sum_list(List, 0, Sum).
+sum_list([X|T], Y, S) :- Z is X + Y, sum_list(T, Z, S).
+sum_list([], S, S).
+----
 
 === Example 14: Master and apprentice
 The master and apprentice example allow you to specify a user (the `master`)
 that must approve all changes done by another user (the `apprentice`).
 
 The code first checks if the commit author is in the apprentice database.
-If the commit is done by an apprentice, it will check if there is a +2
+If the commit is done by an `apprentice`, it will check if there is a `+2`
 review by the associated `master`.
 
-.rules.pl
-[caption=""]
-====
-  % master_apprentice(Master, Apprentice).
-  % Extend this with appropriate user-id's for your master/apprentice setup.
-  master_apprentice(user(1000064), user(1000000)).
+`rules.pl`
+[source,prolog]
+----
+% master_apprentice(Master, Apprentice).
+% Extend this with appropriate user-id for your master/apprentice setup.
+master_apprentice(user(1000064), user(1000000)).
 
-  submit_rule(S) :-
+submit_rule(S) :-
     gerrit:default_submit(In),
     In =.. [submit | Ls],
     add_apprentice_master(Ls, R),
     S =.. [submit | R].
 
-  check_master_approval(S1, S2, Master) :-
+check_master_approval(S1, S2, Master) :-
     gerrit:commit_label(label('Code-Review', 2), R),
     R = Master, !,
     S2 = [label('Master-Approval', ok(R)) | S1].
-  check_master_approval(S1, [label('Master-Approval', need(_)) | S1], _).
+check_master_approval(S1, [label('Master-Approval', need(_)) | S1], _).
 
-  add_apprentice_master(S1, S2) :-
+add_apprentice_master(S1, S2) :-
     gerrit:commit_author(Id),
     master_apprentice(Master, Id),
     !,
     check_master_approval(S1, S2, Master).
 
-  add_apprentice_master(S, S).
-====
+add_apprentice_master(S, S).
+----
 
 === Example 15: Only allow Author to submit change
-This example adds a new needed category `Patchset-Author` for any user that is
-not the author of the patch. This effectively blocks all users except the author
-from submitting the change. This could result in an impossible situation if the
-author does not have permissions for submitting the change.
+This example adds a new needed category `Only-Author-Can-Submit` for any user
+that is not the author of the patch. This effectively blocks all users except
+the author from submitting the change. This could result in an impossible
+situation if the author does not have permissions for submitting the change.
 
-.rules.pl
-[caption=""]
-====
-  submit_rule(S) :-
+`rules.pl`
+[source,prolog]
+----
+submit_rule(S) :-
     gerrit:default_submit(In),
     In =.. [submit | Ls],
     only_allow_author_to_submit(Ls, R),
     S =.. [submit | R].
 
-  only_allow_author_to_submit(S, S) :-
+only_allow_author_to_submit(S, S) :-
     gerrit:commit_author(Id),
     gerrit:current_user(Id),
     !.
 
-  only_allow_author_to_submit(S1, [label('Patchset-Author', need(_)) | S1]).
-====
+only_allow_author_to_submit(S1, [label('Only-Author-Can-Submit', need(_)) | S1]).
+----
 
 == Examples - Submit Type
 The following examples show how to implement own submit type rules.
@@ -975,48 +981,47 @@
 whatever is set as project default submit type.
 
 rules.pl
-[caption=""]
-====
-  submit_type(cherry_pick).
-====
-
+[source,prolog]
+----
+submit_type(cherry_pick).
+----
 
 [[SubmitTypePerBranch]]
-=== Example 2: `Fast Forward Only` for all `refs/heads/stable*` branches
-For all `refs/heads/stable.*` branches we would like to enforce the `Fast
+=== Example 2: `Fast Forward Only` for all `+refs/heads/stable*+` branches
+For all `+refs/heads/stable*+` branches we would like to enforce the `Fast
 Forward Only` submit type. A reason for this decision may be a need to never
 break the build in the stable branches.  For all other branches, those not
-matching the `refs/heads/stable.*` pattern, we would like to use the project's
+matching the `+refs/heads/stable*+` pattern, we would like to use the project's
 default submit type as defined on the project settings page.
 
-.rules.pl
-[caption=""]
-====
-  submit_type(fast_forward_only) :-
+`rules.pl`
+[source,prolog]
+----
+submit_type(fast_forward_only) :-
     gerrit:change_branch(B), regex_matches('refs/heads/stable.*', B),
     !.
-  submit_type(T) :- gerrit:project_default_submit_type(T)
-====
+submit_type(T) :- gerrit:project_default_submit_type(T)
+----
 
 The first `submit_type` predicate defines the `Fast Forward Only` submit type
-for `refs/heads/stable.*` branches. The second `submit_type` predicate returns
+for `+refs/heads/stable.*+` branches. The second `submit_type` predicate returns
 the project's default submit type.
 
 === Example 3: Don't require `Fast Forward Only` if only documentation was changed
-Like in the previous example we want the `Fast Forward Only` submit type for
-the `refs/heads/stable*` branches.  However, if only documentation was changed
-(only `*.txt` files), then we allow project's default submit type for such
+Like in the previous example we want the `Fast Forward Only` submit type for the
+`+refs/heads/stable*+` branches.  However, if only documentation was changed
+(only `+*.txt+` files), then we allow project's default submit type for such
 changes.
 
-.rules.pl
-[caption=""]
-====
-  submit_type(fast_forward_only) :-
+`rules.pl`
+[source,prolog]
+----
+submit_type(fast_forward_only) :-
     gerrit:commit_delta('(?<!\.txt)$'),
     gerrit:change_branch(B), regex_matches('refs/heads/stable.*', B),
     !.
-  submit_type(T) :- gerrit:project_default_submit_type(T)
-====
+submit_type(T) :- gerrit:project_default_submit_type(T)
+----
 
 The `gerrit:commit_delta('(?<!\.txt)$')` succeeds if the change contains a file
 whose name doesn't end with `.txt` The rest of this rule is same like in the
diff --git a/Documentation/replace_macros.py b/Documentation/replace_macros.py
index 5cdfe91..8deee62 100755
--- a/Documentation/replace_macros.py
+++ b/Documentation/replace_macros.py
@@ -193,6 +193,10 @@
 opts.add_option('-o', '--out', help='output file')
 opts.add_option('-s', '--src', help='source file')
 opts.add_option('-x', '--suffix', help='suffix for included filenames')
+opts.add_option('-b', '--searchbox', action="store_true", default=True,
+                help="generate the search boxes")
+opts.add_option('--no-searchbox', action="store_false", dest='searchbox',
+                help="don't generate the search boxes")
 options, _ = opts.parse_args()
 
 try:
@@ -208,7 +212,8 @@
       last_line = ''
     elif PAT_SEARCHBOX.match(last_line):
       # Case of 'SEARCHBOX\n---------'
-      out_file.write(SEARCH_BOX)
+      if options.searchbox:
+        out_file.write(SEARCH_BOX)
       last_line = ''
     elif PAT_INCLUDE.match(line):
       # Case of 'include::<filename>'
diff --git a/Documentation/rest-api-access.txt b/Documentation/rest-api-access.txt
index 42214fe..ee3e8ce 100644
--- a/Documentation/rest-api-access.txt
+++ b/Documentation/rest-api-access.txt
@@ -30,7 +30,7 @@
 .Response
 ----
   HTTP/1.1 200 OK
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -284,12 +284,15 @@
   }
 ----
 
+[[json-entities]]
+== JSON Entities
+
 [[access-section-info]]
 === AccessSectionInfo
 The `AccessSectionInfo` describes the access rights that are assigned
 on a ref.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |==================================
 |Field Name           ||Description
 |`permissions`        ||
@@ -303,7 +306,7 @@
 The `PermissionInfo` entity contains information about an assigned
 permission.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |==================================
 |Field Name     ||Description
 |`label`        |optional|
@@ -321,7 +324,7 @@
 The `PermissionRuleInfo` entity contains information about a permission
 rule that is assigned to group.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |==================================
 |Field Name     ||Description
 |`action`       ||
@@ -343,7 +346,7 @@
 The `ProjectAccessInfo` entity contains information about the access
 rights for a project.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |==================================
 |Field Name           ||Description
 |`revision`           ||
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 201b020..b5c1557 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -7,6 +7,44 @@
 [[account-endpoints]]
 == Account Endpoints
 
+[[suggest-account]]
+=== Suggest Account
+--
+'GET /accounts/'
+--
+
+Suggest users for a given query `q` and result limit `n`. If result
+limit is not passed, then the default 10 is used. Returns a list of
+matching link:#account-info[AccountInfo] entities.
+
+.Request
+----
+  GET /accounts/?q=John HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "_account_id": 1000096,
+      "name": "John Doe",
+      "email": "john.doe@example.com",
+      "username": "john"
+    },
+    {
+      "_account_id": 1001439,
+      "name": "John Smith",
+      "email": "john.smith@example.com",
+      "username": "jsmith"
+    },
+  ]
+----
+
 [[get-account]]
 === Get Account
 --
@@ -24,7 +62,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -49,7 +87,7 @@
 .Request
 ----
   PUT /accounts/john HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "name": "John Doe",
@@ -69,7 +107,7 @@
 ----
   HTTP/1.1 201 Created
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -96,7 +134,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "John Doe"
@@ -118,7 +156,7 @@
 .Request
 ----
   PUT /accounts/self/name HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "name": "John F. Doe"
@@ -131,7 +169,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "John F. Doe"
@@ -177,13 +215,13 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "john.doe"
 ----
 
-If the account does not have a username the response is `404 Not Found`.
+If the account does not have a username the response is "`404 Not Found`".
 
 [[get-active]]
 === Get Active
@@ -207,7 +245,7 @@
   ok
 ----
 
-If the account is inactive the response is `204 No Content`.
+If the account is inactive the response is "`204 No Content`".
 
 [[set-active]]
 === Set Active
@@ -227,7 +265,7 @@
   HTTP/1.1 201 Created
 ----
 
-If the account was already active the response is `200 OK`.
+If the account was already active the response is "`200 OK`".
 
 [[delete-active]]
 === Delete Active
@@ -247,7 +285,7 @@
   HTTP/1.1 204 No Content
 ----
 
-If the account was already inactive the response is `404 Not Found`.
+If the account was already inactive the response is "`404 Not Found`".
 
 [[get-http-password]]
 === Get HTTP Password
@@ -266,13 +304,13 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "Qmxlc21ydCB1YmVyIGFsbGVzIGluIGRlciBXZWx0IQ"
 ----
 
-If the account does not have an HTTP password the response is `404 Not Found`.
+If the account does not have an HTTP password the response is "`404 Not Found`".
 
 [[set-http-password]]
 === Set/Generate HTTP Password
@@ -289,7 +327,7 @@
 .Request
 ----
   PUT /accounts/self/password.http HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "generate": true
@@ -302,7 +340,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "ETxgpih8xrNs"
@@ -348,7 +386,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -382,7 +420,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -420,7 +458,7 @@
 ----
   HTTP/1.1 201 Created
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -488,7 +526,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -523,7 +561,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -561,7 +599,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -618,7 +656,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -642,7 +680,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -684,7 +722,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -721,7 +759,7 @@
 ----
 
 If the user doesn't have the global capability the response is
-`404 Not Found`.
+"`404 Not Found`".
 
 .Check if you can create groups
 ****
@@ -748,7 +786,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -828,7 +866,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: text/plain;charset=UTF-8
+  Content-Type: text/plain; charset=UTF-8
 
   https://profiles/pictures/john.doe
 ----
@@ -853,7 +891,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -864,7 +902,6 @@
     "time_format": "HHMM_12",
     "size_bar_in_change_table": true,
     "review_category_strategy": "ABBREV",
-    "comment_visibility_strategy": "EXPAND_RECENT",
     "diff_view": "SIDE_BY_SIDE",
     "my": [
       {
@@ -872,7 +909,7 @@
         "name": "Changes"
       },
       {
-        "url": "#/q/is:draft",
+        "url": "#/q/owner:self+is:draft",
         "name": "Drafts"
       },
       {
@@ -909,7 +946,7 @@
 .Request
 ----
   PUT /a/accounts/self/preferences HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "changes_per_page": 50,
@@ -919,7 +956,6 @@
     "time_format": "HHMM_12",
     "size_bar_in_change_table": true,
     "review_category_strategy": "NAME",
-    "comment_visibility_strategy": "EXPAND_RECENT",
     "diff_view": "SIDE_BY_SIDE",
     "my": [
       {
@@ -927,7 +963,7 @@
         "name": "Changes"
       },
       {
-        "url": "#/q/is:draft",
+        "url": "#/q/owner:self+is:draft",
         "name": "Drafts"
       },
       {
@@ -957,7 +993,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -968,7 +1004,6 @@
     "time_format": "HHMM_12",
     "size_bar_in_change_table": true,
     "review_category_strategy": "NAME",
-    "comment_visibility_strategy": "EXPAND_RECENT",
     "diff_view": "SIDE_BY_SIDE",
     "my": [
       {
@@ -976,7 +1011,7 @@
         "name": "Changes"
       },
       {
-        "url": "#/q/is:draft",
+        "url": "#/q/owner:self+is:draft",
         "name": "Drafts"
       },
       {
@@ -1019,11 +1054,12 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
     "context": 10,
+    "theme": "DEFAULT",
     "ignore_whitespace": "IGNORE_ALL_SPACE",
     "intraline_difference": true,
     "line_length": 100,
@@ -1048,10 +1084,11 @@
 .Request
 ----
   GET /a/accounts/self/preferences.diff HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "context": 10,
+    "theme": "ECLIPSE",
     "ignore_whitespace": "IGNORE_ALL_SPACE",
     "intraline_difference": true,
     "line_length": 100,
@@ -1070,11 +1107,12 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
     "context": 10,
+    "theme": "ECLIPSE",
     "ignore_whitespace": "IGNORE_ALL_SPACE",
     "intraline_difference": true,
     "line_length": 100,
@@ -1106,7 +1144,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -1120,7 +1158,6 @@
       "created": "2013-02-01 09:59:32.126000000",
       "updated": "2013-02-21 11:16:36.775000000",
       "mergeable": true,
-      "_sortkey": "0023412400000f7d",
       "_number": 3965,
       "owner": {
         "name": "John Doe"
@@ -1209,7 +1246,7 @@
 === AccountInfo
 The `AccountInfo` entity contains information about an account.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
 |`_account_id` ||The numeric ID of the account.
@@ -1230,7 +1267,7 @@
 The `AccountInput` entity contains information for the creation of
 a new account.
 
-[options="header",width="50%",cols="1,^2,4"]
+[options="header",cols="1,^2,4"]
 |============================
 |Field Name     ||Description
 |`username`     |optional|
@@ -1249,7 +1286,7 @@
 The `AccountNameInput` entity contains information for setting a name
 for an account.
 
-[options="header",width="50%",cols="1,^2,4"]
+[options="header",cols="1,^2,4"]
 |=============================
 |Field Name ||Description
 |`name`     |optional|The new full name of the account. +
@@ -1261,7 +1298,7 @@
 The `CapabilityInfo` entity contains information about the global
 capabilities of a user.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |=================================
 |Field Name          ||Description
 |`accessDatabase`    |not set if `false`|Whether the user has the
@@ -1314,151 +1351,63 @@
 link:access-control.html#capability_viewQueue[View Queue] capability.
 |=================================
 
-[[preferences-info]]
-=== PreferencesInfo
-The `PreferencesInfo` entity contains information about a user's preferences.
-
-[options="header",width="50%",cols="1,^1,5"]
-|=====================================
-|Field Name              ||Description
-|`changes_per_page`               ||
-The number of changes to show on each page.
-Allowed values are `10`, `25`, `50`, `100`.
-|`show_site_header`   |not set if `false`|
-Whether the site header should be shown.
-|`use_flash_clipboard`     |not set if `false`|
-Whether to use the flash clipboard widget.
-|`download_scheme`      ||
-The type of download URL the user prefers to use.
-|`download_command`     ||
-The type of download command the user prefers to use.
-|`copy_self_on_email`       |not set if `false`|
-Whether to CC me on comments I write.
-|`date_format`         ||
-The format to display the date in.
-Allowed values are `STD`, `US`, `ISO`, `EURO`, `UK`.
-|`time_format`     ||
-The format to display the time in.
-Allowed values are `HHMM_12`, `HHMM_24`.
-|`reverse_patch_set_order`     |not set if `false`|
-Whether to display the patch sets in reverse order.
-|`relative_date_in_change_table`  |not set if `false`|
-Whether to show relative dates in the changes table.
-|`size_bar_in_change_table`      |not set if `false`|
-Whether to show the change sizes as colored bars in the change table.
-|`legacycid_in_change_table`      |not set if `false`|
-Whether to show change number in the change table.
-|`review_category_strategy`   ||
-The strategy used to displayed info in the review category column.
-Allowed values are `NONE`, `NAME`, `EMAIL`, `USERNAME`, `ABBREV`.
-|`comment_visibility_strategy`         ||
-The strategy used to display the comments.
-Allowed values are `COLLAPSE_ALL`, `EXPAND_MOST_RECENT`, `EXPAND_RECENT`, `EXPAND_ALL`.
-|`diff_view`     ||
-The type of diff view to show.
-Allowed values are `SIDE_BY_SIDE`, `UNIFIED_DIFF`.
-|`change_screen`            ||
-The change screen to use.
-Allowed values are `OLD_UI`, `CHANGE_SCREEN2`.
-|=====================================
-
-[[preferences-input]]
-=== PreferencesInput
-The `PreferencesInput` entity contains information for setting the
-user preferences. Fields which are not set will not be updated.
-
-[options="header",width="50%",cols="1,^1,5"]
-|=====================================
-|Field Name              ||Description
-|`changes_per_page`               |optional|
-The number of changes to show on each page.
-Allowed values are `10`, `25`, `50`, `100`.
-|`show_site_header`   |optional|
-Whether the site header should be shown.
-|`use_flash_clipboard`     |optional|
-Whether to use the flash clipboard widget.
-|`download_scheme`      |optional|
-The type of download URL the user prefers to use.
-|`download_command`     |optional|
-The type of download command the user prefers to use.
-|`copy_self_on_email`       |optional|
-Whether to CC me on comments I write.
-|`date_format`         |optional|
-The format to display the date in.
-Allowed values are `STD`, `US`, `ISO`, `EURO`, `UK`.
-|`time_format`     |optional|
-The format to display the time in.
-Allowed values are `HHMM_12`, `HHMM_24`.
-|`reverse_patch_set_order`     |optional|
-Whether to display the patch sets in reverse order.
-|`relative_date_in_change_table`  |optional|
-Whether to show relative dates in the changes table.
-|`size_bar_in_change_table`      |optional|
-Whether to show the change sizes as colored bars in the change table.
-|`legacycid_in_change_table`      |optional|
-Whether to show change number in the change table.
-|`review_category_strategy`   |optional|
-The strategy used to displayed info in the review category column.
-Allowed values are `NONE`, `NAME`, `EMAIL`, `USERNAME`, `ABBREV`.
-|`comment_visibility_strategy`         |optional|
-The strategy used to display the comments.
-Allowed values are `COLLAPSE_ALL`, `EXPAND_MOST_RECENT`, `EXPAND_RECENT`, `EXPAND_ALL`.
-|`diff_view`     |optional|
-The type of diff view to show.
-Allowed values are `SIDE_BY_SIDE`, `UNIFIED_DIFF`.
-|`change_screen`            |optional|
-The change screen to use.
-Allowed values are `OLD_UI`, `CHANGE_SCREEN2`.
-|=====================================
-
 [[diff-preferences-info]]
 === DiffPreferencesInfo
 The `DiffPreferencesInfo` entity contains information about the diff
 preferences of a user.
 
-[options="header",width="50%",cols="1,^1,5"]
-|=====================================
-|Field Name              ||Description
-|`context`               ||
+[options="header",cols="1,^1,5"]
+|===========================================
+|Field Name                    ||Description
+|`context`                     ||
 The number of lines of context when viewing a patch.
-|`expand_all_comments`   |not set if `false`|
+|`theme`                       ||
+The CodeMirror theme. Currently only a subset of light and dark
+CodeMirror themes are supported.
+|`expand_all_comments`         |not set if `false`|
 Whether all inline comments should be automatically expanded.
-|`ignore_whitespace`     ||
+|`ignore_whitespace`           ||
 Whether whitespace changes should be ignored and if yes, which
 whitespace changes should be ignored. +
 Allowed values are `IGNORE_NONE`, `IGNORE_SPACE_AT_EOL`,
 `IGNORE_SPACE_CHANGE`, `IGNORE_ALL_SPACE`.
-|`intraline_difference`  |not set if `false`|
+|`intraline_difference`        |not set if `false`|
 Whether intraline differences should be highlighted.
-|`line_length`           ||
+|`line_length`                 ||
 Number of characters that should be displayed in one line.
-|`manual_review`         |not set if `false`|
+|`manual_review`               |not set if `false`|
 Whether the 'Reviewed' flag should not be set automatically on a patch
 when it is viewed.
-|`retain_header`         |not set if `false`|
+|`retain_header`               |not set if `false`|
 Whether the header that is displayed above the patch (that either shows
 the commit message, the diff preferences, the patch sets or the files)
 should be retained on file switch.
-|`show_line_endings`     |not set if `false`|
+|`show_line_endings`           |not set if `false`|
 Whether Windows EOL/Cr-Lf should be displayed as '\r' in a dotted-line
 box.
-|`show_tabs`             |not set if `false`|
+|`show_tabs`                   |not set if `false`|
 Whether tabs should be shown.
-|`show_whitespace_errors`|not set if `false`|
+|`show_whitespace_errors`      |not set if `false`|
 Whether whitespace errors should be shown.
-|`skip_deleted`          |not set if `false`|
+|`skip_deleted`                |not set if `false`|
 Whether deleted files should be skipped on file switch.
-|`skip_uncommented`      |not set if `false`|
+|`skip_uncommented`            |not set if `false`|
 Whether uncommented files should be skipped on file switch.
-|`syntax_highlighting`   |not set if `false`|
+|`syntax_highlighting`         |not set if `false`|
 Whether syntax highlighting should be enabled.
-|`hide_top_menu`         |not set if `false`|
+|`hide_top_menu`               |not set if `false`|
 If true the top menu header and site header is hidden.
-|`hide_line_numbers`     |not set if `false`|
+|`auto_hide_diff_table_header` |not set if `false`|
+If true the diff table header is automatically hidden when
+scrolling down more than half of a page.
+|`hide_line_numbers`           |not set if `false`|
 If true the line numbers are hidden.
-|`tab_size`              ||
+|`tab_size`                    ||
 Number of spaces that should be used to display one tab.
-|=====================================
+|'hide_empty_pane'             |not set if `false`|
+Whether empty panes should be hidden. The left pane is empty when a
+file was added; the right pane is empty when a file was deleted.
+|===========================================
 
 [[diff-preferences-input]]
 === DiffPreferencesInput
@@ -1466,56 +1415,59 @@
 diff preferences of a user. Fields which are not set will not be
 updated.
 
-[options="header",width="50%",cols="1,^1,5"]
-|=====================================
-|Field Name              ||Description
-|`context`               |optional|
+[options="header",cols="1,^1,5"]
+|===========================================
+|Field Name                    ||Description
+|`context`                     |optional|
 The number of lines of context when viewing a patch.
-|`expand_all_comments`   |optional|
+|`expand_all_comments`         |optional|
 Whether all inline comments should be automatically expanded.
-|`ignore_whitespace`     |optional|
+|`ignore_whitespace`           |optional|
 Whether whitespace changes should be ignored and if yes, which
 whitespace changes should be ignored. +
 Allowed values are `IGNORE_NONE`, `IGNORE_SPACE_AT_EOL`,
 `IGNORE_SPACE_CHANGE`, `IGNORE_ALL_SPACE`.
-|`intraline_difference`  |optional|
+|`intraline_difference`        |optional|
 Whether intraline differences should be highlighted.
-|`line_length`           |optional|
+|`line_length`                 |optional|
 Number of characters that should be displayed in one line.
-|`manual_review`         |optional|
+|`manual_review`               |optional|
 Whether the 'Reviewed' flag should not be set automatically on a patch
 when it is viewed.
-|`retain_header`         |optional|
+|`retain_header`               |optional|
 Whether the header that is displayed above the patch (that either shows
 the commit message, the diff preferences, the patch sets or the files)
 should be retained on file switch.
-|`show_line_endings`     |optional|
+|`show_line_endings`           |optional|
 Whether Windows EOL/Cr-Lf should be displayed as '\r' in a dotted-line
 box.
-|`show_tabs`             |optional|
+|`show_tabs`                   |optional|
 Whether tabs should be shown.
-|`show_whitespace_errors`|optional|
+|`show_whitespace_errors`      |optional|
 Whether whitespace errors should be shown.
-|`skip_deleted`          |optional|
+|`skip_deleted`                |optional|
 Whether deleted files should be skipped on file switch.
-|`skip_uncommented`      |optional|
+|`skip_uncommented`            |optional|
 Whether uncommented files should be skipped on file switch.
-|`syntax_highlighting`   |optional|
+|`syntax_highlighting`         |optional|
 Whether syntax highlighting should be enabled.
-|`hide_top_menu`         |optional|
+|`hide_top_menu`               |optional|
 True if the top menu header and site header should be hidden.
-|`hide_line_numbers`     |optional|
+|`auto_hide_diff_table_header` |optional|
+True if the diff table header is automatically hidden when
+scrolling down more than half of a page.
+|`hide_line_numbers`           |optional|
 True if the line numbers should be hidden.
-|`tab_size`              |optional|
+|`tab_size`                    |optional|
 Number of spaces that should be used to display one tab.
-|=====================================
+|===========================================
 
 [[email-info]]
 === EmailInfo
 The `EmailInfo` entity contains information about an email address of a
 user.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |========================
 |Field Name ||Description
 |`email`    ||The email address.
@@ -1532,7 +1484,7 @@
 The `EmailInput` entity contains information for registering a new
 email address.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |==============================
 |Field Name       ||Description
 |`email`          ||
@@ -1554,7 +1506,7 @@
 The `HttpPasswordInput` entity contains information for setting/generating
 an HTTP password.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |============================
 |Field Name     ||Description
 |`generate`     |`false` if not set|
@@ -1566,12 +1518,97 @@
 password is deleted.
 |============================
 
+[[preferences-info]]
+=== PreferencesInfo
+The `PreferencesInfo` entity contains information about a user's preferences.
+
+[options="header",cols="1,^1,5"]
+|=====================================
+|Field Name              ||Description
+|`changes_per_page`               ||
+The number of changes to show on each page.
+Allowed values are `10`, `25`, `50`, `100`.
+|`show_site_header`   |not set if `false`|
+Whether the site header should be shown.
+|`use_flash_clipboard`     |not set if `false`|
+Whether to use the flash clipboard widget.
+|`download_scheme`      ||
+The type of download URL the user prefers to use.
+|`download_command`     ||
+The type of download command the user prefers to use.
+|`copy_self_on_email`       |not set if `false`|
+Whether to CC me on comments I write.
+|`date_format`         ||
+The format to display the date in.
+Allowed values are `STD`, `US`, `ISO`, `EURO`, `UK`.
+|`time_format`     ||
+The format to display the time in.
+Allowed values are `HHMM_12`, `HHMM_24`.
+|`relative_date_in_change_table`  |not set if `false`|
+Whether to show relative dates in the changes table.
+|`size_bar_in_change_table`      |not set if `false`|
+Whether to show the change sizes as colored bars in the change table.
+|`legacycid_in_change_table`      |not set if `false`|
+Whether to show change number in the change table.
+|`mute_common_path_prefixes` |not set if `false`|
+Whether to mute common path prefixes in file names in the file table.
+|`review_category_strategy`   ||
+The strategy used to displayed info in the review category column.
+Allowed values are `NONE`, `NAME`, `EMAIL`, `USERNAME`, `ABBREV`.
+|`diff_view`     ||
+The type of diff view to show.
+Allowed values are `SIDE_BY_SIDE`, `UNIFIED_DIFF`.
+|=====================================
+
+[[preferences-input]]
+=== PreferencesInput
+The `PreferencesInput` entity contains information for setting the
+user preferences. Fields which are not set will not be updated.
+
+[options="header",cols="1,^1,5"]
+|=====================================
+|Field Name              ||Description
+|`changes_per_page`               |optional|
+The number of changes to show on each page.
+Allowed values are `10`, `25`, `50`, `100`.
+|`show_site_header`   |optional|
+Whether the site header should be shown.
+|`use_flash_clipboard`     |optional|
+Whether to use the flash clipboard widget.
+|`download_scheme`      |optional|
+The type of download URL the user prefers to use.
+|`download_command`     |optional|
+The type of download command the user prefers to use.
+|`copy_self_on_email`       |optional|
+Whether to CC me on comments I write.
+|`date_format`         |optional|
+The format to display the date in.
+Allowed values are `STD`, `US`, `ISO`, `EURO`, `UK`.
+|`time_format`     |optional|
+The format to display the time in.
+Allowed values are `HHMM_12`, `HHMM_24`.
+|`relative_date_in_change_table`  |optional|
+Whether to show relative dates in the changes table.
+|`size_bar_in_change_table`      |optional|
+Whether to show the change sizes as colored bars in the change table.
+|`legacycid_in_change_table`      |optional|
+Whether to show change number in the change table.
+|`mute_common_path_prefixes` |optional|
+Whether to mute common path prefixes in file names in the file table.
+|`review_category_strategy`   |optional|
+The strategy used to displayed info in the review category column.
+Allowed values are `NONE`, `NAME`, `EMAIL`, `USERNAME`, `ABBREV`.
+|`diff_view`     |optional|
+The type of diff view to show.
+Allowed values are `SIDE_BY_SIDE`, `UNIFIED_DIFF`.
+|=====================================
+
 [[query-limit-info]]
 === QueryLimitInfo
 The `QueryLimitInfo` entity contains information about the
 link:access-control.html#capability_queryLimit[Query Limit] of a user.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",cols="1,6"]
 |================================
 |Field Name          |Description
 |`min`               |Lower limit.
@@ -1583,7 +1620,7 @@
 The `SshKeyInfo` entity contains information about an SSH key of a
 user.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |=============================
 |Field Name      ||Description
 |`seq`           ||The sequence number of the SSH key.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index e5fd1b6..8c10bc7 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -21,7 +21,7 @@
 .Request
 ----
   POST /changes HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "project" : "myProject",
@@ -39,7 +39,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -55,7 +55,6 @@
     "mergeable": true,
     "insertions": 0,
     "deletions": 0,
-    "_sortkey": "002cbc25000004e5",
     "_number": 4711,
     "owner": {
       "name": "John Doe"
@@ -89,7 +88,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -105,7 +104,6 @@
       "mergeable": true,
       "insertions": 26,
       "deletions": 10,
-      "_sortkey": "001e7057000006dc",
       "_number": 1756,
       "owner": {
         "name": "John Doe"
@@ -123,7 +121,6 @@
       "mergeable": true,
       "insertions": 12,
       "deletions": 18,
-      "_sortkey": "001e7056000006dd",
       "_number": 1757,
       "owner": {
         "name": "John Doe"
@@ -160,7 +157,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -177,7 +174,6 @@
         "mergeable": true,
         "insertions": 4,
         "deletions": 7,
-        "_sortkey": "001e7057000006dc",
         "_number": 1756,
         "owner": {
           "name": "John Doe"
@@ -285,8 +281,15 @@
 [[actions]]
 --
 * `CURRENT_ACTIONS`: include information on available actions
-  for the change and its current revision. The caller must be
-  authenticated to obtain the available actions.
+  for the change and its current revision. Ignored if the caller
+  is not authenticated.
+--
+
+[[change-actions]]
+--
+* `CHANGE_ACTIONS`: include information on available
+  change actions for the change. Ignored if the caller
+  is not authenticated.
 --
 
 [[reviewed]]
@@ -295,9 +298,16 @@
   authenticated and has commented on the current revision.
 --
 
-[[patch-set-links]]
+[[web-links]]
 --
-* `PATCHSET_LINKS`: include the `web_links` field.
+* `WEB_LINKS`: include the `web_links` field in link:#commit-info[CommitInfo],
+  therefore only valid in combination with `CURRENT_COMMIT` or
+  `ALL_COMMITS`.
+--
+
+[[check]]
+--
+* `CHECK`: include potential problems with the change.
 --
 
 .Request
@@ -309,7 +319,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -325,7 +335,6 @@
       "mergeable": true,
       "insertions": 16,
       "deletions": 7,
-      "_sortkey": "001c9bf400000061",
       "_number": 97,
       "owner": {
         "name": "Shawn Pearce"
@@ -334,6 +343,7 @@
       "revisions": {
         "184ebe53805e102605d11f6b143486d15c23a09c": {
           "_number": 1,
+          "ref": "refs/changes/97/97/1",
           "fetch": {
             "git": {
               "url": "git://localhost/gerrit",
@@ -446,7 +456,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -461,7 +471,6 @@
     "mergeable": true,
     "insertions": 34,
     "deletions": 101,
-    "_sortkey": "0023412400000f7d",
     "_number": 3965,
     "owner": {
       "name": "John Doe"
@@ -499,7 +508,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -514,7 +523,6 @@
     "mergeable": true,
     "insertions": 126,
     "deletions": 11,
-    "_sortkey": "0023412400000f7d",
     "_number": 3965,
     "owner": {
       "_account_id": 1000096,
@@ -652,7 +660,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "Documentation"
@@ -674,7 +682,7 @@
 .Request
 ----
   PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/topic HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "topic": "Documentation"
@@ -687,7 +695,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "Documentation"
@@ -743,7 +751,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -758,7 +766,6 @@
     "mergeable": true,
     "insertions": 3,
     "deletions": 310,
-    "_sortkey": "0023412400000f7d",
     "_number": 3965,
     "owner": {
       "name": "John Doe"
@@ -774,7 +781,7 @@
 ----
   HTTP/1.1 409 Conflict
   Content-Disposition: attachment
-  Content-Type: text/plain;charset=UTF-8
+  Content-Type: text/plain; charset=UTF-8
 
   change is merged
 ----
@@ -802,7 +809,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -817,7 +824,6 @@
     "mergeable": true,
     "insertions": 2,
     "deletions": 13,
-    "_sortkey": "0023412400000f7d",
     "_number": 3965,
     "owner": {
       "name": "John Doe"
@@ -833,7 +839,7 @@
 ----
   HTTP/1.1 409 Conflict
   Content-Disposition: attachment
-  Content-Type: text/plain;charset=UTF-8
+  Content-Type: text/plain; charset=UTF-8
 
   change is new
 ----
@@ -846,9 +852,17 @@
 
 Rebases a change.
 
+Optionally, the parent revision can be changed to another patch set through the
+link:#rebase-input[RebaseInput] entity.
+
 .Request
 ----
   POST /changes/myProject~master~I3ea943139cb62e86071996f2480e58bf3eeb9dd2/rebase HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "base" : "1234",
+  }
 ----
 
 As response a link:#change-info[ChangeInfo] entity is returned that
@@ -859,7 +873,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -874,7 +888,6 @@
     "mergeable": false,
     "insertions": 33,
     "deletions": 9,
-    "_sortkey": "0024cf9a000012bf",
     "_number": 4799,
     "owner": {
       "name": "John Doe"
@@ -883,6 +896,7 @@
     "revisions": {
       "27cc4558b5a3d3387dd11ee2df7a117e7e581822": {
         "_number": 2,
+        "ref": "refs/changes/99/4799/2",
         "fetch": {
           "http": {
             "url": "http://gerrit:8080/myProject",
@@ -923,7 +937,7 @@
 ----
   HTTP/1.1 409 Conflict
   Content-Disposition: attachment
-  Content-Type: text/plain;charset=UTF-8
+  Content-Type: text/plain; charset=UTF-8
 
   The change could not be rebased due to a path conflict during merge.
 ----
@@ -951,7 +965,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -966,7 +980,6 @@
     "mergeable": true,
     "insertions": 6,
     "deletions": 4,
-    "_sortkey": "0023412400000f7d",
     "_number": 3965,
     "owner": {
       "name": "John Doe"
@@ -982,7 +995,7 @@
 ----
   HTTP/1.1 409 Conflict
   Content-Disposition: attachment
-  Content-Type: text/plain;charset=UTF-8
+  Content-Type: text/plain; charset=UTF-8
 
   change is new
 ----
@@ -1002,7 +1015,7 @@
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/submit HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "wait_for_merge": true
@@ -1016,7 +1029,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -1028,7 +1041,6 @@
     "status": "MERGED",
     "created": "2013-02-01 09:59:32.126000000",
     "updated": "2013-02-21 11:16:36.775000000",
-    "_sortkey": "0023412400000f7d",
     "_number": 3965,
     "owner": {
       "name": "John Doe"
@@ -1044,7 +1056,7 @@
 ----
   HTTP/1.1 409 Conflict
   Content-Disposition: attachment
-  Content-Type: text/plain;charset=UTF-8
+  Content-Type: text/plain; charset=UTF-8
 
   blocked by Verified
 ----
@@ -1060,7 +1072,7 @@
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/publish HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 ----
 
 .Response
@@ -1079,7 +1091,7 @@
 .Request
 ----
   DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940 HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 ----
 
 .Response
@@ -1105,7 +1117,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -1124,10 +1136,6 @@
 
 Adds or updates the change in the secondary index.
 
-The caller must be a member of a group that is granted the
-link:access-control.html#capability_administrateServer[
-Administrate Server] capability.
-
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/index HTTP/1.0
@@ -1138,6 +1146,465 @@
   HTTP/1.1 204 No Content
 ----
 
+[[check-change]]
+=== Check change
+--
+'GET /changes/link:#change-id[\{change-id\}]/check'
+--
+
+Performs consistency checks on the change, and returns a
+link:#change-info[ChangeInfo] entity with the `problems` field set to a
+list of link:#problem-info[ProblemInfo] entities.
+
+Depending on the type of problem, some fields not marked optional may be
+missing from the result. At least `id`, `project`, `branch`, and
+`_number` will be present.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/check HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "project": "myProject",
+    "branch": "master",
+    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "subject": "Implementing Feature X",
+    "status": "NEW",
+    "created": "2013-02-01 09:59:32.126000000",
+    "updated": "2013-02-21 11:16:36.775000000",
+    "mergeable": true,
+    "insertions": 34,
+    "deletions": 101,
+    "_number": 3965,
+    "owner": {
+      "name": "John Doe"
+    },
+    "problems": [
+      {
+        "message": "Current patch set 1 not found"
+      }
+    ]
+  }
+----
+
+[[fix-change]]
+=== Fix change
+--
+'POST /changes/link:#change-id[\{change-id\}]/check'
+--
+
+Performs consistency checks on the change as with link:#check-change[GET
+/check], and additionally fixes any problems that can be fixed
+automatically. The returned field values reflect any fixes.
+
+Only the change owner, a project owner, or an administrator may fix changes.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/check HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "project": "myProject",
+    "branch": "master",
+    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "subject": "Implementing Feature X",
+    "status": "MERGED",
+    "created": "2013-02-01 09:59:32.126000000",
+    "updated": "2013-02-21 11:16:36.775000000",
+    "mergeable": true,
+    "insertions": 34,
+    "deletions": 101,
+    "_number": 3965,
+    "owner": {
+      "name": "John Doe"
+    },
+    "problems": [
+      {
+        "message": "Current patch set 2 not found"
+      },
+      {
+        "message": "Patch set 1 (1eee2c9d8f352483781e772f35dc586a69ff5646) is merged into destination ref master (1eee2c9d8f352483781e772f35dc586a69ff5646), but change status is NEW",
+        "status": FIXED,
+        "outcome": "Marked change as merged"
+      }
+    ]
+  }
+----
+
+[[edit-endpoints]]
+== Change Edit Endpoints
+
+These endpoints are considered to be unstable and can be changed in
+backwards incompatible way any time without notice.
+
+[[get-edit-detail]]
+=== Get Change Edit Details
+--
+'GET /changes/link:#change-id[\{change-id\}]/edit
+--
+
+Retrieves a change edit details.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit HTTP/1.0
+----
+
+As response an link:#edit-info[EditInfo] entity is returned that
+describes the change edit, or "`204 No Content`" when change edit doesn't
+exist for this change. Change edits are stored on special branches and there
+can be max one edit per user per change. Edits aren't tracked in the database.
+When request parameter `list` is provided the response also includes the file
+list. When `base` request parameter is provided the file list is computed
+against this base revision. When request parameter `download-commands` is
+provided fetch info map is also included.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "commit":{
+      "parents":[
+        {
+          "commit":"1eee2c9d8f352483781e772f35dc586a69ff5646",
+        }
+      ],
+      "author":{
+        "name":"Shawn O. Pearce",
+        "email":"sop@google.com",
+        "date":"2012-04-24 18:08:08.000000000",
+        "tz":-420
+       },
+       "committer":{
+         "name":"Shawn O. Pearce",
+         "email":"sop@google.com",
+         "date":"2012-04-24 18:08:08.000000000",
+         "tz":-420
+       },
+       "subject":"Use an EventBus to manage star icons",
+       "message":"Use an EventBus to manage star icons\n\nImage widgets that need to ..."
+    },
+  }
+----
+
+[[put-edit-file]]
+=== Change file content in Change Edit
+--
+'PUT /changes/link:#change-id[\{change-id\}]/edit/path%2fto%2ffile
+--
+
+Put content of a file to a change edit.
+
+.Request
+----
+  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit/foo HTTP/1.0
+----
+
+When change edit doesn't exist for this change yet it is created. When file
+content isn't provided, it is wiped out for that file. As response
+"`204 No Content`" is returned.
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[post-edit]]
+=== Restore file content or rename files in Change Edit
+--
+'POST /changes/link:#change-id[\{change-id\}]/edit
+--
+
+Creates empty change edit, restores file content or renames files in change
+edit. The request body needs to include a
+link:#change-edit-input[ChangeEditInput] entity when a file within change
+edit should be restored or old and new file names to rename a file.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "restore_path": "foo"
+  }
+----
+
+or for rename:
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "old_path": "foo",
+    "new_path": "bar"
+  }
+----
+
+When change edit doesn't exist for this change yet it is created. When path
+and restore flag are provided in request body, this file is restored. When
+old and new file names are provided, the file is renamed. As response
+"`204 No Content`" is returned.
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[put-change-edit-message]]
+=== Change commit message in Change Edit
+--
+'PUT /changes/link:#change-id[\{change-id\}]/edit:message
+--
+
+Modify commit message. The request body needs to include a
+link:#change-edit-message-input[ChangeEditMessageInput]
+entity.
+
+.Request
+----
+  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit:message HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "message": "New commit message\n\nChange-Id: I10394472cbd17dd12454f229e4f6de00b143a444"
+  }
+----
+
+If a change edit doesn't exist for this change yet, it is created. As
+response "`204 No Content`" is returned.
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[delete-edit-file]]
+=== Delete file in Change Edit
+--
+'DELETE /changes/link:#change-id[\{change-id\}]/edit/path%2fto%2ffile'
+--
+
+Deletes a file from a change edit. This deletes the file from the repository
+completely. This is not the same as reverting or restoring a file to its
+previous contents.
+
+.Request
+----
+  DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit/foo HTTP/1.0
+----
+
+When change edit doesn't exist for this change yet it is created.
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[get-edit-file]]
+=== Retrieve file content from Change Edit
+--
+'GET /changes/link:#change-id[\{change-id\}]/edit/path%2fto%2ffile
+--
+
+Retrieves content of a file from a change edit.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit/foo HTTP/1.0
+----
+
+The content of the file is returned as text encoded inside base64.
+The Content-Type header will always be `text/plain` reflecting the
+outer base64 encoding. A Gerrit-specific `X-FYI-Content-Type` header
+can be examined to find the server detected content type of the file.
+
+When the specified file was deleted in the change edit
+"`204 No Content`" is returned.
+
+If only the content type is required, callers should use HEAD to
+avoid downloading the encoded file contents.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: text/plain; charset=ISO-8859-1
+  X-FYI-Content-Encoding: base64
+  X-FYI-Content-Type: text/xml
+
+  RnJvbSA3ZGFkY2MxNTNmZGVhMTdhYTg0ZmYzMmE2ZTI0NWRiYjY...
+----
+
+Alternatively, if the only value of the Accept request header is
+`application/json` the content is returned as JSON string and
+`X-FYI-Content-Encoding` is set to `json`.
+
+[[get-edit-meta-data]]
+=== Retrieve meta data of a file from Change Edit
+--
+'GET /changes/link:#change-id[\{change-id\}]/edit/path%2fto%2ffile/meta
+--
+
+Retrieves meta data of a file from a change edit. Currently only
+web links are returned.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit/foo/meta HTTP/1.0
+----
+
+This REST endpoint retrieves additional information for a file in a
+change edit. As result an link:#edit-file-info[EditFileInfo] entity is
+returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+  "web_links":[
+    {
+      "show_on_side_by_side_diff_view": true,
+      "name": "side-by-side preview diff",
+      "image_url": "plugins/xdocs/static/sideBySideDiffPreview.png",
+      "url": "#/x/xdocs/c/42/1..0/README.md",
+      "target": "_self"
+    },
+    {
+      "show_on_unified_diff_view": true,
+      "name": "unified preview diff",
+      "image_url": "plugins/xdocs/static/unifiedDiffPreview.png",
+      "url": "#/x/xdocs/c/42/1..0/README.md,unified",
+      "target": "_self"
+    }
+  ]}
+----
+
+[[get-edit-message]]
+=== Retrieve commit message from Change Edit or current patch set of the change
+--
+'GET /changes/link:#change-id[\{change-id\}]/edit:message
+--
+
+Retrieves commit message from change edit.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit:message HTTP/1.0
+----
+
+The commit message is returned as base64 encoded string.
+
+.Response
+----
+  HTTP/1.1 200 OK
+
+  VGhpcyBpcyBhIGNvbW1pdCBtZXNzYWdlCgpDaGFuZ2UtSWQ6IElhYzhmZGM1MGRlZjFiYWUzYjAz
+M2JhNjcxZTk0OTBmNzUxNDU5ZGUzCg==
+----
+
+Alternatively, if the only value of the Accept request header is
+`application/json` the commit message is returned as JSON string:
+
+.Response
+----
+  HTTP/1.1 200 OK
+
+)]}'
+"Subject of the commit message\n\nThis is the body of the commit message.\n\nChange-Id: Iaf1ba916bf843c175673d675bf7f52862f452db9\n"
+----
+
+
+[[publish-edit]]
+=== Publish Change Edit
+--
+'POST /changes/link:#change-id[\{change-id\}]/edit:publish
+--
+
+Promotes change edit to a regular patch set.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit:publish HTTP/1.0
+----
+
+As response "`204 No Content`" is returned.
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[rebase-edit]]
+=== Rebase Change Edit
+--
+'POST /changes/link:#change-id[\{change-id\}]/edit:rebase
+--
+
+Rebases change edit on top of latest patch set.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit:rebase HTTP/1.0
+----
+
+When change was rebased on top of latest patch set, response
+"`204 No Content`" is returned. When change edit is aready
+based on top of the latest patch set, the response
+"`409 Conflict`" is returned.
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[delete-edit]]
+=== Delete Change Edit
+--
+'DELETE /changes/link:#change-id[\{change-id\}]/edit'
+--
+
+Deletes change edit.
+
+.Request
+----
+  DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit HTTP/1.0
+----
+
+As response "`204 No Content`" is returned.
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
 
 [[reviewer-endpoints]]
 == Reviewer Endpoints
@@ -1161,7 +1628,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -1206,7 +1673,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -1246,7 +1713,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -1274,7 +1741,7 @@
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "reviewer": "john.doe@example.com"
@@ -1288,7 +1755,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -1316,7 +1783,7 @@
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "reviewer": "MyProjectVerifiers"
@@ -1327,7 +1794,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -1342,7 +1809,7 @@
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "reviewer": "MyProjectVerifiers",
@@ -1391,7 +1858,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -1418,6 +1885,48 @@
   }
 ----
 
+Adding query parameter `links` (for example `/changes/.../commit?links`)
+returns a link:#commit-info[CommitInfo] with the additional field `web_links`.
+
+[[get-revision-actions]]
+=== Get Revision Actions
+--
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/actions'
+--
+
+Retrieves revision link:#action-info[actions] of the revision of a change.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/actions' HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+
+{
+  "submit": {
+    "method": "POST",
+    "label": "Submit",
+    "title": "Submit patch set 1 into master",
+    "enabled": true
+  },
+  "cherrypick": {
+    "method": "POST",
+    "label": "Cherry Pick",
+    "title": "Cherry pick change to a different branch",
+    "enabled": true
+  }
+}
+----
+
+The response is a flat map of possible revision actions mapped to their
+link:#action-info[ActionInfo].
 
 [[get-review]]
 === Get Review
@@ -1445,7 +1954,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -1460,7 +1969,6 @@
     "mergeable": true,
     "insertions": 34,
     "deletions": 45,
-    "_sortkey": "0023412400000f7d",
     "_number": 3965,
     "owner": {
       "_account_id": 1000096,
@@ -1543,6 +2051,7 @@
     "revisions": {
       "674ac754f91e64a0efb8087e59a176484bd534d1": {
       "_number": 2,
+      "ref": "refs/changes/65/3965/2",
       "fetch": {
         "http": {
           "url": "http://gerrit/myProject",
@@ -1574,7 +2083,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -1640,7 +2149,7 @@
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/review HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "message": "Some nits need to be fixed.",
@@ -1678,7 +2187,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -1688,6 +2197,9 @@
   }
 ----
 
+A review cannot be set on a change edit. Trying to post a review for a
+change edit fails with `409 Conflict`.
+
 [[rebase-revision]]
 === Rebase Revision
 --
@@ -1696,9 +2208,17 @@
 
 Rebases a revision.
 
+Optionally, the parent revision can be changed to another patch set through the
+link:#rebase-input[RebaseInput] entity.
+
 .Request
 ----
   POST /changes/myProject~master~I3ea943139cb62e86071996f2480e58bf3eeb9dd2/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/rebase HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "base" : "1234",
+  }
 ----
 
 As response a link:#change-info[ChangeInfo] entity is returned that
@@ -1709,7 +2229,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -1724,7 +2244,6 @@
     "mergeable": false,
     "insertions": 21,
     "deletions": 21,
-    "_sortkey": "0024cf9a000012bf",
     "_number": 4799,
     "owner": {
       "name": "John Doe"
@@ -1733,6 +2252,7 @@
     "revisions": {
       "27cc4558b5a3d3387dd11ee2df7a117e7e581822": {
         "_number": 2,
+        "ref": "refs/changes/99/4799/2",
         "fetch": {
           "http": {
             "url": "http://gerrit:8080/myProject",
@@ -1773,7 +2293,7 @@
 ----
   HTTP/1.1 409 Conflict
   Content-Disposition: attachment
-  Content-Type: text/plain;charset=UTF-8
+  Content-Type: text/plain; charset=UTF-8
 
   The change could not be rebased due to a path conflict during merge.
 ----
@@ -1793,7 +2313,7 @@
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/submit HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "wait_for_merge": true
@@ -1807,7 +2327,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -1823,7 +2343,7 @@
 .Response
 ----
   HTTP/1.1 409 Conflict
-  Content-Type: text/plain;charset=UTF-8
+  Content-Type: text/plain; charset=UTF-8
 
   "revision 674ac754f91e64a0efb8087e59a176484bd534d1 is not current revision"
 ----
@@ -1839,7 +2359,7 @@
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/publish HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 ----
 
 .Response
@@ -1858,7 +2378,7 @@
 .Request
 ----
   DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1 HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 ----
 
 .Response
@@ -1885,7 +2405,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: text/plain;charset=ISO-8859-1
+  Content-Type: text/plain; charset=ISO-8859-1
   X-FYI-Content-Encoding: base64
   X-FYI-Content-Type: application/mbox
 
@@ -1921,7 +2441,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -1933,9 +2453,6 @@
 If the `other-branches` parameter is specified, the mergeability will also be
 checked for all other branches.
 
-If the `force` parameter is specified, the mergeability against the destination
-will be rechecked, in case of prior transient failures or bugs.
-
 .Request
 ----
   GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/mergeable?other-branches HTTP/1.0
@@ -1948,7 +2465,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -1978,7 +2495,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "MERGE_IF_NECESSARY"
@@ -2000,7 +2517,7 @@
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/test.submit_type HTTP/1.0
-  Content-Type: text/plain;charset-UTF-8
+  Content-Type: text/plain; charset-UTF-8
 
   submit_type(cherry_pick).
 ----
@@ -2009,7 +2526,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "CHERRY_PICK"
@@ -2031,7 +2548,7 @@
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/test.submit_rule?filters=SKIP HTTP/1.0
-  Content-Type: text/plain;charset-UTF-8
+  Content-Type: text/plain; charset-UTF-8
 
   submit_rule(submit(R)) :-
     R = label('Any-Label-Name', reject(_)).
@@ -2044,7 +2561,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -2057,6 +2574,17 @@
   ]
 ----
 
+When testing with the `curl` command line client the
+`--data-binary @rules.pl` flag should be used to ensure
+all LFs are included in the Prolog code:
+
+----
+  curl -X POST \
+    -H 'Content-Type: text/plain; charset=UTF-8' \
+    --data-binary @rules.pl \
+    http://.../test.submit_rule
+----
+
 [[list-drafts]]
 === List Drafts
 --
@@ -2079,7 +2607,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -2115,7 +2643,7 @@
 .Request
 ----
   PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/drafts HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "path": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
@@ -2131,7 +2659,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -2164,7 +2692,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -2190,7 +2718,7 @@
 .Request
 ----
   PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/drafts/TvcXrmjM HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "path": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
@@ -2206,7 +2734,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -2259,7 +2787,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -2311,7 +2839,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -2349,7 +2877,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -2368,6 +2896,11 @@
 of the paths the caller has marked as reviewed.  Clients that also
 need the FileInfo should make two requests.
 
+The request parameter `q` changes the response to return a list
+of all files (modified or unmodified) that contain that substring
+in the path name. This is useful to implement suggestion services
+finding a file by partial name.
+
 .Request
 ----
   GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/?reviewed HTTP/1.0
@@ -2377,7 +2910,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -2399,17 +2932,29 @@
   GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/content HTTP/1.0
 ----
 
-The content is returned as base64 encoded string.
+The content is returned as base64 encoded string.  The HTTP response
+Content-Type is always `text/plain`, reflecting the base64 wrapping.
+A Gerrit-specific `X-FYI-Content-Type` header is returned describing
+the server detected content type of the file.
+
+If only the content type is required, callers should use HEAD to
+avoid downloading the encoded file contents.
 
 .Response
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: text/plain;charset=UTF-8
+  Content-Type: text/plain; charset=ISO-8859-1
+  X-FYI-Content-Encoding: base64
+  X-FYI-Content-Type: text/xml
 
   Ly8gQ29weXJpZ2h0IChDKSAyMDEwIFRoZSBBbmRyb2lkIE9wZW4gU291cmNlIFByb2plY...
 ----
 
+Alternatively, if the only value of the Accept request header is
+`application/json` the content is returned as JSON string and
+`X-FYI-Content-Encoding` is set to `json`.
+
 [[get-diff]]
 === Get Diff
 --
@@ -2429,7 +2974,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]
   {
@@ -2499,7 +3044,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]
   {
@@ -2544,6 +3089,9 @@
 The `base` parameter can be specified to control the base patch set from which the diff should
 be generated.
 
+[[weblinks-only]]
+If the `weblinks-only` parameter is specified, only the diff web links are returned.
+
 .Request
 ----
   GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/b6b9c10649b9041884046119ab794374470a1b45/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/diff?base=2 HTTP/1.0
@@ -2553,7 +3101,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]
   {
@@ -2635,7 +3183,7 @@
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/cherrypick HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "message" : "Implementing Feature X",
@@ -2650,7 +3198,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -2665,58 +3213,6 @@
     "mergeable": true,
     "insertions": 12,
     "deletions": 11,
-    "_sortkey": "0023412400000f7d",
-    "_number": 3965,
-    "owner": {
-      "name": "John Doe"
-    }
-  }
-----
-
-[[message]]
-=== Edit Commit Message
---
-'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/message'
---
-
-Edit commit message.
-
-The commit message must be provided in the request body inside a
-link:#cherrypick-input[CherryPickInput] entity.
-
-.Request
-----
-  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/message HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
-
-  {
-    "message" : "Reword Implementing Feature X",
-  }
-----
-
-As response a link:#change-info[ChangeInfo] entity is returned that
-describes the change.
-
-.Response
-----
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
-
-  )]}'
-  {
-    "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9941",
-    "project": "myProject",
-    "branch": "release-branch",
-    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9941",
-    "subject": "Reword Implementing Feature X",
-    "status": "NEW",
-    "created": "2013-02-01 09:59:32.126000000",
-    "updated": "2013-02-21 11:16:36.775000000",
-    "mergeable": true,
-    "insertions": 261,
-    "deletions": 101,
-    "_sortkey": "0023412400000f7d",
     "_number": 3965,
     "owner": {
       "name": "John Doe"
@@ -2777,7 +3273,7 @@
 === AbandonInput
 The `AbandonInput` entity contains information for abandoning a change.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
 |`message`     |optional|
@@ -2791,7 +3287,7 @@
 make to manipulate a resource. These are frequently implemented by
 plugins and may be discovered at runtime.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |====================================
 |Field Name             ||Description
 |`method`               |optional|
@@ -2816,7 +3312,7 @@
 The `AddReviewerResult` entity describes the result of adding a
 reviewer to a change.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
 |`reviewers`   |optional|
@@ -2839,7 +3335,7 @@
 link:rest-api-accounts.html#account-info[AccountInfo].
 In addition `ApprovalInfo` has the following fields:
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
 |`value`       |optional|
@@ -2850,22 +3346,35 @@
 The time and date describing when the approval was made.
 |===========================
 
-[[group-base-info]]
-=== GroupBaseInfo
-The `GroupBaseInfo` entity contains base information about the group.
+[[change-edit-input]]
+=== ChangeEditInput
+The `ChangeEditInput` entity contains information for restoring a
+path within change edit.
 
-[options="header",width="50%",cols="1,6"]
-|==========================
-|Field Name    |Description
-|`id`          |The id of the group.
-|`name`        |The name of the group.
-|==========================
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`restore_path`|optional|Path to file to restore.
+|`old_path`    |optional|Old path to file to rename.
+|`new_path`    |optional|New path to file to rename.
+|===========================
+
+[[change-edit-message-input]]
+=== ChangeEditMessageInput
+The `ChangeEditMessageInput` entity contains information for changing
+the commit message within a change edit.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`message`     ||New commit message.
+|===========================
 
 [[change-info]]
 === ChangeInfo
 The `ChangeInfo` entity contains information about a change.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |==================================
 |Field Name           ||Description
 |`id`                 ||
@@ -2901,7 +3410,6 @@
 Number of inserted lines.
 |`deletions`          ||
 Number of deleted lines.
-|`_sortkey`           ||The sortkey of the change.
 |`_number`            ||The legacy numeric ID of the change.
 |`owner`              ||
 The owner of the change as an link:rest-api-accounts.html#account-info[
@@ -2939,46 +3447,21 @@
 if link:#all-revisions[all revisions] are requested.
 |`_more_changes`      |optional, not set if `false`|
 Whether the query would deliver more results if not limited. +
-Only set on either the last or the first change that is returned.
+Only set on the last change that is returned.
+|`problems`           |optional|
+A list of link:#problem-info[ProblemInfo] entities describing potential
+problems with this change. Only set if link:#check[CHECK] is set.
+|`base_change`        |optional|
+A link:#change-id[\{change-id\}] that identifies the base change for a create
+change operation. Only used for the link:#create-change[CreateChange] endpoint.
 |==================================
 
-[[related-changes-info]]
-=== RelatedChangesInfo
-The `RelatedChangesInfo` entity contains information about related
-changes.
-
-[options="header",width="50%",cols="1,6"]
-|===========================
-|Field Name                |Description
-|`changes`                 |A list of
-link:#related-change-and-commit-info[RelatedChangeAndCommitInfo] entities
-describing the related changes. Sorted by git commit order, newest to
-oldest. Empty if there are no related changes.
-|===========================
-
-[[related-change-and-commit-info]]
-=== RelatedChangeAndCommitInfo
-
-The `RelatedChangeAndCommitInfo` entity contains information about
-a related change and commit.
-
-[options="header",width="50%",cols="1,^1,5"]
-|===========================
-|Field Name                ||Description
-|`change_id`               |optional|The Change-Id of the change.
-|`commit`                  ||The commit as a
-link:#commit-info[CommitInfo] entity.
-|`_change_number`          |optional|The change number.
-|`_revision_number`        |optional|The revision number.
-|`_current_revision_number`|optional|The current revision number.
-|===========================
-
 [[change-message-info]]
 === ChangeMessageInfo
 The `ChangeMessageInfo` entity contains information about a message
 attached to a change.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |==================================
 |Field Name           ||Description
 |`id`                 ||The ID of the message.
@@ -2997,7 +3480,7 @@
 === CherryPickInput
 The `CherryPickInput` entity contains information for cherry-picking a change to a new branch.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",cols="1,6"]
 |===========================
 |Field Name    |Description
 |`message`     |Commit message for the cherry-picked change
@@ -3008,7 +3491,7 @@
 === CommentInfo
 The `CommentInfo` entity contains information about an inline comment.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
 |`id`          ||The URL encoded UUID of the comment.
@@ -3043,7 +3526,7 @@
 The `CommentInput` entity contains information for creating an inline
 comment.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
 |`id`          |optional|
@@ -3080,7 +3563,7 @@
 === CommentRange
 The `CommentRange` entity describes the range of an inline comment.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
 |`start_line`        ||The start line number of the range.
@@ -3093,7 +3576,7 @@
 === CommitInfo
 The `CommitInfo` entity contains information about a commit.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",cols="1,6"]
 |==========================
 |Field Name    |Description
 |`commit`      |The commit ID.
@@ -3108,6 +3591,9 @@
 |`subject`     |
 The subject of the commit (header line of the commit message).
 |`message`     |The commit message.
+|`web_links`   |optional|
+Links to the commit in external sites as a list of
+link:#web-link-info[WebLinkInfo] entities.
 |==========================
 
 [[diff-content]]
@@ -3115,7 +3601,7 @@
 The `DiffContent` entity contains information about the content differences
 in a file.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |==========================
 |Field Name ||Description
 |`a`        |optional|Content only in the file on side A (deleted in B).
@@ -3139,12 +3625,15 @@
 === DiffFileMetaInfo
 The `DiffFileMetaInfo` entity contains meta information about a file diff.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",cols="1,^1,5"]
 |==========================
-|Field Name    |Description
-|`name`        |The name of the file.
-|`content_type`|The content type of the file.
-|`lines`       |The total number of lines in the file.
+|Field Name    ||Description
+|`name`        ||The name of the file.
+|`content_type`||The content type of the file.
+|`lines`       ||The total number of lines in the file.
+|`web_links`   |optional|
+Links to the file in external sites as a list of
+link:rest-api-changes.html#web-link-info[WebLinkInfo] entries.
 |==========================
 
 [[diff-info]]
@@ -3152,7 +3641,10 @@
 The `DiffInfo` entity contains information about the diff of a file
 in a revision.
 
-[options="header",width="50%",cols="1,^1,5"]
+If the link:#weblinks-only[weblinks-only] parameter is specified, only
+the `web_links` field is set.
+
+[options="header",cols="1,^1,5"]
 |==========================
 |Field Name        ||Description
 |`meta_a`          |not present when the file is added|
@@ -3168,6 +3660,10 @@
 |`diff_header`     ||A list of strings representing the patch set diff header.
 |`content`         ||The content differences in the file as a list of
 link:#diff-content[DiffContent] entities.
+|`web_links`       |optional|
+Links to the file diff in external sites as a list of
+link:rest-api-changes.html#diff-web-link-info[DiffWebLinkInfo] entries.
+|`binary`          |not set if `false`|Whether the file is binary.
 |==========================
 
 [[diff-intraline-info]]
@@ -3184,12 +3680,61 @@
 Note that the implied newline character at the end of each line is included in
 the length calculation, and thus it is possible for the edits to span newlines.
 
+[[diff-web-link-info]]
+=== DiffWebLinkInfo
+The `DiffWebLinkInfo` entity describes a link on a diff screen to an
+external site.
+
+[options="header",cols="1,6"]
+|=======================
+|Field Name|Description
+|`name`     |The link name.
+|`url`      |The link URL.
+|`image_url`|URL to the icon of the link.
+|show_on_side_by_side_diff_view|
+Whether the web link should be shown on the side-by-side diff screen.
+|show_on_unified_diff_view|
+Whether the web link should be shown on the unified diff screen.
+|=======================
+
+[[edit-file-info]]
+=== EditFileInfo
+The `EditFileInfo` entity contains additional information
+of a file within a change edit.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`web_links`   |optional|
+Links to the diff info in external sites as a list of
+link:#web-link-info[WebLinkInfo] entities.
+|===========================
+
+[[edit-info]]
+=== EditInfo
+The `EditInfo` entity contains information about a change edit.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`commit`      ||The commit of change edit as
+link:#commit-info[CommitInfo] entity.
+|`baseRevision`||The revision of the patch set change edit is based on.
+|`fetch`       ||
+Information about how to fetch this patch set. The fetch information is
+provided as a map that maps the protocol name ("`git`", "`http`",
+"`ssh`") to link:#fetch-info[FetchInfo] entities.
+|`files`       |optional|
+The files of the change edit as a map that maps the file names to
+link:#file-info[FileInfo] entities.
+|===========================
+
 [[fetch-info]]
 === FetchInfo
 The `FetchInfo` entity contains information about how to fetch a patch
 set via a certain protocol.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |==========================
 |Field Name    ||Description
 |`url`         ||The URL of the project.
@@ -3204,7 +3749,7 @@
 === FileInfo
 The `FileInfo` entity contains information about a file in a patch set.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |=============================
 |Field Name      ||Description
 |`status`        |optional|
@@ -3228,7 +3773,7 @@
 The `GitPersonInfo` entity contains information about the
 author/committer of a commit.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",cols="1,6"]
 |==========================
 |Field Name    |Description
 |`name`        |The name of the author/committer.
@@ -3239,6 +3784,31 @@
 constructed.
 |==========================
 
+[[group-base-info]]
+=== GroupBaseInfo
+The `GroupBaseInfo` entity contains base information about the group.
+
+[options="header",cols="1,6"]
+|==========================
+|Field Name    |Description
+|`id`          |The id of the group.
+|`name`        |The name of the group.
+|==========================
+
+[[included-in-info]]
+=== IncludedInInfo
+The `IncludedInInfo` entity contains information about the branches a
+change was merged into and tags it was tagged with.
+
+[options="header",cols="1,6"]
+|==========================
+|Field Name |Description
+|`branches` | The list of branches this change was merged into.
+Each branch is listed without the 'refs/head/' prefix.
+|`tags`     | The list of tags this change was tagged with.
+Each tag is listed without the 'refs/tags/' prefix.
+|==========================
+
 [[label-info]]
 === LabelInfo
 The `LabelInfo` entity contains information about a label on a change, always
@@ -3252,7 +3822,7 @@
   users and the allowed range of votes for the current user, use `DETAILED_LABELS`.
 
 ==== Common fields
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
 |`optional`    |not set if `false`|
@@ -3262,7 +3832,7 @@
 |===========================
 
 ==== Fields set by `LABELS`
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
 |`approved`    |optional|One user who approved this label on the change
@@ -3287,7 +3857,7 @@
 |===========================
 
 ==== Fields set by `DETAILED_LABELS`
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
 |`all`         |optional|List of all approvals for this label as a list
@@ -3302,7 +3872,7 @@
 The `MergeableInfo` entity contains information about the mergeability of a
 change.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |============================
 |Field Name      ||Description
 |`submit_type`   ||
@@ -3315,11 +3885,76 @@
 A list of other branch names where this change could merge cleanly
 |============================
 
+[[problem-info]]
+=== ProblemInfo
+The `ProblemInfo` entity contains a description of a potential consistency problem
+with a change. These are not related to the code review process, but rather
+indicate some inconsistency in Gerrit's database or repository metadata related
+to the enclosing change.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name||Description
+|`message` ||Plaintext message describing the problem with the change.
+|`status`  |optional|
+The status of fixing the problem (`FIXED`, `FIX_FAILED`). Only set if a
+fix was attempted.
+|`outcome` |optional|
+If `status` is set, an additional plaintext message describing the
+outcome of the fix.
+|===========================
+
+[[rebase-input]]
+=== RebaseInput
+The `RebaseInput` entity contains information for changing parent when rebasing.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`base`        |optional|
+The new parent revision. This can be a ref or a SHA1 to a concrete patchset. +
+Alternatively, a change number can be specified, in which case the current
+patch set is inferred. +
+Empty string is used for rebasing directly on top of the target branch,
+which effectively breaks dependency towards a parent change.
+|===========================
+
+[[related-change-and-commit-info]]
+=== RelatedChangeAndCommitInfo
+
+The `RelatedChangeAndCommitInfo` entity contains information about
+a related change and commit.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name                ||Description
+|`change_id`               |optional|The Change-Id of the change.
+|`commit`                  ||The commit as a
+link:#commit-info[CommitInfo] entity.
+|`_change_number`          |optional|The change number.
+|`_revision_number`        |optional|The revision number.
+|`_current_revision_number`|optional|The current revision number.
+|===========================
+
+[[related-changes-info]]
+=== RelatedChangesInfo
+The `RelatedChangesInfo` entity contains information about related
+changes.
+
+[options="header",cols="1,6"]
+|===========================
+|Field Name                |Description
+|`changes`                 |A list of
+link:#related-change-and-commit-info[RelatedChangeAndCommitInfo] entities
+describing the related changes. Sorted by git commit order, newest to
+oldest. Empty if there are no related changes.
+|===========================
+
 [[restore-input]]
 === RestoreInput
 The `RestoreInput` entity contains information for restoring a change.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
 |`message`     |optional|
@@ -3331,7 +3966,7 @@
 === RevertInput
 The `RevertInput` entity contains information for reverting a change.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
 |`message`     |optional|
@@ -3343,7 +3978,7 @@
 === ReviewInfo
 The `ReviewInfo` entity contains information about a review.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",cols="1,6"]
 |===========================
 |Field Name     |Description
 |`labels`       |
@@ -3356,7 +3991,7 @@
 The `ReviewInput` entity contains information for adding a review to a
 revision.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |============================
 |Field Name     ||Description
 |`message`      |optional|
@@ -3401,7 +4036,7 @@
 link:#detailed-accounts[detailed account information].
 In addition `ReviewerInfo` has the following fields:
 
-[options="header",width="50%",cols="1,6"]
+[options="header",cols="1,6"]
 |==========================
 |Field Name    |Description
 |`approvals`   |
@@ -3414,7 +4049,7 @@
 The `ReviewerInput` entity contains information for adding a reviewer
 to a change.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
 |`reviewer`    ||
@@ -3433,38 +4068,53 @@
 [[revision-info]]
 === RevisionInfo
 The `RevisionInfo` entity contains information about a patch set.
+Not all fields are returned by default.  Additional fields can
+be obtained by adding `o` parameters as described in
+link:#list-changes[Query Changes].
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
 |`draft`       |not set if `false`|Whether the patch set is a draft.
 |`has_draft_comments`       |not set if `false`|Whether the patch
 set has one or more draft comments by the calling user. Only set if
-link:#draft_comments[draft comments] is requested.
+link:#draft_comments[DRAFT_COMMENTS] option is requested.
 |`_number`     ||The patch set number.
+|`created`     ||
+The link:rest-api.html#timestamp[timestamp] of when the patch set was
+created.
+|`uploader`    ||
+The uploader of the patch set as an
+link:rest-api-accounts.html#account-info[AccountInfo] entity.
+|`ref`         ||The Git reference for the patch set.
 |`fetch`       ||
 Information about how to fetch this patch set. The fetch information is
 provided as a map that maps the protocol name ("`git`", "`http`",
-"`ssh`") to link:#fetch-info[FetchInfo] entities.
+"`ssh`") to link:#fetch-info[FetchInfo] entities. This information is
+only included if a plugin implementing the
+link:intro-project-owner.html#download-commands[download commands]
+interface is installed.
 |`commit`      |optional|The commit of the patch set as
 link:#commit-info[CommitInfo] entity.
 |`files`       |optional|
 The files of the patch set as a map that maps the file names to
-link:#file-info[FileInfo] entities.
+link:#file-info[FileInfo] entities. Only set if
+link:#current-files[CURRENT_FILES] or link:#all-files[ALL_FILES]
+option is requested.
 |`actions`     |optional|
 Actions the caller might be able to perform on this revision. The
 information is a map of view name to link:#action-info[ActionInfo]
 entities.
-|'web_links'   |optional|
-Links to the patch set in external sites as a list of
-link:#web-link-info[WebLinkInfo] entities.
+|`reviewed`     |optional|
+Indicates whether the caller is authenticated and has commented on the
+current revision. Only set if link:#reviewed[REVIEWED] option is requested.
 |===========================
 
 [[rule-input]]
 === RuleInput
 The `RuleInput` entity contains information to test a Prolog rule.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name      ||Description
 |`rule`||
@@ -3477,22 +4127,12 @@
 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
 The `SubmitInfo` entity contains information about the change status
 after submitting.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",cols="1,6"]
 |==========================
 |Field Name    |Description
 |`status`      |
@@ -3516,7 +4156,7 @@
 === SubmitInput
 The `SubmitInput` entity contains information for submitting a change.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name      ||Description
 |`wait_for_merge`|`false` if not set|
@@ -3530,7 +4170,7 @@
 === SubmitRecord
 The `SubmitRecord` entity describes results from a submit_rule.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name      ||Description
 |`status`||
@@ -3561,40 +4201,37 @@
 the failure of the rule predicate.
 |===========================
 
+[[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.
+
 [[topic-input]]
 === TopicInput
 The `TopicInput` entity contains information for setting a topic.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
 |`topic`       |optional|The topic. +
 The topic will be deleted if not set.
 |===========================
 
-[[included-in-info]]
-=== IncludedInInfo
-The `IncludedInInfo` entity contains information about the branches a
-change was merged into and tags it was tagged with.
-
-[options="header",width="50%",cols="1,6"]
-|==========================
-|Field Name |Description
-|`branches` | The list of branches this change was merged into.
-Each branch is listed without the 'refs/head/' prefix.
-|`tags`     | The list of tags this change was tagged with.
-Each tag is listed without the 'refs/tags/' prefix.
-|==========================
-
 [[web-link-info]]
 === WebLinkInfo
 The `WebLinkInfo` entity describes a link to an external site.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",cols="1,6"]
 |======================
 |Field Name|Description
 |`name`    |The link name.
 |`url`     |The link URL.
+|`image_url`|URL to the icon of the link.
 |======================
 
 GERRIT
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 2968ebb..0ee6966 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -24,7 +24,7 @@
 .Response
 ----
   HTTP/1.1 200 OK
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "2.7"
@@ -55,7 +55,7 @@
 .Response
 ----
   HTTP/1.1 200 OK
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -280,7 +280,7 @@
 .Response
 ----
   HTTP/1.1 200 OK
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -325,7 +325,7 @@
 .Response
 ----
   HTTP/1.1 200 OK
-  Content-Type: text/plain;charset=UTF-8
+  Content-Type: text/plain; charset=UTF-8
 
   YWNjb3VudHMKYW...ViX3Nlc3Npb25z
 ----
@@ -354,7 +354,7 @@
 .Request
 ----
   POST /config/server/caches/ HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "operation": "FLUSH_ALL"
@@ -372,10 +372,10 @@
 .Request
 ----
   POST /config/server/caches/ HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
-    "operation": "FLUSH"
+    "operation": "FLUSH",
     "caches": [
       "projects",
       "project_list"
@@ -411,7 +411,7 @@
 .Response
 ----
   HTTP/1.1 200 OK
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -488,7 +488,7 @@
 .Response
 ----
   HTTP/1.1 200 OK
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -561,7 +561,7 @@
 .Response
 ----
   HTTP/1.1 200 OK
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -670,7 +670,7 @@
 .Response
 ----
   HTTP/1.1 200 OK
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -719,7 +719,7 @@
 .Response
 ----
   HTTP/1.1 200 OK
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -783,7 +783,7 @@
 .Response
 ----
   HTTP/1.1 200 OK
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -826,7 +826,7 @@
 === CacheInfo
 The `CacheInfo` entity contains information about a cache.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |==================================
 |Field Name           ||Description
 |`name`               |
@@ -847,23 +847,12 @@
 HitRatioInfo] entity.
 |==================================
 
-[[capability-info]]
-=== CapabilityInfo
-The `CapabilityInfo` entity contains information about a capability.
-
-[options="header",width="50%",cols="1,6"]
-|=================================
-|Field Name           |Description
-|`id`                 |capability ID
-|`name`               |capability name
-|=================================
-
 [[cache-operation-input]]
 === CacheOperationInput
 The `CacheOperationInput` entity contains information about an
 operation that should be executed on caches.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |==================================
 |Field Name           ||Description
 |`operation`          ||
@@ -878,12 +867,23 @@
 specified depends on the operation being executed.
 |==================================
 
+[[capability-info]]
+=== CapabilityInfo
+The `CapabilityInfo` entity contains information about a capability.
+
+[options="header",cols="1,6"]
+|=================================
+|Field Name           |Description
+|`id`                 |capability ID
+|`name`               |capability name
+|=================================
+
 [[entries-info]]
 === EntriesInfo
 The `EntriesInfo` entity contains information about the entries in a
 cache.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |==================================
 |Field Name ||Description
 |`mem`      |optional|Number of cache entries that are held in memory.
@@ -901,7 +901,7 @@
 The `HitRatioInfo` entity contains information about the hit ratio of a
 cache.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |==================================
 |Field Name ||Description
 |`mem`      ||
@@ -915,7 +915,7 @@
 === JvmSummaryInfo
 The `JvmSummaryInfo` entity contains information about the JVM.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |========================================
 |Field Name                 ||Description
 |`vm_vendor`                ||The vendor of the virtual machine.
@@ -936,7 +936,7 @@
 The `MemSummaryInfo` entity contains information about the current
 memory usage.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |============================
 |Field Name     ||Description
 |`total`        ||
@@ -963,7 +963,7 @@
 The `SummaryInfo` entity contains information about the current state
 of the server.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |============================
 |Field Name     ||Description
 |`task_summary` ||
@@ -985,7 +985,7 @@
 The `TaskInfo` entity contains information about a task in a background
 work queue.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |====================================
 |Field Name   ||Description
 |`id`         ||The ID of the task.
@@ -1006,7 +1006,7 @@
 The `TaskSummaryInfo` entity contains information about the current
 tasks.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |============================
 |Field Name     ||Description
 |`total`        |optional|
@@ -1024,7 +1024,7 @@
 The `ThreadSummaryInfo` entity contains information about the current
 threads.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",cols="1,6"]
 |===========================
 |Field Name     |Description
 |`cpus`         |
@@ -1048,7 +1048,7 @@
 The `TopMenuEntryInfo` entity contains information about a top menu
 entry.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",cols="1,6"]
 |=================================
 |Field Name           |Description
 |`name`               |Name of the top menu entry.
@@ -1060,7 +1060,7 @@
 The `TopMenuItemInfo` entity contains information about a menu item in
 a top menu entry.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |========================
 |Field Name ||Description
 |`url`      ||The URL of the menu item link.
diff --git a/Documentation/rest-api-documentation.txt b/Documentation/rest-api-documentation.txt
index 2629dcf..4c9db2b 100644
--- a/Documentation/rest-api-documentation.txt
+++ b/Documentation/rest-api-documentation.txt
@@ -130,7 +130,7 @@
 === DocResult
 The `DocResult` entity contains information about a document.
 
-[options="header",width="50%",cols="1,^2,4"]
+[options="header",cols="1,^2,4"]
 |=========================
 |Field Name  ||Description
 |`title`     ||The title of the document.
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index afdcffd..fbd3ba5 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -30,7 +30,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -136,7 +136,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -192,7 +192,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -222,7 +222,7 @@
 .Request
 ----
   PUT /groups/MyProject-Committers HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "description": "contains all committers for MyProject",
@@ -239,7 +239,7 @@
 ----
   HTTP/1.1 201 Created
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -280,7 +280,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -328,7 +328,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "MyProject-Committers"
@@ -347,7 +347,7 @@
 .Request
 ----
   PUT /groups/MyProject-Committers/name HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "name": "My-Project-Committers"
@@ -360,7 +360,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "My-Project-Committers"
@@ -386,7 +386,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "contains all committers for MyProject"
@@ -407,7 +407,7 @@
 .Request
 ----
   PUT /groups/9999c971bb4ab872aab759d8c49833ee6b9ff320/description HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "description": "The committers of MyProject."
@@ -420,7 +420,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "The committers of MyProject."
@@ -466,7 +466,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -488,7 +488,7 @@
 .Request
 ----
   PUT /groups/9999c971bb4ab872aab759d8c49833ee6b9ff320/options HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "visible_to_all": true
@@ -502,7 +502,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -530,7 +530,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -562,7 +562,7 @@
 .Request
 ----
   PUT /groups/9999c971bb4ab872aab759d8c49833ee6b9ff320/description HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "owner": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
@@ -576,7 +576,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -616,7 +616,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -655,7 +655,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -700,7 +700,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -731,7 +731,7 @@
 ----
   HTTP/1.1 201 Created
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -764,7 +764,7 @@
 .Request
 ----
   POST /groups/MyProject-Committers/members.add HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "members": [
@@ -786,7 +786,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -837,7 +837,7 @@
 .Request
 ----
   POST /groups/MyProject-Committers/members.delete HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "members": [
@@ -875,7 +875,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -912,7 +912,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -948,7 +948,7 @@
 ----
   HTTP/1.1 201 Created
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -986,7 +986,7 @@
 .Request
 ----
   POST /groups/MyProject-Committers/groups.add HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "groups": [
@@ -1007,7 +1007,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -1066,7 +1066,7 @@
 .Request
 ----
   POST /groups/MyProject-Committers/groups.delete HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "groups": [
@@ -1113,7 +1113,7 @@
 The `GroupInfo` entity contains information about a group. This can be
 a Gerrit internal group, or an external group that is known to Gerrit.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
 |`id`          ||The URL encoded UUID of the group.
@@ -1139,7 +1139,6 @@
 |===========================
 
 The type of a group can be deduced from the group's UUID:
-[width="50%"]
 |============
 |UUID matches "^[0-9a-f]\{40\}$"|Gerrit internal group
 |UUID starts with "global:"|Gerrit system group
@@ -1152,7 +1151,7 @@
 The 'GroupInput' entity contains information for the creation of
 a new internal group.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name      ||Description
 |`name`          |optional|The name of the group (not encoded). +
@@ -1167,26 +1166,11 @@
 If not set, the new group will be self-owned.
 |===========================
 
-[[groups-input]]
-=== GroupsInput
-The `GroupsInput` entity contains information about groups that should
-be included into a group or that should be deleted from a group.
-
-[options="header",width="50%",cols="1,^1,5"]
-|==========================
-|Field Name   ||Description
-|`_one_group` |optional|
-The link:#group-id[id] of one group that should be included or deleted.
-|`groups`     |optional|
-A list of link:#group-id[group ids] that identify the groups that
-should be included or deleted.
-|==========================
-
 [[group-options-info]]
 === GroupOptionsInfo
 Options of the group.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |=============================
 |Field Name      ||Description
 |`visible_to_all`|not set if `false`|
@@ -1197,20 +1181,35 @@
 === GroupOptionsInput
 New options for a group.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |=============================
 |Field Name      ||Description
 |`visible_to_all`|not set if `false`|
 Whether the group is visible to all registered users.
 |=============================
 
+[[groups-input]]
+=== GroupsInput
+The `GroupsInput` entity contains information about groups that should
+be included into a group or that should be deleted from a group.
+
+[options="header",cols="1,^1,5"]
+|==========================
+|Field Name   ||Description
+|`_one_group` |optional|
+The link:#group-id[id] of one group that should be included or deleted.
+|`groups`     |optional|
+A list of link:#group-id[group ids] that identify the groups that
+should be included or deleted.
+|==========================
+
 [[members-input]]
 MembersInput
 ~~~~~~~~~~~
 The `MembersInput` entity contains information about accounts that should
 be added as members to a group or that should be deleted from the group.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |==========================
 |Field Name   ||Description
 |`_one_member`|optional|
diff --git a/Documentation/rest-api-plugins.txt b/Documentation/rest-api-plugins.txt
index b8f7d36..dfe9f0e 100644
--- a/Documentation/rest-api-plugins.txt
+++ b/Documentation/rest-api-plugins.txt
@@ -40,7 +40,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -76,7 +76,7 @@
 .Request
 ----
   PUT /plugins/delete-project HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "url": "file:///gerrit/plugins/delete-project/delete-project-2.8.jar"
@@ -97,7 +97,7 @@
 ----
   HTTP/1.1 201 Created
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -128,7 +128,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -157,7 +157,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -192,7 +192,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -222,7 +222,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -247,7 +247,7 @@
 === PluginInfo
 The `PluginInfo` entity describes a plugin.
 
-[options="header",width="50%",cols="1,^2,4"]
+[options="header",cols="1,^2,4"]
 |=======================
 |Field Name ||Description
 |`id`       ||The ID of the plugin.
@@ -260,7 +260,7 @@
 === PluginInput
 The `PluginInput` entity describes a plugin that should be installed.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",cols="1,6"]
 |======================
 |Field Name|Description
 |`url`     |URL to the plugin jar.
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 2cc1e9a..a909ab4 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -30,7 +30,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -70,7 +70,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -103,7 +103,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -133,7 +133,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -160,7 +160,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -193,7 +193,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -221,7 +221,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -245,7 +245,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -273,7 +273,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -305,7 +305,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -338,7 +338,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -364,7 +364,7 @@
 .Request
 ----
   PUT /projects/MyProject HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "description": "This is a demo project.",
@@ -382,7 +382,7 @@
 ----
   HTTP/1.1 201 Created
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -410,7 +410,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "Copies to other servers using the Git protocol"
@@ -432,7 +432,7 @@
 .Request
 ----
   PUT /projects/plugins%2Freplication/description HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "description": "Plugin for Gerrit that handles the replication.",
@@ -446,7 +446,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "Plugin for Gerrit that handles the replication."
@@ -498,7 +498,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "All-Projects"
@@ -518,7 +518,7 @@
 .Request
 ----
   PUT /projects/plugins%2Freplication/parent HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "parent": "Public-Plugins",
@@ -532,7 +532,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "Public-Plugins"
@@ -555,7 +555,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "refs/heads/master"
@@ -575,7 +575,7 @@
 .Request
 ----
   PUT /projects/plugins%2Freplication/HEAD HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "ref": "refs/heads/stable"
@@ -588,7 +588,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   "refs/heads/stable"
@@ -614,7 +614,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -651,7 +651,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -671,6 +671,11 @@
       "configured_value": "INHERIT",
       "inherited_value": false
     },
+    "create_new_change_for_all_not_in_target": {
+      "value": false,
+      "configured_value": "INHERIT",
+      "inherited_value": false
+    },
     "require_change_id": {
       "value": false,
       "configured_value": "FALSE",
@@ -718,13 +723,14 @@
 .Request
 ----
   PUT /projects/myproject/config HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "description": "demo project",
     "use_contributor_agreements": "FALSE",
     "use_content_merge": "INHERIT",
     "use_signed_off_by": "INHERIT",
+    "create_new_change_for_all_not_in_target": "INHERIT",
     "require_change_id": "TRUE",
     "max_object_size_limit": "10m",
     "submit_type": "REBASE_IF_NECESSARY",
@@ -739,7 +745,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -758,6 +764,11 @@
       "configured_value": "INHERIT",
       "inherited_value": false
     },
+    "create_new_change_for_all_not_in_target": {
+      "value": true,
+      "configured_value": "INHERIT",
+      "inherited_value": false
+    },
     "require_change_id": {
       "value": true,
       "configured_value": "TRUE",
@@ -788,7 +799,7 @@
 .Request
 ----
   POST /projects/plugins%2Freplication/gc HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "show_progress": true
@@ -801,7 +812,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: text/plain;charset=UTF-8
+  Content-Type: text/plain; charset=UTF-8
 
   collecting garbage for "plugins/replication":
   Pack refs:              100% (21/21)
@@ -844,7 +855,7 @@
 .Request
 ----
   PUT /projects/plugins%2Freplication/ban HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "commits": [
@@ -861,7 +872,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -895,7 +906,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -919,6 +930,112 @@
   ]
 ----
 
+[[branch-options]]
+==== Branch Options
+
+Limit(n)::
+Limit the number of branches to be included in the results.
++
+.Request
+----
+  GET /projects/testproject/branches?n=1 HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "ref": "HEAD",
+      "revision": "master",
+      "can_delete": false
+    }
+  ]
+----
+
+Skip(s)::
+Skip the given number of branches from the beginning of the list.
++
+.Request
+----
+  GET /projects/testproject/branches?n=1&s=0 HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "ref": "HEAD",
+      "revision": "master",
+      "can_delete": false
+    }
+  ]
+----
+
+Substring(m)::
+Limit the results to those projects that match the specified substring.
++
+List all projects that match substring `test`:
++
+.Request
+----
+  GET /projects/testproject/branches?m=test HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "ref": "refs/heads/test1",
+      "revision": "9c9d08a438e55e52f33b608415e6dddd9b18550d",
+      "can_delete": true
+    }
+  ]
+----
+
+Regex(r)::
+Limit the results to those branches that match the specified regex.
+Boundary matchers '^' and '$' are implicit. For example: the regex 't*' will
+match any branches that start with 'test' and regex '*t' will match any
+branches that end with 'test'.
++
+List all branches that match regex `t.*1`:
++
+.Request
+----
+  GET /projects/testproject/branches?r=t.*1 HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "ref": "refs/heads/test1",
+      "revision": "9c9d08a438e55e52f33b608415e6dddd9b18550d",
+      "can_delete": true
+    }
+  ]
+----
+
 [[get-branch]]
 === Get Branch
 --
@@ -939,7 +1056,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -962,7 +1079,7 @@
 .Request
 ----
   PUT /projects/MyProject/branches/stable HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "revision": "76016386a0d8ecc7b6be212424978bb45959d668"
@@ -976,7 +1093,7 @@
 ----
   HTTP/1.1 201 Created
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -1004,6 +1121,38 @@
   HTTP/1.1 204 No Content
 ----
 
+[[delete-branches]]
+=== Delete Branches
+--
+'POST /projects/link:#project-name[\{project-name\}]/branches:delete'
+--
+
+Delete one or more branches.
+
+The branches to be deleted must be provided in the request body as a
+link:#delete-branches-input[DeleteBranchesInput] entity.
+
+.Request
+----
+  POST /projects/MyProject/branches:delete HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "branches": [
+      "stable-1.0",
+      "stable-2.0"
+    ]
+  }
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+If some branches could not be deleted, the response is "`409 Conflict`" and the
+error message is contained in the response body.
+
 [[get-content]]
 === Get Content
 --
@@ -1023,7 +1172,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: text/plain;charset=UTF-8
+  Content-Type: text/plain; charset=UTF-8
 
   Ly8gQ29weXJpZ2h0IChDKSAyMDEwIFRoZSBBbmRyb2lkIE9wZW4gU291cmNlIFByb2plY...
 ----
@@ -1051,7 +1200,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -1130,7 +1279,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -1170,7 +1319,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -1228,7 +1377,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -1239,6 +1388,102 @@
   }
 ----
 
+[[tag-endpoints]]
+== Tag Endpoints
+
+[[list-tags]]
+=== List Tags
+--
+'GET /projects/link:#project-name[\{project-name\}]/tags/'
+--
+
+List the tags of a project.
+
+As result a list of link:#tag-info[TagInfo] entries is returned.
+
+Only includes tags under the `refs/tags/` namespace.
+
+.Request
+----
+  GET /projects/work%2Fmy-project/tags/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "ref": "refs/tags/v1.0",
+      "revision": "49ce77fdcfd3398dc0dedbe016d1a425fd52d666",
+      "object": "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+      "message": "Annotated tag",
+      "tagger": {
+        "name": "David Pursehouse",
+        "email": "david.pursehouse@sonymobile.com",
+        "date": "2014-10-06 07:35:03.000000000",
+        "tz": 540
+      }
+    },
+    {
+      "ref": "refs/tags/v2.0",
+      "revision": "1624f5af8ae89148d1a3730df8c290413e3dcf30"
+    },
+    {
+      "ref": "refs/tags/v3.0",
+      "revision": "c628685b3c5a3614571ecb5c8fceb85db9112313",
+      "object": "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+      "message": "Signed tag\n-----BEGIN PGP SIGNATURE-----\nVersion: GnuPG v1.4.11 (GNU/Linux)\n\niQEcBAABAgAGBQJUMlqYAAoJEPI2qVPgglptp7MH/j+KFcittFbxfSnZjUl8n5IZ\nveZo7wE+syjD9sUbMH4EGv0WYeVjphNTyViBof+stGTNkB0VQzLWI8+uWmOeiJ4a\nzj0LsbDOxodOEMI5tifo02L7r4Lzj++EbqtKv8IUq2kzYoQ2xjhKfFiGjeYOn008\n9PGnhNblEHZgCHguGR6GsfN8bfA2XNl9B5Ysl5ybX1kAVs/TuLZR4oDMZ/pW2S75\nhuyNnSgcgq7vl2gLGefuPs9lxkg5Fj3GZr7XPZk4pt/x1oiH7yXxV4UzrUwg2CC1\nfHrBlNbQ4uJNY8TFcwof52Z0cagM5Qb/ZSLglHbqEDGA68HPqbwf5z2hQyU2/U4\u003d\n\u003dZtUX\n-----END PGP SIGNATURE-----",
+      "tagger": {
+        "name": "David Pursehouse",
+        "email": "david.pursehouse@sonymobile.com",
+        "date": "2014-10-06 09:02:16.000000000",
+        "tz": 540
+      }
+    }
+  ]
+----
+
+[[get-tag]]
+=== Get Tag
+--
+'GET /projects/link:#project-name[\{project-name\}]/tags/link:#tag-id[\{tag-id\}]'
+--
+
+Retrieves a tag of a project.
+
+.Request
+----
+  GET /projects/work%2Fmy-project/tags/v1.0 HTTP/1.0
+----
+
+As response a link:#tag-info[TagInfo] entity is returned that describes the tag.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "ref": "refs/tags/v1.0",
+    "revision": "49ce77fdcfd3398dc0dedbe016d1a425fd52d666",
+    "object": "1624f5af8ae89148d1a3730df8c290413e3dcf30",
+    "message": "Annotated tag",
+    "tagger": {
+      "name": "David Pursehouse",
+      "email": "david.pursehouse@sonymobile.com",
+      "date": "2014-10-06 07:35:03.000000000",
+      "tz": 540
+    }
+  }
+----
+
+
 [[commit-endpoints]]
 == Commit Endpoints
 
@@ -1264,7 +1509,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -1311,7 +1556,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: text/plain;charset=UTF-8
+  Content-Type: text/plain; charset=UTF-8
 
   Ly8gQ29weXJpZ2h0IChDKSAyMDEwIFRoZSBBbmRyb2lkIE9wZW4gU291cmNlIFByb2plY...
 ----
@@ -1341,7 +1586,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   [
@@ -1393,7 +1638,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -1429,7 +1674,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -1469,7 +1714,7 @@
 .Request
 ----
   PUT /projects/work%2Fmy-project/dashboards/default HTTP/1.0
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   {
     "id": "main:closed",
@@ -1484,7 +1729,7 @@
 ----
   HTTP/1.1 200 OK
   Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+  Content-Type: application/json; charset=UTF-8
 
   )]}'
   {
@@ -1547,6 +1792,10 @@
 === \{commit-id\}
 Commit ID.
 
+[[tag-id]]
+=== \{tag-id\}
+The name of a tag. The prefix `refs/tags/` can be omitted.
+
 [[dashboard-id]]
 === \{dashboard-id\}
 The ID of a dashboard in the format '<ref>:<path>'.
@@ -1558,29 +1807,18 @@
 === \{project-name\}
 The name of the project.
 
+If the name ends with `.git`, the suffix will be automatically removed.
+
 
 [[json-entities]]
 == JSON Entities
 
-[[branch-info]]
-=== BranchInfo
-The `BranchInfo` entity contains information about a branch.
-
-[options="header",width="50%",cols="1,^2,4"]
-|=========================
-|Field Name  ||Description
-|`ref`       ||The ref of the branch.
-|`revision`  ||The revision to which the branch points.
-|`can_delete`|`false` if not set|
-Whether the calling user can delete this branch.
-|=========================
-
 [[ban-input]]
 === BanInput
 The `BanInput` entity contains information for banning commits in a
 project.
 
-[options="header",width="50%",cols="1,^2,4"]
+[options="header",cols="1,^2,4"]
 |=======================
 |Field Name||Description
 |`commits` ||List of commits to be banned.
@@ -1591,7 +1829,7 @@
 === BanResultInfo
 The `BanResultInfo` entity describes the result of banning commits.
 
-[options="header",width="50%",cols="1,^2,4"]
+[options="header",cols="1,^2,4"]
 |=============================
 |Field Name      ||Description
 |`newly_banned`  |optional|List of newly banned commits.
@@ -1599,12 +1837,28 @@
 |`ignored`       |optional|List of object IDs that were ignored.
 |=============================
 
+[[branch-info]]
+=== BranchInfo
+The `BranchInfo` entity contains information about a branch.
+
+[options="header",cols="1,^2,4"]
+|=========================
+|Field Name  ||Description
+|`ref`       ||The ref of the branch.
+|`revision`  ||The revision to which the branch points.
+|`can_delete`|`false` if not set|
+Whether the calling user can delete this branch.
+|`web_links` |optional|
+Links to the branch in external sites as a list of
+link:rest-api-changes.html#web-link-info[WebLinkInfo] entries.
+|=========================
+
 [[branch-input]]
 === BranchInput
 The `BranchInput` entity contains information for the creation of
 a new branch.
 
-[options="header",width="50%",cols="1,^2,4"]
+[options="header",cols="1,^2,4"]
 |=======================
 |Field Name||Description
 |`ref`     |optional|
@@ -1621,26 +1875,29 @@
 The `ConfigInfo` entity contains information about the effective project
 configuration.
 
-[options="header",width="50%",cols="1,^2,4"]
-|=========================================
-|Field Name                  ||Description
-|`description`               |optional|
+[options="header",cols="1,^2,4"]
+|=======================================================
+|Field Name                                ||Description
+|`description`                             |optional|
 The description of the project.
-|`use_contributor_agreements`|optional|
+|`use_contributor_agreements`              |optional|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
 authors must complete a contributor agreement on the site before
 pushing any commits or changes to this project.
-|`use_content_merge`         |optional|
+|`use_content_merge`                       |optional|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
 Gerrit will try to perform a 3-way merge of text file content when a
 file has been modified by both the destination branch and the change
 being submitted. This option only takes effect if submit type is not
 FAST_FORWARD_ONLY.
-|`use_signed_off_by`         |optional|
+|`use_signed_off_by`                       |optional|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
 each change must contain a Signed-off-by line from either the author or
 the uploader in the commit message.
-|`require_change_id`         |optional|
+|`create_new_change_for_all_not_in_target` |optional|
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
+a new change is created for every commit not in target branch.
+|`require_change_id`                       |optional|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether a
 valid link:user-changeid.html[Change-Id] footer in any commit uploaded
 for review is required. This does not apply to commits pushed directly
@@ -1662,71 +1919,76 @@
 configuration, which has the same format as the
 link:config-gerrit.html#_a_id_commentlink_a_section_commentlink[
 commentlink section] of `gerrit.config`.
-|`theme`                     |optional|
+|`theme`                                   |optional|
 The theme that is configured for the project as a link:#theme-info[
 ThemeInfo] entity.
-|`plugin_config`             |optional|
+|`plugin_config`                           |optional|
 Plugin configuration as map which maps the plugin name to a map of
 parameter names to link:#config-parameter-info[ConfigParameterInfo]
 entities.
-|`actions`                   |optional|
+|`actions`                                 |optional|
 Actions the caller might be able to perform on this project. The
 information is a map of view names to
 link:rest-api-changes.html#action-info[ActionInfo] entities.
-|=========================================
+|=======================================================
 
 [[config-input]]
 === ConfigInput
 The `ConfigInput` entity describes a new project configuration.
 
-[options="header",width="50%",cols="1,^2,4"]
-|=========================================
-|Field Name                  ||Description
-|`description`               |optional|
+[options="header",cols="1,^2,4"]
+|======================================================
+|Field Name                               ||Description
+|`description`                            |optional|
 The new description of the project. +
 If not set, the description is removed.
-|`use_contributor_agreements`|optional|
+|`use_contributor_agreements`             |optional|
 Whether authors must complete a contributor agreement on the site
 before pushing any commits or changes to this project. +
 Can be `TRUE`, `FALSE` or `INHERIT`. +
 If not set, this setting is not updated.
-|`use_content_merge`         |optional|
+|`use_content_merge`                       |optional|
 Whether Gerrit will try to perform a 3-way merge of text file content
 when a file has been modified by both the destination branch and the
 change being submitted. This option only takes effect if submit type is
 not FAST_FORWARD_ONLY. +
 Can be `TRUE`, `FALSE` or `INHERIT`. +
 If not set, this setting is not updated.
-|`use_signed_off_by`         |optional|
+|`use_signed_off_by`                       |optional|
 Whether each change must contain a Signed-off-by line from either the
 author or the uploader in the commit message. +
 Can be `TRUE`, `FALSE` or `INHERIT`. +
 If not set, this setting is not updated.
-|`require_change_id`         |optional|
+|`create_new_change_for_all_not_in_target` |optional|
+Whether a new change will be created for every commit not in target
+branch. +
+Can be `TRUE`, `FALSE` or `INHERIT`. +
+If not set, this setting is not updated.
+|`require_change_id`                       |optional|
 Whether a valid link:user-changeid.html[Change-Id] footer in any commit
 uploaded for review is required. This does not apply to commits pushed
 directly to a branch or tag. +
 Can be `TRUE`, `FALSE` or `INHERIT`. +
 If not set, this setting is not updated.
-|`max_object_size_limit`     |optional|
+|`max_object_size_limit`                   |optional|
 The link:config-gerrit.html#receive.maxObjectSizeLimit[max object size
 limit] of this project as a link:#max-object-size-limit-info[
 MaxObjectSizeLimitInfo] entity. +
 If set to `0`, the max object size limit is removed. +
 If not set, this setting is not updated.
-|`submit_type`               |optional|
+|`submit_type`                             |optional|
 The default submit type of the project, can be `MERGE_IF_NECESSARY`,
 `FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `MERGE_ALWAYS` or
 `CHERRY_PICK`. +
 If not set, the submit type is not updated.
-|`state`                     |optional|
+|`state`                                   |optional|
 The state of the project, can be `ACTIVE`, `READ_ONLY` or `HIDDEN`. +
 Not set if the project state is `ACTIVE`. +
 If not set, the project state is not updated.
-|`plugin_config_values`      |optional|
+|`plugin_config_values`                    |optional|
 Plugin configuration values as map which maps the plugin name to a map
 of parameter names to values.
-|=========================================
+|======================================================
 
 [[config-parameter-info]]
 ConfigParameterInfo
@@ -1734,7 +1996,7 @@
 The `ConfigParameterInfo` entity describes a project configuration
 parameter.
 
-[options="header",width="50%",cols="1,^2,4"]
+[options="header",cols="1,^2,4"]
 |===============================
 |Field Name        ||Description
 |`display_name`    |optional|
@@ -1773,7 +2035,7 @@
 The `DashboardInfo` entity contains information about a project
 dashboard.
 
-[options="header",width="50%",cols="1,^2,4"]
+[options="header",cols="1,^2,4"]
 |===============================
 |Field Name        ||Description
 |`id`              ||
@@ -1810,7 +2072,7 @@
 The `DashboardInput` entity contains information to create/update a
 project dashboard.
 
-[options="header",width="50%",cols="1,^2,4"]
+[options="header",cols="1,^2,4"]
 |=============================
 |Field Name      ||Description
 |`id`            |optional|
@@ -1824,7 +2086,7 @@
 The `DashboardSectionInfo` entity contains information about a section
 in a dashboard.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",cols="1,6"]
 |===========================
 |Field Name    |Description
 |`name`        |The title of the section.
@@ -1832,12 +2094,24 @@
 Tokens such as `${project}` are not resolved.
 |===========================
 
+[[delete-branches-input]]
+=== DeleteBranchesInput
+The `DeleteBranchesInput` entity contains information about branches that should
+be deleted.
+
+[options="header",width="50%",cols="1,6"]
+|==========================
+|Field Name   |Description
+|`branches`   |A list of branch names that identify the branches that should be
+deleted.
+|==========================
+
 [[gc-input]]
 === GCInput
 The `GCInput` entity contains information to run the Git garbage
 collection.
 
-[options="header",width="50%",cols="1,^2,4"]
+[options="header",cols="1,^2,4"]
 |=============================
 |Field Name      ||Description
 |`show_progress` |`false` if not set|
@@ -1849,7 +2123,7 @@
 The `HeadInput` entity contains information for setting `HEAD` for a
 project.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",cols="1,6"]
 |============================
 |Field Name      |Description
 |`ref`           |
@@ -1861,7 +2135,7 @@
 === InheritedBooleanInfo
 A boolean value that can also be inherited.
 
-[options="header",width="50%",cols="1,^2,4"]
+[options="header",cols="1,^2,4"]
 |================================
 |Field Name         ||Description
 |`value`            ||
@@ -1879,7 +2153,7 @@
 link:config-gerrit.html#receive.maxObjectSizeLimit[max object size
 limit] of a project.
 
-[options="header",width="50%",cols="1,^2,4"]
+[options="header",cols="1,^2,4"]
 |===============================
 |Field Name        ||Description
 |`value`           |optional|
@@ -1900,7 +2174,7 @@
 The `ProjectDescriptionInput` entity contains information for setting a
 project description.
 
-[options="header",width="50%",cols="1,^2,4"]
+[options="header",cols="1,^2,4"]
 |=============================
 |Field Name      ||Description
 |`description`   |optional|The project description. +
@@ -1915,7 +2189,7 @@
 === ProjectInfo
 The `ProjectInfo` entity contains information about a project.
 
-[options="header",width="50%",cols="1,^2,4"]
+[options="header",cols="1,^2,4"]
 |===========================
 |Field Name    ||Description
 |`id`          ||The URL encoded project name.
@@ -1929,7 +2203,7 @@
 |`description` |optional|The description of the project.
 |`state`       |optional|`ACTIVE`, `READ_ONLY` or `HIDDEN`.
 |`branches`    |optional|Map of branch names to HEAD revisions.
-|'web_links'   |optional|
+|`web_links`   |optional|
 Links to the project in external sites as a list of
 link:rest-api-changes.html#web-link-info[WebLinkInfo] entries.
 |===========================
@@ -1939,12 +2213,13 @@
 The `ProjectInput` entity contains information for the creation of
 a new project.
 
-[options="header",width="50%",cols="1,^2,4"]
+[options="header",cols="1,^2,4"]
 |=========================================
 |Field Name                  ||Description
 |`name`                      |optional|
 The name of the project (not encoded). +
-If set, must match the project name in the URL.
+If set, must match the project name in the URL. +
+If name ends with `.git` the suffix will be automatically removed.
 |`parent`                    |optional|
 The name of the parent project. +
 If not set, the `All-Projects` project will be the parent project.
@@ -1970,17 +2245,20 @@
 If not set, the link:config-gerrit.html#repository.name.ownerGroup[
 groups that are configured as default owners] are set as project
 owners.
-|`use_contributor_agreements`|`INHERIT` if not set|
+|`use_contributor_agreements`                  |`INHERIT` if not set|
 Whether contributor agreements should be used for the project  (`TRUE`,
 `FALSE`, `INHERIT`).
-|`use_signed_off_by`         |`INHERIT` if not set|
+|`use_signed_off_by`                           |`INHERIT` if not set|
 Whether the usage of 'Signed-Off-By' footers is required for the
 project (`TRUE`, `FALSE`, `INHERIT`).
-|`use_content_merge`         |`INHERIT` if not set|
+|`create_new_change_for_all_not_in_target`     |`INHERIT` if not set|
+Whether a new change is created for every commit not in target branch
+for the project (`TRUE`, `FALSE`, `INHERIT`).
+|`use_content_merge`                           |`INHERIT` if not set|
 Whether content merge should be enabled for the project (`TRUE`,
 `FALSE`, `INHERIT`). +
 `FALSE`, if the `submit_type` is `FAST_FORWARD_ONLY`.
-|`require_change_id`         |`INHERIT` if not set|
+|`require_change_id`                           |`INHERIT` if not set|
 Whether the usage of Change-Ids is required for the project (`TRUE`,
 `FALSE`, `INHERIT`).
 |`max_object_size_limit`     |optional|
@@ -1996,7 +2274,7 @@
 The `ProjectParentInput` entity contains information for setting a
 project parent.
 
-[options="header",width="50%",cols="1,^2,4"]
+[options="header",cols="1,^2,4"]
 |=============================
 |Field Name      ||Description
 |`parent`        ||The name of the parent project.
@@ -2009,7 +2287,7 @@
 === ReflogEntryInfo
 The `ReflogEntryInfo` entity describes an entry in a reflog.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",cols="1,6"]
 |============================
 |Field Name      |Description
 |`old_id`        |The old commit ID.
@@ -2025,7 +2303,7 @@
 The `RepositoryStatisticsInfo` entity contains information about
 statistics of a Git repository.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",cols="1,6"]
 |======================================
 |Field Name                |Description
 |`number_of_loose_objects` |Number of loose objects.
@@ -2037,11 +2315,29 @@
 |`size_of_packed_objects`  |Size of packed objects in bytes.
 |======================================
 
+[[tag-info]]
+=== TagInfo
+The `TagInfo` entity contains information about a tag.
+
+[options="header",cols="1,^2,4"]
+|=========================
+|Field Name  ||Description
+|`ref`       ||The ref of the tag.
+|`revision`  ||For lightweight tags, the revision of the commit to which the tag
+points. For annotated tags, the revision of the tag object.
+|`object`|Only set for annotated tags.|The revision of the object to which the
+tag points.
+|`message`|Only set for annotated tags.|The tag message. For signed tags, includes
+the signature.
+|`tagger`|Only set for annotated tags.|The tagger as a
+link:rest-api-changes.html#git-person-info[GitPersonInfo] entity.
+|=========================
+
 [[theme-info]]
 === ThemeInfo
 The `ThemeInfo` entity describes a theme.
 
-[options="header",width="50%",cols="1,^2,4"]
+[options="header",cols="1,^2,4"]
 |=============================
 |Field Name      ||Description
 |`css`           |optional|
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
index b228dda..a87f5b6 100644
--- a/Documentation/rest-api.txt
+++ b/Documentation/rest-api.txt
@@ -101,58 +101,58 @@
 context of the Gerrit REST API.
 
 ==== 400 Bad Request
-`400 Bad Request` is returned if the request is not understood by the
+"`400 Bad Request`" is returned if the request is not understood by the
 server due to malformed syntax.
 
-E.g. `400 Bad Request` is returned if JSON input is expected but the
+E.g. "`400 Bad Request`" is returned if JSON input is expected but the
 'Content-Type' of the request is not 'application/json' or the request
 body doesn't contain valid JSON.
 
-`400 Bad Request` is also returned if required input fields are not set or
+"`400 Bad Request`" is also returned if required input fields are not set or
 if options are set which cannot be used together.
 
 ==== 403 Forbidden
-`403 Forbidden` is returned if the operation is not allowed because the
+"`403 Forbidden`" is returned if the operation is not allowed because the
 calling user does not have sufficient permissions.
 
 E.g. some REST endpoints require that the calling user has certain
 link:access-control.html#global_capabilities[global capabilities]
 assigned.
 
-`403 Forbidden` is also returned if `self` is used as account ID and the
+"`403 Forbidden`" is also returned if `self` is used as account ID and the
 REST call was done without authentication.
 
 ==== 404 Not Found
-`404 Not Found` is returned if the resource that is specified by the
+"`404 Not Found`" is returned if the resource that is specified by the
 URL is not found or is not visible to the calling user. A resource
 cannot be found if the URL contains a non-existing ID or view.
 
 ==== 405 Method Not Allowed
-`405 Method Not Allowed` is returned if the resource exists but doesn't
+"`405 Method Not Allowed`" is returned if the resource exists but doesn't
 support the operation.
 
 E.g. some of the `/groups/` endpoints are only supported for Gerrit
 internal groups; if they are invoked for an external group the response
-is `405 Method Not Allowed`.
+is "`405 Method Not Allowed`".
 
 ==== 409 Conflict
-`409 Conflict` is returned if the request cannot be completed because the
+"`409 Conflict`" is returned if the request cannot be completed because the
 current state of the resource doesn't allow the operation.
 
 E.g. if you try to submit a change that is abandoned, this fails with
-`409 Conflict` because the state of the change doesn't allow the submit
+"`409 Conflict`" because the state of the change doesn't allow the submit
 operation.
 
-`409 Conflict` is also returned if you try to create a resource but the
+"`409 Conflict`" is also returned if you try to create a resource but the
 name is already occupied by an existing resource.
 
 ==== 412 Precondition Failed
-`412 Precondition Failed` is returned if a precondition from the request
+"`412 Precondition Failed`" is returned if a precondition from the request
 header fields is not fulfilled, as described in the link:#preconditions[
 Preconditions] section.
 
 ==== 422 Unprocessable Entity
-`422 Unprocessable Entity` is returned if the ID of a resource that is
+"`422 Unprocessable Entity`" is returned if the ID of a resource that is
 specified in the request body cannot be resolved.
 
 GERRIT
diff --git a/Documentation/user-changeid.txt b/Documentation/user-changeid.txt
index fff14b4..44ca6e0 100644
--- a/Documentation/user-changeid.txt
+++ b/Documentation/user-changeid.txt
@@ -56,6 +56,8 @@
 
   $ curl -Lo .git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
 
+or:
+
   $ scp -p -P 29418 john.doe@review.example.com:hooks/commit-msg .git/hooks/
 
 Then ensure that the execute bit is set on the hook script:
@@ -68,8 +70,8 @@
 Change Upload
 --------------
 
-During upload by pushing to `refs/for/*`, `refs/drafts/*` or
-`refs/heads/*`, Gerrit will try to find an existing review the
+During upload by pushing to `+refs/for/*+`, `+refs/drafts/*+` or
+`+refs/heads/*+`, Gerrit will try to find an existing review the
 uploaded commit relates to. For an existing review to match, the
 following properties have to match:
 
diff --git a/Documentation/user-dashboards.txt b/Documentation/user-dashboards.txt
index 52916b9..f6db6cf 100644
--- a/Documentation/user-dashboards.txt
+++ b/Documentation/user-dashboards.txt
@@ -58,7 +58,7 @@
 == Project Dashboards
 
 It is possible to share custom dashboards at a project level. To do
-this define the dashboards in a `refs/meta/dashboards/*` branch of the
+this define the dashboards in a `+refs/meta/dashboards/*+` branch of the
 project. For each dashboard create a config file. The file path/name
 will be used as name (equivalent to a title in a custom dashboard) for
 the dashboard.
diff --git a/Documentation/user-inline-edit.txt b/Documentation/user-inline-edit.txt
new file mode 100644
index 0000000..5ad6b39
--- /dev/null
+++ b/Documentation/user-inline-edit.txt
@@ -0,0 +1,189 @@
+= Inline Edit
+
+This page explains the workflow for creating and amending changes in the
+browser.
+
+
+[[create-change]]
+== Creating a New Change
+
+A new change can be created directly in the browser, meaning it is not necessary
+to clone the whole repository to make trivial changes.
+
+The new change is created as a draft change, unless
+link:config-gerrit.html#change.allowDrafts[change.allowDrafts] is set to false,
+in which case the change is created as a normal new change.
+
+There are two different ways to create a new change:
+
+By clicking on the 'Create Change' button in the project screen:
+
+[[create-change-from-project-info-screen]]
+
+image::images/inline-edit-create-change-project-screen.png[width=800, link="images/inline-edit-create-change-project-screen.png"]
+
+The user can select the branch on which the new change should be created:
+
+image::images/inline-edit-create-change-project-screen-dialog.png[width=800, link="images/inline-edit-create-change-project-screen-dialog.png"]
+
+By clicking the 'Follow-Up' button on the change screen, to create a new change
+based on the selected change.
+
+[[create-change-from-change-screen]]
+
+image::images/inline-edit-create-follow-up-change.png[width=800, link="images/inline-edit-create-follow-up-change.png"]
+
+[[editing-change]]
+== Editing Changes
+
+To switch to edit mode, press the 'Edit' button at the top of the file list:
+
+[[switch-to-edit-mode]]
+image::images/inline-edit-enter-edit-mode-from-file-list.png[width=800, link="images/inline-edit-enter-edit-mode-from-file-list.png"]
+
+While in edit mode, it is possible to add new files to the change by clicking
+the 'Add...' button at the top of the file list.
+
+Files can be removed from the change, or restored, by clicking the icon to the
+left of the file name. Reverting a file in the change is also supported and is
+achieved in two steps: remove file from the change and restore the file in the
+change.
+
+To switch from edit mode back to review mode, click the 'Done Editing' button.
+
+image::images/inline-edit-file-list-in-edit-mode.png[width=800, link="images/inline-edit-file-list-in-edit-mode.png"]
+
+[[open-full-screen-editor]]
+While in edit mode, clicking on a file name in the file list opens a full
+screen editor for that file.
+
+To save edits, click the 'Save' button or press `CTRL-S`.  To return to the
+change screen, click the 'Close' button.
+
+image::images/inline-edit-full-screen-editor.png[width=800, link="images/inline-edit-full-screen-editor.png"]
+
+If there are unsaved edits when the 'Close' button is pressed, a dialog will
+pop up asking to confirm the edits.
+
+image::images/inline-edit-confirm-unsaved-edits.png[width=800, link="images/inline-edit-confirm-unsaved-edits.png"]
+
+To discard the unsaved edits and return to the change screen, click the 'OK'
+button. To continue editing, click 'Cancel'.
+
+[[switch-to-edit-mode-from-side-by-side]]
+
+While in review mode, it is possible to switch directly to edit mode and into an
+editor for a file under review by clicking on the edit icon in the patch set list
+on the side-by-side diff view.
+
+image::images/inline-edit-enter-edit-mode-from-diff.png[width=800, link="images/inline-edit-enter-edit-mode-from-diff.png"]
+
+[[reviewing-changes-made-in-change-edit]]
+== Reviewing Change Edits
+
+Change edits are reviewed in the same way as regular patch sets, using the
+side-by-side diff screen. Change edits are shown as 'edit' in the patch list
+on the diff screen:
+
+image::images/inline-edit-edit-in-diff-screen-patch-list.png[width=800, link="images/inline-edit-edit-in-diff-screen-patch-list.png"]
+
+and on the change screen:
+
+image::images/inline-edit-edit-in-patch-list.png[width=800, link="images/inline-edit-edit-in-patch-list.png"]
+
+Note that patch sets may exist that were created after the change edit was created.
+
+For example this sequence:
+
+`1 2 3 4 5 6 7 8 9 edit 10`
+
+means that the change edit was created on top of patch set number 9 and a regular
+patch set was uploaded later.
+
+[[change-edit-actions]]
+== Change Edit Actions
+
+Change edits can be deleted, published and rebased, and a patch set that
+represents a change edit can be downloaded like a regular patch set.
+
+[[delete-change-edit]]
+
+There is a special ref for a change edit. When the change edit is deleted, this
+ref is deleted as well. To delete a change edit click on the "Delete Edit"
+button.
+
+[[publish-change-edit]]
+
+When a change edit is based on the current patch set, it can be published. By
+publishing a change edit it is promoted to a regular patch set. The special ref
+that represents the change edit is deleted on publish. To publish a change edit
+click on the "Publish Edit" button. This button is only shown when the change
+edit is based on the current patch set. Otherwise the change edit must first be
+rebased onto the current patch set.
+
+[[rebase-change-edit]]
+
+Only change edits that are based on the current patch set can be published. If
+in the meantime a new patch set was uploaded, the change edit must be rebased on
+top of the current patch set before it can be published. Rebasing a change
+edit is done by clicking on the "Rebase Edit" button. If the rebase results in
+conflicts, these conflicts cannot be resolved in the browser. In this case the
+change edit must be downloaded (see below) and the conflicts must be resolved in
+the local environment. The commit that contains the conflict resolution can then
+be uploaded by setting `edit` as option on the target ref:
+
+----
+  $ git push host HEAD:refs/for/master%edit
+----
+
+[[download-change-edit-patch]]
+
+Like regular patch sets, change edits can be downloaded by the download
+commands (e.g. provided by the `download-commands` plugin). To download a
+change edit, select the desired scheme from the "Download" dropdown and copy the
+command to your terminal. Note: only change edit owners and users that were
+granted the link:access-control.html#capability_accessDatabase[accessDatabase]
+global capability are able to access change edit refs.
+
+[[not-implemented-features]]
+== Not Implemented Features
+
+* [PENDING CHANGE]
+The inline editor uses settings decided from the user's diff preferences, but those
+preferences are only modifiable from the side-by-side diff screen. It should be possible
+to open the preferences also from within the editor.
+
+* Allow to rename files that are already contained in the change (from the file table).
+The same rename file dialog can be used with preselected and disabled original file
+name.
+
+* Changed files in change edit should be marked as changed in file table in edit mode.
+One option is to use dirty icon or "*" char in front of changed files, another option
+is to use different hyperlink color for changed files (red?), to avoid adding yet another
+column to the file table
+
+* Add navigation icons in header area of edit screen. When dozen files need to be changed
+in context of change edit, this is not the best workflow to open one file in edit screen,
+change it, save it, close edit screen and select next file from the file table to edit.
+"<-" | "->" icons in header of edit screen could be used to navigate to the next file to
+change from the file table. This would behave like the navigation icons in side by side
+with thefollowing logic on click:
+
+** "save-when-file-was-changed" or
+** "close-when-no-changes"
+
+* Allow to activate different key maps, supported by CM: Emacs, Sublime, Vim. Load key
+maps dynamically. Currently default mode is used.
+
+* Implement conflict resolution during rebase of change edit using inline edit
+feature by creating new edit on top of current patch set with auto merge content
+
+* Similarly, reuse inline edit feature for conflict resolution during rebase of regular
+patch sets
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/user-notify.txt b/Documentation/user-notify.txt
index dd7c4c6..9dffa51 100644
--- a/Documentation/user-notify.txt
+++ b/Documentation/user-notify.txt
@@ -18,7 +18,9 @@
 
 link:user-search.html[Change search expressions] can be used to filter
 change notifications to specific subsets, for example `branch:master`
-to only see changes proposed for the master branch.
+to only see changes proposed for the master branch. If a filter would
+match at the `All-Projects` level as well as a specific project, the
+more specific project's notification settings are used.
 
 Notification mails for new changes and new patch sets are not sent to
 the change owner.
diff --git a/Documentation/user-review-ui.txt b/Documentation/user-review-ui.txt
index bb1aeef..dddbadc 100644
--- a/Documentation/user-review-ui.txt
+++ b/Documentation/user-review-ui.txt
@@ -22,66 +22,58 @@
 
 image::images/user-review-ui-change-screen-commit-message.png[width=800, link="images/user-review-ui-change-screen-commit-message.png"]
 
-The commit message can be edited directly in the Web UI by clicking on
-the `Edit Message` button in the change header. This opens a drop-down
-editor box in which the commit message can be edited. Saving
-modifications of the commit message automatically creates a new patch
-set for the change. The commit message may only be edited on the
-current patch set.
-
-image::images/user-review-ui-change-screen-edit-commit-message.png[width=800, link="images/user-review-ui-change-screen-edit-commit-message.png"]
-
+[[permalink]]
 The numeric change ID is a link to the change and clicking on it
 refreshes the change screen. By copying the link location you can get
 the permalink of the change.
 
 image::images/user-review-ui-change-screen-permalink.png[width=800, link="images/user-review-ui-change-screen-permalink.png"]
 
+[[change-status]]
 The change status shows the state of the change:
 
-- `Needs <label>`:
+- [[needs]]`Needs <label>`:
 +
 The change is in review and an approval on the shown label is still
 required to make the change submittable.
 
-- `Not <label>`:
+- [[not]]`Not <label>`:
 +
 The change is in review and a veto vote on the shown label is
 preventing the submit.
 
-[[not-current]]
-- `Not Current`:
+- [[not-current]]`Not Current`:
 +
 The currently viewed patch set is outdated.
 +
 Please note that some operations, like voting, are not available on
 outdated patch sets, but only on the current patch set.
 
-- `Ready to Submit`:
+- [[ready-to-submit]]`Ready to Submit`:
 +
 The change has all necessary approvals and may be submitted.
 
-- `Submitted, Merge Pending`:
+- [[submitted-merge-pending]]`Submitted, Merge Pending`:
 +
 The change was submitted and was added to the merge queue.
 +
 The change stays in the merge queue if it depends on a change that is
 still in review. In this case it will get automatically merged when all
-predecessor changes have been merged.
+dependency changes have been merged.
 +
 This status can also mean that the change depends on an abandoned
 change or on an outdated patch set of another change. In this case you
 may want to rebase the change.
 
-- `Merged`:
+- [[merged]]`Merged`:
 +
 The change was successfully merged into the destination branch.
 
-- `Abandoned`:
+- [[abandoned]]`Abandoned`:
 +
 The change was abandoned.
 
-- `Draft`:
+- [[draft]]`Draft`:
 +
 The change is a draft that is only visible to the change owner, the
 reviewers that were explicitly added to the change, and users who have
@@ -119,14 +111,14 @@
 
 image::images/user-review-ui-change-screen-change-info.png[width=800, link="images/user-review-ui-change-screen-change-info.png"]
 
-- Change Owner:
+- [[change-owner]]Change Owner:
 +
 The owner of the change is displayed as a link to a list of the owner's
 changes that have the same status as the currently viewed change.
 +
 image::images/user-review-ui-change-screen-change-info-owner.png[width=800, link="images/user-review-ui-change-screen-change-info-owner.png"]
 
-- Reviewers:
+- [[reviewers]]Reviewers:
 +
 The reviewers of the change are displayed as chip tokens.
 +
@@ -137,6 +129,7 @@
 into the pop-up text field activates auto completion of user and group
 names.
 +
+[[remove-reviewer]]
 Reviewers can be removed from the change by clicking on the `x` icon
 in the reviewer's chip token. Removing a reviewer also removes the
 current votes of the reviewer. The removal of votes is recorded as a
@@ -153,7 +146,7 @@
 +
 image::images/user-review-ui-change-screen-change-info-reviewers.png[width=800, link="images/user-review-ui-change-screen-change-info-reviewers.png"]
 
-- Project / Branch / Topic:
+- [[project-branch-topic]]Project / Branch / Topic:
 +
 The name of the project for which the change was done is displayed as a
 link to the link:user-dashboards.html#project-default-dashboard[default
@@ -175,7 +168,7 @@
 +
 image::images/user-review-ui-change-screen-change-info-project-branch-topic.png[width=800, link="images/user-review-ui-change-screen-change-info-project-branch-topic.png"]
 
-- Submit Strategy:
+- [[submit-strategy]]Submit Strategy:
 +
 The link:project-setup.html#submit_type[submit strategy] that will be
 used to submit the change. The submit strategy is only displayed for
@@ -188,16 +181,16 @@
 +
 image::images/user-review-ui-change-screen-change-info-cannot-merge.png[width=800, link="images/user-review-ui-change-screen-change-info-cannot-merge.png"]
 
-- Time of Last Update:
+- [[update-time]]Time of Last Update:
 +
 image::images/user-review-ui-change-screen-change-info-last-update.png[width=800, link="images/user-review-ui-change-screen-change-info-last-update.png"]
 
-- Actions:
+- [[actions]]Actions:
 +
 Depending on the change state and the permissions of the user, different
 actions are available on the change:
 
-** `Submit`:
+** [[submit]]`Submit`:
 +
 Submits the change and adds it to the merge queue. If possible the
 change is merged into the destination branch.
@@ -210,7 +203,7 @@
 allows to do the conflict resolution for a change series in a single
 merge commit and submit the changes in reverse order.
 
-** `Abandon`:
+** [[abandon]]`Abandon`:
 +
 Abandons the change.
 +
@@ -221,7 +214,7 @@
 When a change is abandoned, a panel appears that allows one to type a
 comment message to explain why the change is being abandoned.
 
-** `Restore`:
+** [[restore]]`Restore`:
 +
 Restores the change.
 +
@@ -233,7 +226,7 @@
 When a change is restored, a panel appears that allows one to type a
 comment message to explain why the change is being restored.
 
-** `Rebase`:
+** [[rebase]]`Rebase`:
 +
 Rebases the change. The rebase is always done with content merge
 enabled. If the rebase is successful a new patch set with the rebased
@@ -246,11 +239,15 @@
 If the change depends on another open change, it is rebased onto the
 current patch set of that other change.
 +
-The `Rebase` button is only available if the change can be rebased and
+It is possible to change parent revision of a change. The new parent
+revision can be another change towards the same target branch, or
+the tip of the target branch.
++
+The `Rebase` button is only available if
 the link:access-control.html#category_rebase[Rebase] access right is
 assigned. Rebasing merge commits is not supported.
 
-** `Cherry-Pick`:
+** [[cherry-pick]]`Cherry-Pick`:
 +
 Allows to cherry-pick the change to another branch. The destination
 branch can be selected from a dialog. Cherry-picking a change creates a
@@ -264,7 +261,7 @@
 Users can only cherry-pick changes to branches for which they are
 allowed to upload changes for review.
 
-** `Publish`:
+** [[publish]]`Publish`:
 +
 Publishes the currently viewed draft patch set. If this is the first
 patch set of a change that is published, the change will be published
@@ -275,7 +272,7 @@
 link:access-control.html#category_publish_drafts[Publish Drafts] access
 right assigned.
 
-** `Delete Change` / `Delete Revision`:
+** [[delete]]`Delete Change` / `Delete Revision`:
 +
 Deletes the draft change / the currently viewed draft patch set.
 +
@@ -284,12 +281,12 @@
 link:access-control.html#category_delete_drafts[Delete Drafts] access
 right assigned.
 
-** Further actions may be available if plugins are installed.
+** [[plugin-actions]]Further actions may be available if plugins are installed.
 
 +
 image::images/user-review-ui-change-screen-change-info-actions.png[width=800, link="images/user-review-ui-change-screen-change-info-actions.png"]
 
-- Labels & Votes:
+- [[labels]]Labels & Votes:
 +
 Approving votes are colored green; veto votes are colored red.
 +
@@ -303,10 +300,12 @@
 
 image::images/user-review-ui-change-screen-file-list.png[width=800, link="images/user-review-ui-change-screen-file-list.png"]
 
+[[change-screen-mark-reviewed]]
 The checkboxes in front of the file names allow files to be marked as reviewed.
 
 image::images/user-review-ui-change-screen-file-list-mark-as-reviewed.png[width=800, link="images/user-review-ui-change-screen-file-list-mark-as-reviewed.png"]
 
+[[modification-type]]
 The type of a file modification is indicated by the character in front
 of the file name:
 
@@ -332,15 +331,18 @@
 
 image::images/user-review-ui-change-screen-file-list-modification-type.png[width=800, link="images/user-review-ui-change-screen-file-list-modification-type.png"]
 
+[[rename-or-copy]]
 If a file is renamed or copied, the name of the original file is
 displayed in gray below the file name.
 
 image::images/user-review-ui-change-screen-file-list-rename.png[width=800, link="images/user-review-ui-change-screen-file-list-rename.png"]
 
+[[repeating-path-segments]]
 Repeating path segments are grayed out.
 
 image::images/user-review-ui-change-screen-file-list-repeating-paths.png[width=800, link="images/user-review-ui-change-screen-file-list-repeating-paths.png"]
 
+[[inline-comments-column]]
 Inline comments on a file are shown in the `Comments` column.
 
 Draft comments, i.e. comments that have been written by the current
@@ -351,28 +353,36 @@
 
 image::images/user-review-ui-change-screen-file-list-comments.png[width=800, link="images/user-review-ui-change-screen-file-list-comments.png"]
 
-The size of the modifications in the files can be seen in the `Size`
-column. The footer row shows the total size of the change.
-
-For files, the `Size` column shows the sum of inserted and deleted
-lines as one number. For the total size, inserted and deleted lines are
-shown separately. In addition, the number of insertions and deletions
-is shown as a bar. The size of the bar indicates the amount of changed
-lines, and its coloring in green and red shows the proportion of
-insertions to deletions.
+[[size]]
+The size of the modifications in the files can be seen in the `Size` column. The
+footer row shows the total size of the change.
 
 The size information is useful to easily spot the files that contain
 the most modifications; these files are likely to be the most relevant
 files for this change. The total change size gives an estimate of how
 long a review of this change may take.
 
+When the "Show Change Sizes As Colored Bars" user preference is enabled, the
+`Size` column shows the sum of inserted and deleted lines as one number.  In
+addition, the change size is shown as a bar. The size of the bar indicates the
+amount of changed lines, and its coloring shows the proportion of insertions
+(green) to deletions (red).
+
+When the "Show Change Sizes As Colored Bars" user preference is disabled, the
+colored bar is not shown.  For added and renamed files, the `Size` column
+shows the number of inserted and deleted lines. For new files, the column only
+shows the total number of lines in the new file. No size is shown for binary
+files and deleted files.
+
 image::images/user-review-ui-change-screen-file-list-size.png[width=800, link="images/user-review-ui-change-screen-file-list-size.png"]
 
+[[diff-against]]
 In the header of the file list, the `Diff Against` selection can be
 changed. This selection allows one to choose if the currently viewed
 patch set should be compared against its base or against another patch
 set of this change. The file list is updated accordingly.
 
+[[open-all]]
 The file list header also provides an `Open All` button that opens the
 diff views for all files in the file list.
 
@@ -393,6 +403,7 @@
 
 image::images/user-review-ui-change-screen-patch-sets.png[width=800, link="images/user-review-ui-change-screen-patch-sets.png"]
 
+[[patch-set-drop-down]]
 The patch set drop-down list shows the list of patch sets and allows to
 switch between them. The patch sets are sorted in descending order so
 that the current patch set is always on top.
@@ -479,8 +490,7 @@
 
 The following tabs may be displayed:
 
-[[related-changes-tab]]
-- `Related Changes`:
+- [[related-changes-tab]]`Related Changes`:
 +
 This tab page shows changes on which the current change depends
 (ancestors) and open changes that depend on the current change
@@ -502,7 +512,7 @@
 on outdated patch sets, or commits that are not associated to changes
 under review:
 +
-** Orange Dot:
+** [[outdated]]Orange Dot:
 +
 The selected patch set of the change is outdated; it is not the current
 patch set of the change.
@@ -518,7 +528,7 @@
 patch set. It may be that the descendant was rebased in the meantime
 and with the new patch set this dependency was removed.
 
-** Green Tilde:
+** [[indirect-descendant]]Green Tilde:
 +
 The selected patch set of the change is an indirect descendant of the
 currently viewed patch set; it has a dependency to another patch set of
@@ -527,7 +537,7 @@
 note that following the link to an indirect descendant change may
 result in a completely different related changes listing.
 
-** Black Dot:
+** [[closed-ancestor]]Black Dot:
 +
 Indicates a merged ancestor, e.g. the commit was directly pushed into
 the repository bypassing code review, or the ancestor change was
@@ -539,7 +549,7 @@
 +
 image::images/user-review-ui-change-screen-related-changes-indicators.png[width=800, link="images/user-review-ui-change-screen-related-changes-indicators.png"]
 
-- `Conflicts With`:
+- [[conflicts-with]]`Conflicts With`:
 +
 This tab page shows changes that conflict with the current change.
 Non-mergeable changes are filtered out; only conflicting changes that
@@ -551,14 +561,14 @@
 +
 image::images/user-review-ui-change-screen-conflicts-with.png[width=800, link="images/user-review-ui-change-screen-conflicts-with.png"]
 
-- `Same Topic`:
+- [[same-topic]]`Same Topic`:
 +
 This tab page shows changes that have the same topic as the current
 change. Only open changes are included in the list.
 +
 image::images/user-review-ui-change-screen-same-topic.png[width=800, link="images/user-review-ui-change-screen-same-topic.png"]
 
-- `Cherry-Picks`:
+- [[cherry-picks]]`Cherry-Picks`:
 +
 This tab page shows changes with the same link:user-changeid.html[
 Change-Id] for the current project.
@@ -586,14 +596,14 @@
 A text box allows to type a summary comment for the currently viewed
 patch set.
 
+Note that you can set the text and tooltip of the button in
+link:config-gerrit.html#change.replyLabel[gerrit.config].
+
+[[vote]]
 If the current patch set is viewed, radio buttons are displayed for
 each label on which the user is allowed to vote. Voting on non-current
 patch sets is not possible.
 
-Typing "LGTM" (acronym for 'Looks Good To Me') in the summary comment
-text box automatically selects the highest possible score for the
-'Code-Review' label.
-
 The inline draft comments that will be published are displayed in a
 separate section so that they can be reviewed before publishing. There
 are links to navigate to the inline comments which can be used if a
@@ -603,6 +613,7 @@
 
 image::images/user-review-ui-change-screen-replying.png[width=800, link="images/user-review-ui-change-screen-replying.png"]
 
+[[quick-approve]]
 If a user can approve a label that is still required, a quick approve
 button appears in the change header that allows to add this missing
 approval by a single click. The quick approve button only appears if
@@ -635,6 +646,7 @@
 
 image::images/user-review-ui-change-screen-history.png[width=800, link="images/user-review-ui-change-screen-history.png"]
 
+[[reply-to-message]]
 It is possible to directly reply to a change message by clicking on the
 reply icon in the right upper corner of a change message. This opens
 the reply popup panel and prefills the text box with the quoted comment.
@@ -645,11 +657,13 @@
 
 image::images/user-review-ui-change-screen-reply-to-comment.png[width=800, link="images/user-review-ui-change-screen-reply-to-comment.png"]
 
+[[inline-comments-in-history]]
 Inline comments are directly displayed in the change history and there
 are links to navigate to the inline comments.
 
 image::images/user-review-ui-change-screen-inline-comments.png[width=800, link="images/user-review-ui-change-screen-inline-comments.png"]
 
+[[expand-all]]
 The `Expand All` button expands all messages; the `Collapse All` button
 collapses all messages.
 
@@ -675,20 +689,6 @@
 
 image::images/user-review-ui-change-screen-plugin-extensions.png[width=800, link="images/user-review-ui-change-screen-plugin-extensions.png"]
 
-[[old-change-screen]]
-=== Old Change Screen
-
-In addition to the normal change screen, this Gerrit version still
-includes the old change screen that was used in earlier Gerrit
-versions. Users that want to continue using the old change screen can
-configure it in their preferences under
-`Settings` > `Preferences` > `Change View`:
-
-image::images/user-review-ui-change-view-preference.png[width=800, link="images/user-review-ui-change-view-preference.png"]
-
-[WARNING]
-The old change screen will be removed in a later version of Gerrit.
-
 [[side-by-side]]
 == Side-by-Side Diff Screen
 
@@ -700,6 +700,7 @@
 
 image::images/user-review-ui-side-by-side-diff-screen.png[width=800, link="images/user-review-ui-side-by-side-diff-screen.png"]
 
+[[side-by-side-header]]
 In the screen header the project name and the name of the viewed patch
 file are shown.
 
@@ -709,6 +710,7 @@
 
 image::images/user-review-ui-side-by-side-diff-screen-project-and-file.png[width=800, link="images/user-review-ui-side-by-side-diff-screen-project-and-file.png"]
 
+[[side-by-side-mark-reviewed]]
 The checkbox in front of the project name and the file name allows the
 patch to be marked as reviewed. The link:#mark-reviewed[Mark Reviewed]
 diff preference allows to control whether the files should be
@@ -716,6 +718,7 @@
 
 image::images/user-review-ui-side-by-side-diff-screen-reviewed.png[width=800, link="images/user-review-ui-side-by-side-diff-screen-reviewed.png"]
 
+[[scrollbar]]
 The scrollbar shows patch diffs and inline comments as annotations.
 This provides a good overview of the lines in the patch that are
 relevant for reviewing. By clicking on an annotation one can quickly
@@ -723,6 +726,7 @@
 
 image::images/user-review-ui-side-by-side-diff-screen-scrollbar.png[width=800, link="images/user-review-ui-side-by-side-diff-screen-scrollbar.png"]
 
+[[gaps]]
 A gap between lines in the file content that is caused by aligning the
 left and right side or by displaying inline comments is shown as a
 vertical red bar in the line number column. This prevents a gap from
@@ -750,21 +754,25 @@
 
 image::images/user-review-ui-side-by-side-diff-screen-patch-sets.png[width=800, link="images/user-review-ui-side-by-side-diff-screen-patch-sets.png"]
 
+[[download-file]]
 The download icon next to the patch set list allows to download the
 patch. Unless the mime type of the file is configured as safe, the
 download file is a zip archive that contains the patch file.
 
+[[no-differences]]
 If the compared patches are identical, this is highlighted by a red
 `No Differences` label in the screen header.
 
 image::images/user-review-ui-side-by-side-diff-screen-no-differences.png[width=800, link="images/user-review-ui-side-by-side-diff-screen-no-differences.png"]
 
+[[side-by-side-rename]]
 If a file was renamed, the old and new file paths are shown in the
 header together with a similarity index that shows how much of the file
 content is unmodified.
 
 image::images/user-review-ui-side-by-side-diff-screen-rename.png[width=800, link="images/user-review-ui-side-by-side-diff-screen-rename.png"]
 
+[[navigation]]
 For navigating between the patches in a patch set there are navigation
 buttons on the right side of the screen header. The left arrow button
 navigates to the previous patch; the right arrow button navigates to
@@ -786,6 +794,7 @@
 Code blocks with comments may overlap. This means it is possible to
 attach several comments to the same code.
 
+[[line-links]]
 The lines of the patch file are linkable. To link to a certain line in
 the patch file, '@<line-number>' must be appended to the patch link,
 e.g. `http://host:8080/#/c/56857/2/Documentation/user-review-ui.txt@665`.
@@ -798,6 +807,7 @@
 
 image::images/user-review-ui-side-by-side-diff-screen-inline-comments.png[width=800, link="images/user-review-ui-side-by-side-diff-screen-inline-comments.png"]
 
+[[comment]]
 In the header of the comment box, the name of the comment author and
 the timestamp of the comment are shown. If avatars are configured on
 the server, the avatar image of the comment author is displayed in the
@@ -806,6 +816,7 @@
 
 image::images/user-review-ui-side-by-side-diff-screen-comment-box.png[width=800, link="images/user-review-ui-side-by-side-diff-screen-comment-box.png"]
 
+[[reply-inline-comment]]
 Clicking on the `Reply` button opens an editor to type the reply.
 
 Quoting is supported, but only by manually copying & pasting the old
@@ -824,6 +835,7 @@
 
 image::images/user-review-ui-side-by-side-diff-screen-comment-reply.png[width=800, link="images/user-review-ui-side-by-side-diff-screen-comment-reply.png"]
 
+[[draft-inline-comment]]
 Draft comments are marked by the text "Draft" in the header in the
 place of the comment author.
 
@@ -832,6 +844,7 @@
 
 image::images/user-review-ui-side-by-side-diff-screen-comment-edit.png[width=800, link="images/user-review-ui-side-by-side-diff-screen-comment-edit.png"]
 
+[[done]]
 Clicking on the `Done` button is a quick way to reply with "Done" to a
 comment. This is used to mark a comment as addressed by a follow-up
 patch set.
@@ -946,7 +959,7 @@
 
 The following diff preferences can be configured:
 
-- `Theme`:
+- [[theme]]`Theme`:
 +
 Controls the theme that is used to render the file content.
 +
@@ -954,7 +967,7 @@
 +
 image::images/user-review-ui-side-by-side-diff-screen-dark-theme.png[width=800, link="images/user-review-ui-side-by-side-diff-screen-dark-theme.png"]
 
-- `Ignore Whitespace`:
+- [[ignore-whitespace]]`Ignore Whitespace`:
 +
 Controls whether differences in whitespace should be ignored or not.
 +
@@ -974,11 +987,11 @@
 +
 All differences in whitespace are ignored.
 
-- `Tab Width`:
+- [[tab-width]]`Tab Width`:
 +
 Controls how many spaces should be displayed for a tab.
 
-- `Columns`:
+- [[columns]]`Columns`:
 +
 Sets the preferred line length. At this position a vertical dashed line
 is displayed so that one can easily detect lines the exceed the
@@ -986,7 +999,7 @@
 +
 image::images/user-review-ui-side-by-side-diff-screen-column.png[width=800, link="images/user-review-ui-side-by-side-diff-screen-column.png"]
 
-- `Lines Of Context`:
+- [[lines-of-context]]`Lines Of Context`:
 +
 The number of context lines that should be displayed before and after
 any diff. If the `entire file` checkbox is selected, the full file is
@@ -1003,13 +1016,13 @@
 +
 image::images/user-review-ui-side-by-side-diff-screen-expand-skipped-lines.png[width=800, link="images/user-review-ui-side-by-side-diff-screen-expand-skipped-lines.png"]
 
-- `Intraline Difference`:
+- [[intraline-difference]]`Intraline Difference`:
 +
 Controls whether intraline differences should be highlighted.
 +
 image::images/user-review-ui-side-by-side-diff-screen-intraline-difference.png[width=800, link="images/user-review-ui-side-by-side-diff-screen-intraline-difference.png"]
 
-- `Syntax Highlighting`:
+- [[syntax-highlighting]]`Syntax Highlighting`:
 +
 Controls whether syntax highlighting should be enabled.
 +
@@ -1019,45 +1032,48 @@
 +
 image::images/user-review-ui-side-by-side-diff-screen-syntax-coloring.png[width=800, link="images/user-review-ui-side-by-side-diff-screen-syntax-coloring.png"]
 
-- `Whitespace Errors`:
+- [[whitespace-errors]]`Whitespace Errors`:
 +
 Controls whether whitespace errors are highlighted.
 
-- `Show Tabs`:
+- [[show-tabs]]`Show Tabs`:
 +
 Controls whether tabs are highlighted.
 
-- `Line Numbers`:
+- [[line-numbers]]`Line Numbers`:
 +
 Controls whether line numbers are shown.
 
-- `Empty Pane`:
+- [[empty-pane]]`Empty Pane`:
 +
 Controls whether empty panes are shown or not. The Left pane is empty when a
 file was added; the right pane is empty when a file was deleted.
 
-- `Left Side`:
+- [[left-side]]`Left Side`:
 +
 Controls whether the left side is shown. This preference is not
 persistent and is ignored by the `Save` button. Every time a
 patch diff is opened, this preference is reset to `Show`.
 
-- `Top Menu`:
+- [[top-menu]]`Top Menu`:
 +
 Controls whether the top menu is shown.
 
-[[mark-reviewed]]
-- `Mark Reviewed`:
+- [[auto-hide-diff-table-header]]`Auto Hide Diff Table Header`:
++
+Controls whether the diff table header should be automatically hidden
+when scrolling down more than half of a page.
+
+- [[mark-reviewed]]`Mark Reviewed`:
 +
 Controls whether the files of the patch set should be automatically
 marked as reviewed when they are viewed.
 
-[[expand-all-comments]]
-- `Expand All Comments`:
+- [[expand-all-comments]]`Expand All Comments`:
 +
 Controls whether all comments should be automatically expanded.
 
-- `Render`:
+- [[render]]`Render`:
 +
 Controls how patch files that exceed the screen size are rendered.
 +
@@ -1109,18 +1125,13 @@
   e.g. by selecting a code block and clicking on the popup comment
   icon.
 
+[[limitations]]
 Limitations of the new review UI:
 
 - The new side-by-side diff screen cannot render images.
 
-- Unified diff view is missing:
-+
-By setting `Diff View (New Change Screen)` in the user preferences to
-`Unified Diff` the new change screen can be configured to open the file
-diffs in the old unified diff view.
-
-Users preferring the old review UI can link:#old-change-screen[
-configure the change view] in their preferences.
+- The new side-by-side diff screen isn't able to highlight line
+  endings.
 
 GERRIT
 ------
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index eaf0f6e..83accb8 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -14,7 +14,7 @@
 |All > Open           | status:open '(or is:open)'
 |All > Merged         | status:merged
 |All > Abandoned      | status:abandoned
-|My > Drafts          | is:draft
+|My > Drafts          | owner:self is:draft
 |My > Watched Changes | status:open is:watched
 |My > Starred Changes | is:starred
 |My > Draft Comments  | has:draft
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 5c54259..de9a2fb 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -156,8 +156,8 @@
 ====
 
 [[review_labels]]
-Review labels can be applied to the change by using the -l option
-in the reference:
+Review labels can be applied to the change by using the `label` (or `l`)
+option in the reference:
 
 ====
   git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental%l=Verified+1
@@ -169,6 +169,23 @@
 The value is optional.  If not specified, it defaults to +1 (if
 the label range allows it).
 
+[[change_edit]]
+A change edit can be pushed by specifying the `edit` (or `e`) option on
+the reference:
+
+====
+  git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master%edit
+====
+
+There is at most one change edit per user and change. In order to push
+a change edit the change must already exist.
+
+[NOTE]
+When a change edit already exists for a change then pushing with
+`%edit` replaces the existing change edit. This option is useful to
+rebase a change edit on the newest patch set when the rebase of the
+change edit in the web UI fails due to conflicts.
+
 If you are frequently uploading changes to the same Gerrit server,
 consider adding an SSH host block in `~/.ssh/config` to remember
 your username, hostname and port number.  This permits the use of
@@ -186,8 +203,8 @@
 ====
 
 Specific reviewers can be requested and/or additional 'carbon
-copies' of the notification message may be sent by including these
-as options in the reference
+copies' of the notification message may be sent by including the
+`reviewer` (or `r`) and `cc` options in the reference:
 
 ====
   git push tr:kernel/common HEAD:refs/for/experimental%r=a@a.com,cc=b@o.com
@@ -235,7 +252,7 @@
 [[manual_replacement_mapping]]
 ==== Manual Replacement Mapping
 
-.Deprecation Warning
+.Note
 ****
 The remainder of this section describes a manual method of replacing
 changes by matching each commit name to an existing change number.
@@ -302,9 +319,9 @@
 
 Gerrit restricts direct pushes that bypass review to:
 
-* `refs/heads/*`: any branch can be updated, created, deleted,
+* `+refs/heads/*+`: any branch can be updated, created, deleted,
 or rewritten by the pusher.
-* `refs/tags/*`: annotated tag objects pointing to any other type
+* `+refs/tags/*+`: annotated tag objects pointing to any other type
 of Git object can be created.
 
 To push branches, the proper access rights must be configured first.
@@ -428,8 +445,8 @@
 own process space, Gerrit maintains complete control over how the
 repository is updated, and what responses are sent to the `git push`
 client invoked by the end-user, or by `repo upload`.  This allows
-Gerrit to provide magical refs, such as `refs/for/*` for new
-change submission and `refs/changes/*` for change replacement.
+Gerrit to provide magical refs, such as `+refs/for/*+` for new
+change submission and `+refs/changes/*+` for change replacement.
 When a push request is received to create a ref in one of these
 namespaces Gerrit performs its own logic to update the database,
 and then lies to the client about the result of the operation.
diff --git a/ReleaseNotes/ReleaseNotes-2.0.18.txt b/ReleaseNotes/ReleaseNotes-2.0.18.txt
index 303690c..18c5abe 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.18.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.18.txt
@@ -22,19 +22,19 @@
 
 * Caches
 +
-The groups that a user is a member of is no longer stored in the 
+The groups that a user is a member of is no longer stored in the
 `groups` cache; it is now part of the `accounts` cache.  If you
 use a cron script to update the `account_groups` database table
-based upon an external data source (such as LDAP), you will need  
+based upon an external data source (such as LDAP), you will need
 to adjust your script to flush the `accounts` cache.
-The `diff` cache is no longer written to disk by default.  
+The `diff` cache is no longer written to disk by default.
 To enable the disk store again, administrators must explicitly
 set `cache.directory` in the gerrit.config file prior to starting
 Gerrit.
 
 * SSH Usernames
 +
-SSH usernames are no longer automatically assigned to the 
+SSH usernames are no longer automatically assigned to the
 local part of the user's email address.  With 2.0.18, usernames
 must also be unique within the database.  These changes were
 implemented to resolve a minor potential security issue with
diff --git a/ReleaseNotes/ReleaseNotes-2.0.19.txt b/ReleaseNotes/ReleaseNotes-2.0.19.txt
index b6a731b4..6c33e6b 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.19.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.19.txt
@@ -48,14 +48,14 @@
 ------------
 * New ssh create-project command
 +
-Thanks to Ulrik Sjölin we now have `gerrit create-project` 
+Thanks to Ulrik Sjölin we now have `gerrit create-project`
 available over SSH, to construct a new repository and database
 record for a project.  Documentation has also been updated to
 reflect that the command is now available.
 
 * Be more liberal in accepting Signed-off-by lines
 +
-The "Require Signed-off-by line" feature in a project is now 
+The "Require Signed-off-by line" feature in a project is now
 more liberal.  Gerrit now requires that the commit be signed off
 by either the author or the committer.  This was relaxed because
 kernel developers often cherry-pick in patches signed off by
@@ -64,7 +64,7 @@
 
 * Allow cache.name.diskLimit = 0 to disable on disk cache
 +
-Setting cache.name.diskLimit to 0 will disable the disk for 
+Setting cache.name.diskLimit to 0 will disable the disk for
 that cache, even though cache.directory was set.  This allows
 sites to set cache.diff.diskLimit to 0 to avoid caching the diff
 records on disk, but still allow caching web_sessions to disk,
@@ -83,7 +83,7 @@
 
 * Add native LDAP support to Gerrit
 +
-Gerrit now has native LDAP support.  Setting auth.type to 
+Gerrit now has native LDAP support.  Setting auth.type to
 HTTP_LDAP and then configuring the handful of ldap properties
 in gerrit.config will allow Gerrit to load group membership
 directly from the organization's LDAP server.  This replaces
@@ -105,7 +105,7 @@
 username, cannot be modified by the end-user.  This allows the
 Gerrit site administrator to require that users conform to the
 standard information published by the organization's directory
-service.  Updates in LDAP are automatically reflected in Gerrit 
+service.  Updates in LDAP are automatically reflected in Gerrit
 the next time the user signs-in.
 
 * Remembers anchor during HTTP logins
@@ -113,7 +113,7 @@
 When using an HTTP SSO product, clicking on a Gerrit link received
 out-of-band (e.g. by email or IM) often required clicking the
 link twice.  On the first click Gerrit redirect you to the
-organization's single-sign-on authentication system, which upon 
+organization's single-sign-on authentication system, which upon
 success redirected to your dashboard.  The actual target of the
 link was often lost, so a second click was required.
 With .19 and later, if the administrator changes the frontend web
@@ -129,9 +129,9 @@
 ----
    During a request for an arbitrary URL, such as '/#change,42',
    Gerrit realizes the user is not logged in.  Instead of sending an
-   immediate redirect for authentication, Gerrit sends JavaScript   
+   immediate redirect for authentication, Gerrit sends JavaScript
    to save the target token (the part after the '#' in the URL)
-   by redirecting the user to '/login/change,42'.  This enters    
+   by redirecting the user to '/login/change,42'.  This enters
    the secured area, and performs the authentication.  When the
    authenticated user returns to '/login/change,42' Gerrit sends
    a redirect back to the original URL, '/#change,42'.
@@ -139,7 +139,7 @@
 
 * Create check_schema_version during schema creation
 +
-Schema upgrades for PostgreSQL now validate that the current 
+Schema upgrades for PostgreSQL now validate that the current
 schema version matches the expected schema version at the start
 of the upgrade script.  If the schema does not match, the script
 aborts, although it will spew many errors.
@@ -148,7 +148,7 @@
 +
 Uploading commits to a project now requires that the new commits
 share a common ancestry with the existing commits of that project.
-This catches and prevents problems caused by a user making a typo 
+This catches and prevents problems caused by a user making a typo
 in the project name, and inadvertently selecting the wrong project.
 
 * Change-Id tags in commit messages to associate commits
@@ -156,12 +156,12 @@
 Gerrit now looks for 'Change-Id: I....' in the footer area of a
 commit message and uses this to identify a change record within
 the project.
-If the listed Change-Id has not been seen before, a new change 
+If the listed Change-Id has not been seen before, a new change
 record is created.  If the Change-Id is already known, Gerrit
 updates the change with the new commit.  This simplifies updating
 multiple changes at once, such as might happen when rebasing an
 entire series of commits that are still being reviewed.
-A commit-msg hook can be installed to automatically generate 
+A commit-msg hook can be installed to automatically generate
 these Change-Id lines during initial commit:
 {{{
 scp -P 29418 review.example.com:hooks/commit-msg .git/hooks/
@@ -175,7 +175,7 @@
 ---------
 * Fix yet another ArrayIndexOutOfBounds during side-by-s...
 +
-We found yet another bug with the side-by-side view failing 
+We found yet another bug with the side-by-side view failing
 under certain conditions.  I think this is the last bug.
 
 * Apply URL decoding to parameter of /cat/
@@ -183,12 +183,12 @@
 +
 Images weren't displaying correctly, even though
 mimetype.image/png.safe was true in gerrit.config.
-Turned out to be a problem with the parameter decoding of the 
+Turned out to be a problem with the parameter decoding of the
 /cat/ servlet, as well as the link being generated wrong.
 
 * Fix high memory usage seen in `gerrit show-caches`
 +
-In Gerrit 2.0.18 JGit had a bug where the repository wasn't being 
+In Gerrit 2.0.18 JGit had a bug where the repository wasn't being
 reused in memory.  This meant that we were constantly reloading
 the repository data in from disk, so the server was always maxed
 out at core.packedGitLimit and core.packedGitOpenFiles, as no
@@ -196,19 +196,19 @@
 
 * Fix display of timeouts in `gerrit show-caches`
 +
-Timeouts were not always shown correctly, sometimes 12 hours 
+Timeouts were not always shown correctly, sometimes 12 hours
 was showing up as 2.5 days, which is completely wrong.  Fixed.
 
 * GERRIT-261  Fix reply button when comment is on the last line
 +
-The "Reply" button didn't work if the comment was on the last 
+The "Reply" button didn't work if the comment was on the last
 line of the file, the browser caught an array index out of
 bounds exception as we walked off the end of the table looking
 for where to insert the new editor box.
 
 * GERRIT-83   Make sign-out really invalidate the user's session
 +
-The sign-out link now does more than delete the cookie from the 
+The sign-out link now does more than delete the cookie from the
 user's browser, it also removes the token from the server side.
 By removing it from the server, we prevent replay attacks where
 an attacker has observed the user's cookie and then later tries
@@ -219,16 +219,16 @@
 
 * Evict account record after changing SSH username
 +
-Changing the SSH username on the web immediately affected the 
+Changing the SSH username on the web immediately affected the
 SSH daemon, but the web still showed the old username.  This
 was due to the change operation not flushing the cache that
 the web code was displaying from.  Fixed.
 
 * Really don't allow commits to replace in wrong project
 +
-It was possible for users to upload replacement commits to the 
+It was possible for users to upload replacement commits to the
 wrong project, e.g. uploading a replacement commit to project
-B while picking a change number from project A.  Fixed. 
+B while picking a change number from project A.  Fixed.
 
 =Fixes in 2.0.19.1=
 -------------------
@@ -241,7 +241,7 @@
 * Ignore harmless "Pipe closed" in scp command
 +
 scp command on the server side threw exceptions when a client aborted the
-data transfer.  We typically don't care to log such cases. 
+data transfer.  We typically don't care to log such cases.
 
 * Refactor user lookup during permission checking
 * GERRIT-264  Fix membership in Registered Users group
diff --git a/ReleaseNotes/ReleaseNotes-2.0.20.txt b/ReleaseNotes/ReleaseNotes-2.0.20.txt
index 11958e8..d46f74d 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.20.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.20.txt
@@ -24,18 +24,18 @@
 
 * Support changing Google Account identity strings
 +
-For various reasons, including but not being limited to server 
+For various reasons, including but not being limited to server
 host name changes, the Google Accounts OpenID provider service
 may change the identity string it returns to users.  By setting
 auth.allowGoogleAccountUpgrade = true in the configuration file
-administrators may permit automatically updating an existing 
+administrators may permit automatically updating an existing
 account with a new identity by matching on the email address.
 
 Bug Fixes
 ---------
 * GERRIT-262  Disallow creating comments on line 0
 +
-Users were able to create comments in dead regions of a file. 
+Users were able to create comments in dead regions of a file.
 That is, if a region was deleted, and thus the left hand side
 showed red deletion of lines, and the right hand side showed a
 grey background of nothing, users were able to place a comment on
@@ -45,8 +45,8 @@
 comments become hidden and could not be seen, but showed up in
 the "X comments" counter seen on the Patch History or in the
 listing of files in a patch set.
-The UI and RPC layer was fixed to prevent comments on line 0,    
-but existing comments need to be manually moved to a real line. 
+The UI and RPC layer was fixed to prevent comments on line 0,
+but existing comments need to be manually moved to a real line.
 See above for the suggested SQL UPDATE command.
 
 * Make ID column same font size as rest of table
diff --git a/ReleaseNotes/ReleaseNotes-2.0.22.txt b/ReleaseNotes/ReleaseNotes-2.0.22.txt
index ae6d7dd..3a35421 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.22.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.22.txt
@@ -12,7 +12,7 @@
 
 * Restriction on SSH Username
 +
-There is a new restriction placed on the SSH Username field 
+There is a new restriction placed on the SSH Username field
 within an account.  Users who are using invalid names should
 be asked to change their name to something more suitable.
 Administrators can identify these users with the following query:
@@ -20,12 +20,12 @@
      -- PostgreSQL
      SELECT account_id,preferred_email,ssh_user_name
      FROM accounts
-     WHERE NOT (ssh_user_name ~ '^[a-zA-Z][a-zA-Z0-9._-]*[a-zA-Z0-9]$');
+     WHERE NOT (ssh_user_name ~ '[a-zA-Z][a-zA-Z0-9._-]*[a-zA-Z0-9]$');
 
      -- MySQL
      SELECT account_id,preferred_email,ssh_user_name
      FROM accounts
-     WHERE NOT (ssh_user_name REGEXP '^[a-zA-Z][a-zA-Z0-9._-]*[a-zA-Z0-9]$');
+     WHERE NOT (ssh_user_name REGEXP '[a-zA-Z][a-zA-Z0-9._-]*[a-zA-Z0-9]$');
 ----
    Administrators can force these users to select a new name by
    setting ssh_user_name to NULL; the user will not be able to
diff --git a/ReleaseNotes/ReleaseNotes-2.0.4.txt b/ReleaseNotes/ReleaseNotes-2.0.4.txt
index 6da317d..4869233 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.4.txt
@@ -47,4 +47,4 @@
 * Make sure the WorkQueue terminates when running command ...
 * Move all contact information out of database to encrypte...
 * Peg the versions of JGit and MINA SSHD to something known
-* gerrit 2.0.4 
\ No newline at end of file
+* gerrit 2.0.4
\ No newline at end of file
diff --git a/ReleaseNotes/ReleaseNotes-2.1.6.txt b/ReleaseNotes/ReleaseNotes-2.1.6.txt
index d1c6335..ce65f1a 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.6.txt
@@ -55,7 +55,7 @@
 resulting query output.
 
 Notifications
-~~~~~~~~~~~~ 
+~~~~~~~~~~~~
 * Customize email notification templates
 +
 Email notifications are now driven by the Velocity template engine,
diff --git a/ReleaseNotes/ReleaseNotes-2.10.txt b/ReleaseNotes/ReleaseNotes-2.10.txt
index 336f02f3..18d0f34 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.txt
@@ -37,6 +37,12 @@
 
 *WARNING:* The `auth.allowGoogleAccountUpgrade` setting is no longer supported.
 
+*WARNING:* When upgrading from version 2.8.4 or older with a site that uses
+Bouncy Castle Crypto, new versions of the libraries will be downloaded. The old
+libraries should be manually removed from site's `lib` folder to prevent the
+startup failure described in
+link:https://code.google.com/p/gerrit/issues/detail?id=3084[Issue 3084].
+
 
 Release Highlights
 ------------------
diff --git a/ReleaseNotes/ReleaseNotes-2.11.txt b/ReleaseNotes/ReleaseNotes-2.11.txt
new file mode 100644
index 0000000..def1ad5
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.11.txt
@@ -0,0 +1,896 @@
+Release notes for Gerrit 2.11
+=============================
+
+
+Gerrit 2.11 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.11.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.11.war]
+
+Gerrit 2.11 includes the bug fixes done with
+link:ReleaseNotes-2.10.1.html[Gerrit 2.10.1] and
+link:ReleaseNotes-2.10.2.html[Gerrit 2.10.2].
+These bug fixes are *not* listed in these release notes.
+
+
+Important Notes
+---------------
+
+
+*WARNING:* This release contains schema changes.  To upgrade:
+----
+  java -jar gerrit.war init -d site_path
+----
+
+Gerrit 2.11 requires a secondary index, which can be created offline
+by running the `reindex` program:
+
+----
+  java -jar gerrit.war reindex -d site_path
+----
+
+If the site that is upgraded already has a secondary index, the
+secondary index can be upgraded online. This is important for large
+sites since running the `reindex` program can take a long time and
+contributes significantly to the downtime that is required for the
+upgrade.
+
+Gerrit 2.11 supports online reindexing only from the index version `11`
+which is the index version of Gerrit 2.10. This means if you come from
+an older release it makes sense to first upgrade to 2.10 and then do
+the upgrade to 2.11 so that you can profit from online reindexing.
+
+In case you are upgrading from 2.10 it is *important* to check *before*
+the upgrade to 2.11 that the index version of your Gerrit 2.10 site is
+`11`. You can check the index version in
+`$site_path/index/gerrit_index.config`. Your Gerrit 2.10 site may run
+with an older index version (e.g. if online reindexing to index version
+`11` is still running or if online reindexing to version `11` has
+failed). In this case you first need to successfully migrate your index
+version of your Gerrit 2.10 site to `11` and only then start with the
+2.11 upgrade. If you start the 2.11 upgrade when the schema version of
+your Gerrit 2.10 site is older than `11`, online reindexing is no longer
+possible and you need to reindex offline by using the `reindex` program.
+
+*WARNING:* Upgrading to 2.11.x requires the server be first upgraded to 2.8 (or
+2.9) and then to 2.11.x. If you are upgrading from 2.8.x or later, you may ignore
+this warning and upgrade directly to 2.11.x.
+
+*WARNING:* When upgrading from version 2.8.4 or older with a site that uses
+Bouncy Castle Crypto, new versions of the libraries will be downloaded. The old
+libraries should be manually removed from site's `lib` folder to prevent the
+startup failure described in
+link:https://code.google.com/p/gerrit/issues/detail?id=3084[Issue 3084].
+
+*WARNING:* The 'Generate HTTP Password' capability has been
+link:#remove-generate-http-password-capability[removed].
+
+*WARNING:* Google will
+link:https://developers.google.com/+/api/auth-migration[shut down their OpenID
+service on 20th April 2015]. Administrators of sites whose users are registered
+with Google OpenID accounts should encourage the users to
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-sso.html#_multiple_identities[
+add an alternative identity to their account] before this date. Users who do
+not add an alternative identity before this date will need to create a new
+account and ask the site administrator to
+link:https://code.google.com/p/gerrit/wiki/SqlMergeUserAccounts[merge it].
+
+*WARNING:* The
+link:https://gerrit-review.googlesource.com/Documentation/2.10/rest-api-changes.html#message[
+Edit Commit Message] REST API endpoint is removed
+
+*WARNING:* The deprecated '/query' URL is removed and will now return `Not Found`.
+
+Release Highlights
+------------------
+
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=505[Issue 505]:
+Changes can be created and edited directly in the browser. See the
+link:#inline-editing[Inline editing] section for more details.
+
+* Many improvements in the new change screen.
+
+* The old change screen is removed.
+
+
+New Features
+------------
+
+
+Web UI
+~~~~~~
+
+[[inline-editing]]
+Inline Editing
+^^^^^^^^^^^^^^
+
+Refer to the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/user-inline-edit.html[
+inline editing user guide] for detailed instructions.
+
+* New changes can be created directly in the browser via a 'Create Change'
+button on the project info screen.
+
+* New follow-up changes can be created via a 'Follow-Up' button on the change
+screen.
+
+* File content can be edited in a full screen CodeMirror editor with support for
+themes and syntax highlighting.
+
+* The CodeMirror screen can be configured in the same way as the side-by-side
+diff screen.
+
+* The file table in the change screen supports seamless navigation to the
+CodeMirror editor.
+
+* Edit mode can be started from the side-by-side diff screen with seamless
+navigation to the CodeMirror editor.
+
+* The commit message must now be changed in the context of a change edit. The
+'Edit Message' button is removed from the change screen.
+
+* Files can be added, deleted, restored and modified directly in browser.
+
+Change Screen
+^^^^^^^^^^^^^
+
+* Remove the 'Edit Message' button from the change screen.
++
+The commit message is now edited using the inline edit feature.
+
+* Add support for changing parent revision with the 'Rebase' button.
++
+Using the 'Rebase' button it is now possible to rebase a change onto a
+different change (on the same destination branch), rather than only onto the
+head of the destination branch or the latest patch set of the predecessor change.
+
+* Show the parent commit's subject as a tooltip.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=2541[Issue 2541],
+link:http://code.google.com/p/gerrit/issues/detail?id=2974[Issue 2974]:
+Allow the 'Reply' button's
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#change.replyLabel[
+label and tooltip] to be configured.
+
+* Improve file sorting for C and C++ files.
++
+Header files are now listed before implementation files.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3148[Issue 3148]:
+Allow display of colored size bars to be enabled or disabled per user.
++
+The 'Show Change Sizes As Colored Bars In Changes Table' setting is renamed to
+'Show Change Sizes As Colored Bars' and is now used to also control how the
+change size is shown per file in the file table.
++
+When enabled (which is the default), the change size per file is shown as a sum
+of lines added/removed, and also representated by a colored bar showing the
+proportion of added/removed lines.
++
+When disabled, the colored bar is not shown and the change size per file is shown
+in the same way as it used to be in the old change screen.
+
+* Show changes across all projects and branches in the `Same Topic` tab.
+
+
+Side-By-Side Diff
+^^^^^^^^^^^^^^^^^
+
+* New button to switch between side-by-side diff and unified diff.
+
+* New preference setting to toggle auto-hiding of the diff table header.
++
+The setting determines whether or not the diff table header with the patch set
+selection should be automatically hidden when scrolling down more than half of
+a page.
+
+* Highlight search results on scrollbar.
++
+Search results in vim mode are highlighted in the scrollbar with gold
+colored annotations.
+
+* Set line length to 72 characters for commit messages.
+
+* Add syntax highlighting for several new modes:
+
+** link:https://code.google.com/p/gerrit/issues/detail?id=2848[Issue 2848]: CSharp
+** Dart
+** Dockerfile
+** GLSL shader
+** Go
+** Objective C
+** RELAX NG
+** link:http://code.google.com/p/gerrit/issues/detail?id=2779[Issue 2779]: reStructured text
+** Soy
+
+
+Projects Screen
+^^^^^^^^^^^^^^^
+
+* Add pagination and filtering on the branch list page.
+
+* Add an 'Edit Config' button on the project info page.
++
+The button creates a new change on the `refs/meta/config` branch and opens the
+`project.config` file in the inline editor.
++
+This allows project owners to easily edit the `project.config` file from the
+browser, which is useful since it is possible that not all configuration options
+are available in the UI.
+
+REST
+~~~~
+
+Accounts
+^^^^^^^^
+
+* Add new link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-accounts.html#suggest-account[
+Suggest Account endpoint].
+
+Changes
+^^^^^^^
+
+* The link:https://gerrit-review.googlesource.com/Documentation/2.10/rest-api-changes.html#message[
+Edit Commit Message] endpoint is removed in favor of the new
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#put-change-edit-message[
+Change commit message in Change Edit] and
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#publish-edit[
+Publish Change Edit] endpoints.
+
+* Add new
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#check-change[
+Check Change endpoint].
++
+In the past, Gerrit bugs, lack of transactions, and unreliable NoSQL backends
+have at various times produced a bewildering variety of corrupt states.
++
+This endpoint can be used to detect, explain, and repair some of these possible
+states of a change.
+
+* Add new
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#get-revision-actions[
+Get Revision Actions endpoint].
+
+* Add
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#change-actions[
+`CHANGE_ACTIONS`] option on the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#get-change-detail[
+Get Change Detail] endpoint.
+
+
+Change Edits
+^^^^^^^^^^^^
+
+Several new endpoints are added to support the inline edit feature.
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#get-edit-detail[
+Get Edit Detail].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#put-edit-file[
+Change file content in Change Edit].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#post-edit[
+Restore file content in Change Edit].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#put-change-edit-message[
+Change commit message in Change Edit].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#delete-edit-file[
+Delete file in Change Edit].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#get-edit-file[
+Retrieve file content from Change Edit].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#get-edit-message[
+Retrieve commit message from Change Edit or current patch set of the change].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#publish-edit[
+Publish Change Edit].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#rebase-edit[
+Rebase Change Edit].
+
+* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#delete-edit[
+Delete Change Edit].
+
+
+Projects
+^^^^^^^^
+
+* Add new
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-projects.html#delete-branches[
+Delete Branches] endpoint.
+
+* Add filtering and pagination options on the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-projects.html#list-branches[
+List Branches] endpoint.
+
+* Add new
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-projects.html#list-tags[
+List Tags] endpoint.
+
+* Add new
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-projects.html#get-tag[
+Get Tag] endpoint.
+
+
+Configuration
+~~~~~~~~~~~~~
+
+* Add support for
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#auth.httpExternalIdHeader[
+HTTP external ID header].
++
+This can be used when authenticating with a federated identity token from
+an external system, e.g. GitHub's OAuth 2.0 authentication.
+
+* Add
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-labels.html#label_copyAllScoresIfNoChange[
+`copyAllScoresIfNoChange`] setting for labels.
++
+Allows to copy scores forward when a new patch set is uploaded that has the same
+parent tree, code delta, and commit message as the previous patch set.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2786[Issue 2786]:
+Allow non-administrators to modify user accounts.
++
+A new global capability,
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/access-control.html#capability_modifyAccount[
+'Modify Account'], which allows the granted group members to modify user account
+settings via the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/cmd-set-account.html[
+`set-account` SSH command].
++
+Modification of users' SSH keys is still restricted to administrators.
+
+* Add support for
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#ldap.useConnectionPooling[
+LDAP connection pooling].
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=699[Issue 699]: Allow to
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#receive.maxBatchChanges[
+limit max number of changes pushed in a batch].
++
+Can be overridden by members of groups that are granted the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/access-control.html#capability_batchChangesLimit[
+Batch Changes Limit] capability.
+
+* Allow to
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#gerrit.disableReverseDnsLookup[
+disable reverse DNS lookup].
++
+This option can be set to improve push time from hosts without a reverse DNS
+entry.
+
+* Allow to
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#cache.projects.loadOnStartup[
+load the project cache at server startup].
+
+* Allow members of groups granted the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/access-control.html#capability_accessDatabase[
+AccessDatabase capability] to view metadata refs.
+
+* Allow to
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#http.addUserAsRequestAttribute[
+add the user to the http request attributes].
+
+* Allow to
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#suggest.fullTextSearch[
+enable full text search in memory for review suggestions].
++
+The maximum number of reviewers evaluated can be limited with
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#suggest.fullTextSearchMaxMatches[
+suggest.fullTextSearchMaxMatches].
+
+* Allow to provide an alternative
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#gerrit.secureStoreClass[
+secure store implementation].
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1195[Issue 1195]:
+Allow projects to be configured to create a new change for every uploaded commit that is not in the target branch.
+
+* Allow to configure
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#container.daemonOpt[
+options to pass to the daemon].
+
+* Allow to configure
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#sshd.rekeyBytesLimit[
+SSHD rekey parameters].
+
+Daemon
+~~~~~~
+
+* Allow to enable the http daemon when running in slave mode.
++
+The `--enable-httpd` option can be used in conjunction with the `--slave` option
+to allow clients to fetch from the slave over the http protocol.
++
+HTTP Authentication may also be used when running in slave mode.
+
+* Include the submitter's name in the change message when a change is submitted.
+
+* Add a message to changes created via cherry pick.
++
+When a change is cherry-picked to another branch using the cherry-pick action,
+the message 'Patch Set <number>: Cherry Picked from branch <name>.' is added as
+a change message on the created change.
+
+* Don't send 'new patch set' notification emails for trivial rebases.
+
+
+SSH
+~~~
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2797[Issue 2797]:
+Add support for ECDSA based public key authentication.
+
+* Add new commands
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/cmd-logging-ls-level.html[
+`logging ls-level`] and
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/cmd-logging-set-level.html[
+`logging set-level`] to show and set the logging level at runtime.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=602[Issue 602]:
+Add `--json` option to the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/cmd-review.html[
+`review` SSH command].
++
+Review input can be given to the `review` command in JSON format corresponding
+to the REST API's
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#review-input[
+ReviewInput] entity.
+
+*  link:https://code.google.com/p/gerrit/issues/detail?id=2824[Issue 2824]:
+Add `--rebase` option to the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/cmd-review.html[
+`review` SSH command].
+
+* Add `--clear-http-password` option to the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/cmd-set-account.html[
+`set-account` SSH command].
+
+* Add `--preferred-email` option to the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/cmd-set-account.html[
+`set-account` SSH command].
+
+Email
+~~~~~
+
+* Add `$change.originalSubject` field for email templates.
++
+GMail threads messages together by subject and ignores the list headers included
+by Gerrit.
++
+Site administrators that run servers whose end-user base is mostly on GMail can
+modify the site's `ChangeSubject.vm` template to use `$change.originalSubject` to
+improve threading for GMail inboxes.
++
+The `originalSubject` field is automatically taken from the existing subject
+field during first use.
+
+
+Plugins
+~~~~~~~
+
+General
+^^^^^^^
+
+* Plugins can listen to account group membership changes.
++
+The audit log service allows to register listeners to group member added and
+group member deleted events. A default listener logs these events to the database
+as before, but additional listeners may now be registered for these events using
+the `GroupMemberAuditListener` interface.
+
+* Plugins can validate ref operations.
++
+Plugins implementing the `RefOperationValidationListener` interface can
+perform additional validation checks against ref creation/deletion operations
+before they are applied to the git repository.
+
+* Plugins can provide project-aware top menu extensions
++
+Plugins can provide sub-menu items within the 'Projects' context. The
+'${projectName}' placeholder is replaced by the project name.
+
+* Auto register static/init.js as JavaScript plugin.
++
+When a plugin does not expose Guice Modules explicitly, auto discover and
+register static/init.js as WebUi extension if found by the plugin content
+scanner.
+
+* Plugins can validate outgoing emails.
++
+Plugins implementing `OutgoingEmailValidationListener` interface can filter
+and modify outgoing emails before they are sent.
+
+* Plugins that provide initialization steps may now use functionality
+from InitUtil in core Gerrit.
+
+* Plugins can post change reviews with historic timestamps.
++
+This allows, for example, to write a plugin that can import a project including
+review information from another Gerrit server.
+
+* New extensions in the Java Plugin API:
+
+** Set/Put topic.
+** Get mergeable status.
+** link:https://code.google.com/p/gerrit/issues/detail?id=461[Issue 461]:
+Get current user.
+** Get file content.
+** Get file diff.
+** Get comments and drafts.
+** Get change edit.
+
+Replication
+^^^^^^^^^^^
+
+* Projects can be specified with wildcard in the `start` command.
+
+
+Bug Fixes
+---------
+
+Daemon
+~~~~~~
+
+* Change 'Merge topic' to 'Merge changes from topic'.
++
+When multiple changes from a topic are submitted resulting in a merge commit,
+the title of the merge commit is now 'Merge changes from topic' instead of
+'Merge topic'.
+
+* Fix visibility checks for `refs/meta/config`.
++
+Under some conditions it was possible for the `refs/meta/config` branch to be
+erroneously considered not visible to the user.
+
+* Sort list of updated changes in output from push.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2940[Issue 2940]:
+Improve warning messages when `Change-Id` is missing in the commit message.
+
+** Add a hint to amend the commit after installing the commit-msg hook.
+** Don't show 'Suggestion for commit message' when `Change-Id` is missing.
+
+* Allow to publish draft patch sets even when `allowDrafts` is false.
++
+If a user uploaded a change while `allowDrafts` was enabled, and then it was
+disabled by the administrator, the uploaded change could not be published and
+was stuck in the draft state.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3249[Issue 3249]:
+Fix server error when checking mergeability of a change.
+
+* Workaround Guice bug "getPathInfo not decoded".
++
+Due to link:https://github.com/google/guice/issues/745[Guice issue 745], cloning
+of a repository with a space in its name was impossible.
+
+* Print proper names for tasks in output of `show-queue` command.
++
+Some tasks were not displayed with the proper name.
+
+* Prevent wrong content type for CSS files.
++
+The mime-util library contains two content type mappings for .css files:
+`application/x-pointplus` and `text/css`.  Unfortunately, using the wrong one
+will result in most browsers discarding the file as a CSS file.  Ensure we only
+use the correct type for CSS files.
+
+SSH
+~~~
+
+* Prevent double authentication for the same public key.
+
+
+Secondary Index / Search
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2822[Issue 2822]:
+Improve Lucene analysis of words linked with underscore or dot.
++
+Instead of treating words linked with underscore or dot as one word, Lucene now
+treats them as separate words.
+
+* Fix support for `change~branch~id` in query syntax.
+
+
+Configuration
+~~~~~~~~~~~~~
+
+[[remove-generate-http-password-capability]]
+* Remove the 'Generate HTTP Password' capability.
++
+The 'Generate HTTP Password' capability has been removed to close a security
+vulnerability.  Now only administrators are allowed to generate and delete other
+users' http passwords via the REST or SSH interface.
++
+It is encouraged to clean up your `project.config` settings after upgrading.
+
+* Fix support for multiple `footer` tokens in tracking ID config.
++
+Contrary to
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#trackingid[
+the documentation], if more than one `footer` token was specified in the
+`trackingid` section, only the first was used.
+
+* Treat empty
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#hooks[
+`hooks.*`] values as missing, rather than trying to execute the hooks
+directory.
+
+* Fix `changed-merged` hook configuration.
++
+Contrary to the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/config-gerrit.html#hooks[
+documentation], the changed-merged hook configuration value was being
+read from `hooks.changeMerged`. Fix to use `hooks.changeMergedHook` as
+documented.
+
+Web UI
+~~~~~~
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3044[Issue 3044]:
+Remove stripping `#` in login redirect.
+
+Change Screen
+^^^^^^^^^^^^^
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3147[Issue 3147]:
+Allow to disable muting of common path prefixes in the file list.
++
+In the file table, parts of the file path that are common to the file previously
+listed are muted. The purpose of this is to make it easier to see files that all
+belong under the same path, but some users find it annoying.
++
+This feature can now be enabled or disabled, per user, with the 'Mute Common
+Path Prefixes In File List' setting.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3130[Issue 3130]:
+Remove special handling of 'LGTM' in review comments
++
+Typing 'LGTM' in the review cover message no longer automatically selects the
+highest available Code-Review score.
+
+* Show a confirmation dialog before deleting a draft change or patch set.
++
+Previously there was no confirmation and a draft change or revision patch
+set would be lost if the button was accidentally clicked.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2533[Issue 2533]:
+Improve the layout and color scheme of buttons.
++
+Several improvements have been made:
++
+** Move 'Publish' and 'Delete Change/Revision' buttons into header.
++
+If a change/revision is a draft the natural next step is to publish (or delete)
+it, hence these buttons should be displayed in a more prominent place.
+
+** Highlight the 'Publish' button in blue.
++
+If a change is a draft the natural next step is to publish it, hence
+the 'Publish' button should be highlighted similar to the quick
+approve button.
+
+** Fix the border color of buttons on the reply popup.
++
+The buttons are blue but had white borders, which was inconsistent with the
+buttons on the change screen.
+
+** Remove red color for 'Abandon' and 'Restore' buttons.
++
+There is nothing dangerous about these operations that justifies
+highlighting the buttons in red color. When the buttons are clicked
+there is a popup where the user must confirm the operation, so it can
+still be canceled.
+
+** Hide quick approve button for draft changes.
++
+A draft change cannot be submitted, hence quick approving it is not that
+important. Hiding the quick approve button on draft changes makes space in the
+header for displaying more important actions such as 'Publish'.
+
+* Differentiate between conflicts and already merged errors in cherry-pick
++
+When a cherry-pick operation failed with 'Cherry pick failed' error, there was no
+way to know the reason for the failure: merge conflict or the commit is already
+on the target branch.  These failures are now differentiated and an appropriate
+error is reported.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2837[Issue 2837]:
+Improve display of long user names for collapsed comments in history.
++
+If there were several users with long user names with the same prefix, e.g.
+'AutomaticGerritVoterLinux' and 'AutomaticGerritVoterWindows', they would both
+be shown as 'AutomaticGerritVo...' and users had to expand the comment to see
+the full user name.
++
+The ellipsis is now inserted in the middle of the user name so that the start
+and end of the user name are always visible, e.g. 'AutomaticG...VoterLinux' and
+'AutomaticG...terWindows'.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2992[Issue 2992]:
+Fix display of review comments for Chrome on Android.
++
+Chrome for Android has Font Boosting, which caused the review comments to
+be displayed too large.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2909[Issue 2909]:
+Make change owner votes removable.
++
+If a change owner voted on a change, it was not possible for anyone other
+than the owner to remove the vote.
+
+* Preserve topic when cherry-picking.
++
+When a change is cherry-picked, the topic from the source change is preserved
+on the newly created change.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3007[Issue 3007]:
+Make the selected tab persistent.
++
+If a change from the 'Same Topic' tab was clicked, the selected tab would reset
+to the default tab ('Related Changes').
+
+* Left-align column titles in the file list.
+
+* Increase right margin of download box to make space for scrollbar.
++
+Under some circumstances the browser's scrollbar would be shown over the
+copy-to-clipboard icons in the download dropdown.
+
+* Display +1 score's text next to the checkbox for simple boolean labels.
++
+In the reply box, the text of the label score is displayed on the right hand
+side when a score is selected, but this was missing for simple boolean labels.
+
+* Don't show missing accounts as reviewer suggestions.
+
+* Show the email address that matched the search in reviewer suggestions.
++
+When matching accounts by email address against an external account, results
+now show the email address that matched, not the preferred email address.
+
+* Fix accidental reviewer selection on slow networks.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3120[Issue 3120]:
+Align parent weblinks with parent commits in the commit box.
+
+
+Side-By-Side Diff
+^^^^^^^^^^^^^^^^^
+
+* Return to normal mode after editing a draft comment.
++
+Previously it would remain in visual mode.
+
+* Fix C++ header and source syntax highlighting
++
+cpp and hpp files were sometimes rendered with C mode and not the extended C++
+mode.  This prevented keywords like `class` from being colored by the
+highlighter.
+
+
+Project Screen
+^^^^^^^^^^^^^^
+
+* Fix alignment of checkboxes on project access screen.
++
+The 'Exclusive' checkbox was not aligned with the other checkboxes.
+
+REST API
+~~~~~~~~
+
+Changes
+^^^^^^^
+
+* Remove the administrator restriction on the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#index-change[
+index change] endpoint.
++
+The endpoint can now be used by any user who has visibility of the change.
+
+* Only include account ID in responses unless `DETAILED_ACCOUNTS` option is set.
++
+The behavior was inconsistent with the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-accounts.html#account-info[
+documentation]. In the default case it was including only the account name,
+rather than only the account ID.
+
+* Include revision's ref in responses.
++
+The ref of a revision was only returned as part of the fetch info, which is only
+available if the download commands are installed.
+
+* Correctly set the limit to the default when no limit is given in the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-changes.html#suggest-reviewers[
+suggest reviewers] endpoint.
+
+* Return correct response from 'delete draft' endpoints.
++
+When the `change.allowDrafts` setting is False, it is not allowed to delete
+draft changes or patch sets.
++
+In this case the response `405 Method Not Allowed` is now returned, instead of
+`409 Conflict`.
+
+
+Projects
+^^^^^^^^
+
+* Make it mandatory to specify at least one of the `--prefix`, `--match` or `--regex`
+options in the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11/rest-api-projects.html#list-projects[
+list projects] endpoint.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2706[Issue 2706]:
+Do not delete branches concurrently.
++
+Deleting multiple branches from the UI was resulting in a server error when
+branches were in the packed-refs.
+
+* Add retry logic for lock failure when deleting a branch.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3153[Issue 3153]:
+Fix handling of project names ending with `.git`.
++
+The projects REST API documentation states that the `.git` suffix will be
+stripped off the input project name, if present.
++
+This was working for the 'Create Project' endpoint, but not for any of the
+others.
+
+
+Plugins
+~~~~~~~
+
+Replication
+^^^^^^^^^^^
+
+* Create missing repositories on the remote when replicating with the git
+protocol.
+
+* Make `createMissingRepositories = false` take effect on `project-created` event.
++
+Previously `createMissingRepositories = false` would prevent the replication
+plugin from trying to create a new project when a `ref-updated` event was fired,
+but when a `project-created` event was fired the replication plugin would try to
+create a project on the remote.
+
+
+Upgrades
+--------
+
+* Update Antlr to 3.5.2.
+
+* Update ASM to 5.0.3.
+
+* Update Bouncycastle to 1.51.
+
+* Update CodeMirror to 4.10.0-6-gd0a2dda.
+
+* Update Guava to 18.0.
+
+* Update Guice to 4.0-beta5.
+
+* Update GWT to 2.7.
+
+* Update gwtjsonrpc to 1.7-2-g272ca32.
+
+* Update gwtorm to 1.14-14-gf54f1f1.
+
+* Update Jetty to 9.2.9.v20150224.
+
+* Update JGit to 3.7.0.201502260915-r.58-g65c379e.
+
+* Update Lucene to 4.10.2.
+
+* Update Parboiled to 1.1.7.
+
+* Update Pegdown to 1.4.2.
+
+* Update SSHD to 0.14.0.
diff --git a/ReleaseNotes/ReleaseNotes-2.9.1.txt b/ReleaseNotes/ReleaseNotes-2.9.1.txt
index 42422ad..656b5b2 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.1.txt
@@ -7,6 +7,12 @@
 link:https://gerrit-releases.storage.googleapis.com/gerrit-2.9.1.war[
 https://gerrit-releases.storage.googleapis.com/gerrit-2.9.1.war]
 
+*WARNING:* When upgrading from version 2.8.4 or older with a site that uses
+Bouncy Castle Crypto, new versions of the libraries will be downloaded. The old
+libraries should be manually removed from site's `lib` folder to prevent the
+startup failure described in
+link:https://code.google.com/p/gerrit/issues/detail?id=3084[Issue 3084].
+
 Bug Fixes
 ---------
 
diff --git a/ReleaseNotes/ReleaseNotes-2.9.2.txt b/ReleaseNotes/ReleaseNotes-2.9.2.txt
index b175546..a586b45 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.2.txt
@@ -22,6 +22,12 @@
   java -jar gerrit.war init -d site_path
 ----
 
+*WARNING:* When upgrading from version 2.8.4 or older with a site that uses
+Bouncy Castle Crypto, new versions of the libraries will be downloaded. The old
+libraries should be manually removed from site's `lib` folder to prevent the
+startup failure described in
+link:https://code.google.com/p/gerrit/issues/detail?id=3084[Issue 3084].
+
 Bug Fixes
 ---------
 
diff --git a/ReleaseNotes/ReleaseNotes-2.9.3.txt b/ReleaseNotes/ReleaseNotes-2.9.3.txt
index 95832c8..f3fcf16 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.3.txt
@@ -22,6 +22,12 @@
   java -jar gerrit.war init -d site_path
 ----
 
+*WARNING:* When upgrading from version 2.8.4 or older with a site that uses
+Bouncy Castle Crypto, new versions of the libraries will be downloaded. The old
+libraries should be manually removed from site's `lib` folder to prevent the
+startup failure described in
+link:https://code.google.com/p/gerrit/issues/detail?id=3084[Issue 3084].
+
 Bug Fixes
 ---------
 
diff --git a/ReleaseNotes/ReleaseNotes-2.9.4.txt b/ReleaseNotes/ReleaseNotes-2.9.4.txt
index 38bf9c6..0a7010d 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.4.txt
@@ -22,6 +22,12 @@
   java -jar gerrit.war init -d site_path
 ----
 
+*WARNING:* When upgrading from version 2.8.4 or older with a site that uses
+Bouncy Castle Crypto, new versions of the libraries will be downloaded. The old
+libraries should be manually removed from site's `lib` folder to prevent the
+startup failure described in
+link:https://code.google.com/p/gerrit/issues/detail?id=3084[Issue 3084].
+
 Bug Fixes
 ---------
 
diff --git a/ReleaseNotes/ReleaseNotes-2.9.txt b/ReleaseNotes/ReleaseNotes-2.9.txt
index 2a57054..de5c665 100644
--- a/ReleaseNotes/ReleaseNotes-2.9.txt
+++ b/ReleaseNotes/ReleaseNotes-2.9.txt
@@ -30,6 +30,17 @@
   java -jar gerrit.war reindex --recheck-mergeable -d site_path
 ----
 
+*WARNING:* Upgrading to 2.9.x requires the server be first upgraded to 2.1.7 (or
+a later 2.1.x version), and then to 2.9.x.  If you are upgrading from 2.2.x.x or
+later, you may ignore this warning and upgrade directly to 2.9.x.
+
+*WARNING:* When upgrading from version 2.8.4 or older with a site that uses
+Bouncy Castle Crypto, new versions of the libraries will be downloaded. The old
+libraries should be manually removed from site's `lib` folder to prevent the
+startup failure described in
+link:https://code.google.com/p/gerrit/issues/detail?id=3084[Issue 3084].
+
+
 *WARNING:* Support for query via the SQL index is removed. The usage of
 a secondary index is now mandatory.
 
@@ -86,10 +97,6 @@
 The plugin can be upgraded manually by copying the new plugin jar into
 the site's `plugins` folder.
 
-*WARNING:* Upgrading to 2.9.x requires the server be first upgraded to 2.1.7 (or
-a later 2.1.x version), and then to 2.9.x.  If you are upgrading from 2.2.x.x or
-later, you may ignore this warning and upgrade directly to 2.9.x.
-
 
 Release Highlights
 ------------------
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 464e9f5..d12acf4 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,6 +1,11 @@
 Gerrit Code Review - Release Notes
 ==================================
 
+[[2_11]]
+Version 2.11.x
+--------------
+* link:ReleaseNotes-2.11.html[2.11]
+
 [[2_10]]
 Version 2.10.x
 --------------
diff --git a/VERSION b/VERSION
index 5b0f873..e4a187b 100644
--- a/VERSION
+++ b/VERSION
@@ -2,4 +2,4 @@
 # Used by :api_install and :api_deploy targets
 # when talking to the destination repository.
 #
-GERRIT_VERSION = '2.10.3'
+GERRIT_VERSION = '2.11'
diff --git a/bucklets/gerrit_plugin.bucklet b/bucklets/gerrit_plugin.bucklet
index eb10456..ae7e1a2 100644
--- a/bucklets/gerrit_plugin.bucklet
+++ b/bucklets/gerrit_plugin.bucklet
@@ -13,3 +13,8 @@
 #
 # When compiling from standalone cookbook-plugin, bucklets directory points
 # to cloned bucklets library that includes real gerrit_plugin.bucklet code.
+
+GERRIT_PLUGIN_API = ['//gerrit-plugin-api:lib']
+GERRIT_GWT_API = ['//gerrit-plugin-gwtui/gerrit:gwtui-api']
+
+STANDALONE_MODE = False
diff --git a/bucklets/java_doc.bucklet b/bucklets/java_doc.bucklet
new file mode 120000
index 0000000..cc8b6db
--- /dev/null
+++ b/bucklets/java_doc.bucklet
@@ -0,0 +1 @@
+../tools/java_doc.defs
\ No newline at end of file
diff --git a/bucklets/java_sources.bucklet b/bucklets/java_sources.bucklet
new file mode 120000
index 0000000..8a1a5dd
--- /dev/null
+++ b/bucklets/java_sources.bucklet
@@ -0,0 +1 @@
+../tools/java_sources.defs
\ No newline at end of file
diff --git a/bucklets/local_jar.bucklet b/bucklets/local_jar.bucklet
new file mode 120000
index 0000000..8904824
--- /dev/null
+++ b/bucklets/local_jar.bucklet
@@ -0,0 +1 @@
+../lib/local.defs
\ No newline at end of file
diff --git a/bucklets/maven_package.bucklet b/bucklets/maven_package.bucklet
new file mode 120000
index 0000000..b5f5ea8
--- /dev/null
+++ b/bucklets/maven_package.bucklet
@@ -0,0 +1 @@
+../tools/maven/package.defs
\ No newline at end of file
diff --git a/contrib/check-valid-commit.py b/contrib/check-valid-commit.py
index 150b310..d26fa58 100755
--- a/contrib/check-valid-commit.py
+++ b/contrib/check-valid-commit.py
@@ -25,7 +25,7 @@
     patchset = None
 
     try:
-        opts, args = getopt.getopt(sys.argv[1:], '', \
+        opts, _args = getopt.getopt(sys.argv[1:], '', \
             ['change=', 'project=', 'branch=', 'commit=', 'patchset='])
     except getopt.GetoptError as err:
         print('Error: %s' % (err))
diff --git a/contrib/git-push-review b/contrib/git-push-review
new file mode 100755
index 0000000..898b023
--- /dev/null
+++ b/contrib/git-push-review
@@ -0,0 +1,88 @@
+#!/usr/bin/python
+# Copyright (C) 2014 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import print_function
+
+import argparse
+import collections
+import os
+import subprocess
+import sys
+
+
+def get_config(name):
+  args = ['git', 'config', '--get', name]
+  p = subprocess.Popen(args, stdout=subprocess.PIPE)
+  out, _ = p.communicate()
+  ret = p.poll()
+  if ret not in (0, 1):
+    raise subprocess.CalledProcessError(ret, ' '.join(args), output=out)
+  return out.strip()
+
+
+def deref(name):
+  p = subprocess.Popen(
+      ['git', 'rev-parse', '--symbolic-full-name', name],
+      stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+  out, _ = p.communicate()
+  return out.strip()
+
+
+def main(argv):
+  p = argparse.ArgumentParser(description='Push changes to Gerrit for review')
+  p.add_argument('-r', '--remote', default='', metavar='REMOTE',
+                 help='remote name or URL to push to')
+  p.add_argument('-b', '--branch', default='', metavar='BRANCH',
+                 help='remote branch name, refs/for/BRANCH')
+  p.add_argument('reviewers', nargs='*', metavar='REVIEWER',
+                 help='reviewer names or aliases')
+  p.add_argument('-t', '--topic', default='', metavar='TOPIC',
+                 help='topic for new changes')
+  p.add_argument('--dry-run', action='store_true',
+                 help='dry run, print git command and exit')
+  args = p.parse_args()
+
+  if not args.remote or not args.branch:
+    hp = 'refs/heads/'
+    upstream = deref('HEAD')
+    while upstream.startswith(hp):
+      upstream = deref(upstream[len(hp):] + '@{u}')
+
+    rp = 'refs/remotes/'
+    if upstream.startswith(rp):
+      def_remote, def_branch = upstream[len(rp):].split('/', 1)
+    else:
+      def_remote, def_branch = 'origin', 'master'
+    args.remote = args.remote or def_remote
+    args.branch = args.branch or def_branch
+
+  opts = collections.defaultdict(list)
+  opts['r'].extend((get_config('reviewer.' + r) or r) for r in args.reviewers)
+  if args.topic:
+    opts['topic'].append(args.topic)
+  opts_str = ','.join('%s=%s' % (k, v) for k in opts for v in opts[k])
+  if opts_str:
+    opts_str = '%' + opts_str
+
+  git_args = ['git', 'push', args.remote,
+              'HEAD:refs/for/%s%s' % (args.branch, opts_str)]
+  if args.dry_run:
+    print(' '.join(git_args))
+    return 0
+  os.execvp('git', git_args)
+
+
+if __name__ == '__main__':
+  sys.exit(main(sys.argv))
diff --git a/gerrit-acceptance-tests/BUCK b/gerrit-acceptance-tests/BUCK
index 3c26720..c7bea4e 100644
--- a/gerrit-acceptance-tests/BUCK
+++ b/gerrit-acceptance-tests/BUCK
@@ -8,10 +8,12 @@
     '//gerrit-launcher:launcher',
     '//gerrit-lucene:lucene',
     '//gerrit-httpd:httpd',
-    '//gerrit-pgm:init-base',
+    '//gerrit-pgm:init',
     '//gerrit-pgm:pgm',
+    '//gerrit-pgm:util',
     '//gerrit-reviewdb:server',
     '//gerrit-server:server',
+    '//gerrit-server/src/main/prolog:common',
     '//gerrit-server:testutil',
     '//gerrit-sshd:sshd',
 
@@ -24,9 +26,10 @@
     '//lib:jsch',
     '//lib:junit',
     '//lib:servlet-api-3_1',
+    '//lib:truth',
 
-    '//lib/commons:httpclient',
-    '//lib/commons:httpcore',
+    '//lib/httpcomponents:httpclient',
+    '//lib/httpcomponents:httpcore',
     '//lib/log:impl_log4j',
     '//lib/log:log4j',
     '//lib/guice:guice',
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 2b01a22..ff61899 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -14,33 +14,53 @@
 
 package com.google.gerrit.acceptance;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.cloneProject;
 import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static com.google.gerrit.acceptance.GitUtil.initSsh;
-import static org.junit.Assert.assertEquals;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.Util.block;
 
 import com.google.common.base.Joiner;
 import com.google.common.primitives.Chars;
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.OutputFormat;
-import com.google.gerrit.server.change.ChangeJson;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.Util;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.TempFileUtil;
 import com.google.gson.Gson;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.util.Providers;
 
 import org.apache.http.HttpStatus;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Rule;
 import org.junit.rules.TestRule;
@@ -60,6 +80,9 @@
   public Config baseConfig;
 
   @Inject
+  protected AllProjectsName allProjects;
+
+  @Inject
   protected AccountCreator accounts;
 
   @Inject
@@ -77,6 +100,27 @@
   @Inject
   protected PushOneCommit.Factory pushFactory;
 
+  @Inject
+  protected MetaDataUpdate.Server metaDataUpdateFactory;
+
+  @Inject
+  protected ProjectCache projectCache;
+
+  @Inject
+  protected GroupCache groupCache;
+
+  @Inject
+  protected GitRepositoryManager repoManager;
+
+  @Inject
+  protected ChangeIndexer indexer;
+
+  @Inject
+  protected Provider<InternalChangeQuery> queryProvider;
+
+  @Inject
+  protected @GerritServerConfig Config cfg;
+
   protected Git git;
   protected GerritServer server;
   protected TestAccount admin;
@@ -120,6 +164,26 @@
     }
   }
 
+  protected static Config submitWholeTopicEnabledConfig() {
+    Config cfg = new Config();
+    cfg.setBoolean("change", null, "submitWholeTopic", true);
+    return cfg;
+  }
+
+  protected static Config allowDraftsDisabledConfig() {
+    Config cfg = new Config();
+    cfg.setBoolean("change", null, "allowDrafts", false);
+    return cfg;
+  }
+
+  protected boolean isAllowDrafts() {
+    return cfg.getBoolean("change", "allowDrafts", true);
+  }
+
+  protected boolean isSubmitWholeTopicEnabled() {
+    return cfg.getBoolean("change", null, "submitWholeTopic", false);
+  }
+
   private void beforeTest(Config cfg, boolean memory, boolean enableHttpd) throws Exception {
     server = startServer(cfg, memory, enableHttpd);
     server.getTestInjector().injectMembers(this);
@@ -159,24 +223,29 @@
       Chars.asList(new char[]{'a','b','c','d','e','f','g','h'});
   protected PushOneCommit.Result amendChange(String changeId)
       throws GitAPIException, IOException {
+    return amendChange(changeId, "refs/for/master");
+  }
+
+  protected PushOneCommit.Result amendChange(String changeId, String ref)
+      throws GitAPIException, IOException {
     Collections.shuffle(RANDOM);
     PushOneCommit push =
         pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
             PushOneCommit.FILE_NAME, new String(Chars.toArray(RANDOM)), changeId);
-    return push.to(git, "refs/for/master");
+    return push.to(git, ref);
   }
 
-  protected ChangeJson.ChangeInfo getChange(String changeId, ListChangesOption... options)
+  protected ChangeInfo getChange(String changeId, ListChangesOption... options)
       throws IOException {
     return getChange(adminSession, changeId, options);
   }
 
-  protected ChangeJson.ChangeInfo getChange(RestSession session, String changeId,
+  protected ChangeInfo getChange(RestSession session, String changeId,
       ListChangesOption... options) throws IOException {
     String q = options.length > 0 ? "?o=" + Joiner.on("&o=").join(options) : "";
     RestResponse r = session.get("/changes/" + changeId + q);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    return newGson().fromJson(r.getReader(), ChangeJson.ChangeInfo.class);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    return newGson().fromJson(r.getReader(), ChangeInfo.class);
   }
 
   protected ChangeInfo info(String id)
@@ -189,6 +258,11 @@
     return gApi.changes().id(id).get();
   }
 
+  protected EditInfo getEdit(String id)
+      throws RestApiException {
+    return gApi.changes().id(id).getEdit();
+  }
+
   protected ChangeInfo get(String id, ListChangesOption... options)
       throws RestApiException {
     EnumSet<ListChangesOption> s = EnumSet.noneOf(ListChangesOption.class);
@@ -218,4 +292,69 @@
         .id(r.getChangeId())
         .current();
   }
+
+  protected void allow(String permission, AccountGroup.UUID id, String ref)
+      throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    Util.allow(cfg, permission, id, ref);
+    saveProjectConfig(project, cfg);
+  }
+
+  protected void allowGlobalCapability(String capabilityName,
+      AccountGroup.UUID id) throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+    Util.allow(cfg, capabilityName, id);
+    saveProjectConfig(allProjects, cfg);
+  }
+
+  protected void deny(String permission, AccountGroup.UUID id, String ref)
+      throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    Util.deny(cfg, permission, id, ref);
+    saveProjectConfig(project, cfg);
+  }
+
+  protected void saveProjectConfig(Project.NameKey p, ProjectConfig cfg)
+      throws Exception {
+    MetaDataUpdate md = metaDataUpdateFactory.create(p);
+    try {
+      cfg.commit(md);
+    } finally {
+      md.close();
+    }
+    projectCache.evict(cfg.getProject());
+  }
+
+  protected void grant(String permission, Project.NameKey project, String ref)
+      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
+    grant(permission, project, ref, false);
+  }
+
+  protected void grant(String permission, Project.NameKey project, String ref,
+      boolean force) throws RepositoryNotFoundException, IOException,
+      ConfigInvalidException {
+    MetaDataUpdate md = metaDataUpdateFactory.create(project);
+    md.setMessage(String.format("Grant %s on %s", permission, ref));
+    ProjectConfig config = ProjectConfig.read(md);
+    AccessSection s = config.getAccessSection(ref, true);
+    Permission p = s.getPermission(permission, true);
+    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
+    PermissionRule rule = new PermissionRule(config.resolve(adminGroup));
+    rule.setForce(force);
+    p.add(rule);
+    config.commit(md);
+    projectCache.evict(config.getProject());
+  }
+
+  protected void blockRead(Project.NameKey project, String ref) throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    block(cfg, Permission.READ, REGISTERED_USERS, ref);
+    saveProjectConfig(project, cfg);
+  }
+
+  protected PushOneCommit.Result pushTo(String ref) throws GitAPIException,
+      IOException {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent());
+    return push.to(git, ref);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
index 6ee7efa..2a578c2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance;
 
 import com.google.common.collect.Maps;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.RequestCleanup;
@@ -22,7 +23,6 @@
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Key;
@@ -164,8 +164,10 @@
 
   /** Returns exactly one instance per command executed. */
   static final Scope REQUEST = new Scope() {
+    @Override
     public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) {
       return new Provider<T>() {
+        @Override
         public T get() {
           return requireContext().get(key, creator);
         }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
index 0fc053e..a376332 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -14,12 +14,7 @@
 
 package com.google.gerrit.acceptance;
 
-import java.io.ByteArrayOutputStream;
-import java.io.UnsupportedEncodingException;
-import java.util.Collections;
-
-import javax.inject.Inject;
-
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -30,14 +25,18 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
 
 import com.jcraft.jsch.JSch;
 import com.jcraft.jsch.JSchException;
 import com.jcraft.jsch.KeyPair;
 
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Collections;
+
 public class AccountCreator {
 
   private SchemaFactory<ReviewDb> reviewDbProvider;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
index 41df3e6..07d0f50 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
@@ -42,7 +42,7 @@
     return cfg;
   }
 
-  static private void parseAnnotation(Config cfg, GerritConfig c) {
+  private static void parseAnnotation(Config cfg, GerritConfig c) {
     ArrayList<String> l = Lists.newArrayList(splitter.split(c.name()));
     if (l.size() == 2) {
       cfg.setString(l.get(0), null, l.get(1), c.value());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index 85da2f5..644a4bb 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -22,22 +22,22 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.index.ChangeSchemas;
 import com.google.gerrit.server.util.SocketUtil;
+import com.google.gerrit.testutil.TempFileUtil;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Module;
 
-import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.RepositoryCache;
 import org.eclipse.jgit.util.FS;
 
 import java.io.File;
-import java.io.IOException;
 import java.lang.reflect.Field;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.URI;
-import java.net.UnknownHostException;
 import java.util.concurrent.BrokenBarrierException;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CyclicBarrier;
@@ -50,8 +50,10 @@
   /** Returns fully started Gerrit server */
   static GerritServer start(Config cfg, boolean memory, boolean enableHttpd)
       throws Exception {
+    Logger.getLogger("com.google.gerrit").setLevel(Level.DEBUG);
     final CyclicBarrier serverStarted = new CyclicBarrier(2);
     final Daemon daemon = new Daemon(new Runnable() {
+      @Override
       public void run() {
         try {
           serverStarted.await();
@@ -83,6 +85,7 @@
       site = initSite(cfg);
       daemonService = Executors.newSingleThreadExecutor();
       daemonService.submit(new Callable<Void>() {
+        @Override
         public Void call() throws Exception {
           int rc = daemon.main(new String[] {"-d", site.getPath(), "--headless" });
           if (rc != 0) {
@@ -121,14 +124,14 @@
     return tmp;
   }
 
-  private static void mergeTestConfig(Config cfg)
-      throws IOException {
+  private static void mergeTestConfig(Config cfg) {
     String forceEphemeralPort = String.format("%s:0",
         getLocalHost().getHostName());
     String url = "http://" + forceEphemeralPort + "/";
     cfg.setString("gerrit", null, "canonicalWebUrl", url);
     cfg.setString("httpd", null, "listenUrl", url);
     cfg.setString("sshd", null, "listenAddress", forceEphemeralPort);
+    cfg.setBoolean("sshd", null, "testUseInsecureRandom", true);
     cfg.setString("cache", null, "directory", null);
     cfg.setString("gerrit", null, "basePath", "git");
     cfg.setBoolean("sendemail", null, "enable", false);
@@ -156,7 +159,7 @@
     return (T) f.get(obj);
   }
 
-  private static InetAddress getLocalHost() throws UnknownHostException {
+  private static InetAddress getLocalHost() {
     return InetAddress.getLoopbackAddress();
   }
 
@@ -168,7 +171,7 @@
   private InetSocketAddress httpAddress;
 
   private GerritServer(Injector testInjector, Daemon daemon,
-      ExecutorService daemonService) throws IOException, ConfigInvalidException {
+      ExecutorService daemonService) {
     this.testInjector = testInjector;
     this.daemon = daemon;
     this.daemonService = daemonService;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GitUtil.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GitUtil.java
index 3234ffd..494f094 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GitUtil.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GitUtil.java
@@ -14,8 +14,12 @@
 
 package com.google.gerrit.acceptance;
 
+import static com.google.common.base.Preconditions.checkState;
+
 import com.google.common.collect.Iterables;
+import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.testutil.TempFileUtil;
 
 import com.jcraft.jsch.JSch;
 import com.jcraft.jsch.JSchException;
@@ -29,12 +33,9 @@
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.PushCommand;
 import org.eclipse.jgit.api.errors.GitAPIException;
-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;
 import org.eclipse.jgit.transport.OpenSshConfig.Host;
 import org.eclipse.jgit.transport.PushResult;
@@ -47,6 +48,7 @@
 import java.io.File;
 import java.io.FileWriter;
 import java.io.IOException;
+import java.util.List;
 import java.util.Properties;
 
 public class GitUtil {
@@ -100,6 +102,10 @@
       b.append("\"");
     }
     s.exec(b.toString());
+    if (s.hasError()) {
+      throw new IllegalStateException(
+          "gerrit create-project returned error: " + s.getError());
+    }
   }
 
   public static Git cloneProject(String url) throws GitAPIException, IOException {
@@ -136,56 +142,41 @@
   }
 
   public static void rm(Git gApi, String path)
-      throws GitAPIException, IOException {
+      throws GitAPIException {
     gApi.rm()
         .addFilepattern(path)
         .call();
   }
 
   public static Commit createCommit(Git git, PersonIdent i, String msg)
-      throws GitAPIException, IOException {
+      throws GitAPIException {
     return createCommit(git, i, msg, null);
   }
 
   public static Commit amendCommit(Git git, PersonIdent i, String msg, String changeId)
-      throws GitAPIException, IOException {
+      throws GitAPIException {
     msg = ChangeIdUtil.insertId(msg, ObjectId.fromString(changeId.substring(1)));
     return createCommit(git, i, msg, changeId);
   }
 
   private static Commit createCommit(Git git, PersonIdent i, String msg,
-      String changeId) throws GitAPIException, IOException {
+      String changeId) throws GitAPIException {
 
     final CommitCommand commitCmd = git.commit();
     commitCmd.setAmend(changeId != null);
     commitCmd.setAuthor(i);
     commitCmd.setCommitter(i);
-
-    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);
+    commitCmd.setInsertChangeId(changeId == null);
 
     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 {
-      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();
-    }
+    List<String> ids = c.getFooterLines(FooterConstants.CHANGE_ID);
+    checkState(ids.size() >= 1,
+        "No Change-Id found in new commit:\n%s", c.getFullMessage());
+    changeId = ids.get(ids.size() - 1);
+
+    return new Commit(c, changeId);
   }
 
   public static void fetch(Git git, String spec) throws GitAPIException {
@@ -202,7 +193,13 @@
 
   public static PushResult pushHead(Git git, String ref, boolean pushTags)
       throws GitAPIException {
+    return pushHead(git, ref, pushTags, false);
+  }
+
+  public static PushResult pushHead(Git git, String ref, boolean pushTags,
+      boolean force) throws GitAPIException {
     PushCommand pushCmd = git.push();
+    pushCmd.setForce(force);
     pushCmd.setRefSpecs(new RefSpec("HEAD:" + ref));
     if (pushTags) {
       pushCmd.setPushTags();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpResponse.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
index c58a5a2..872c912 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
@@ -43,7 +43,7 @@
   public void consume() throws IllegalStateException, IOException {
     Reader reader = getReader();
     if (reader != null) {
-      while (reader.read() != -1);
+      while (reader.read() != -1) {}
     }
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpSession.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpSession.java
index 5d81900..f765e7a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpSession.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/HttpSession.java
@@ -52,7 +52,7 @@
       client = HttpClientBuilder
           .create()
           .setDefaultCredentialsProvider(creds)
-          .setMaxConnPerRoute(10)
+          .setMaxConnPerRoute(512)
           .setMaxConnTotal(1024)
           .build();
     }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
index 32705ba..6cd8031 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -14,12 +14,11 @@
 
 package com.google.gerrit.acceptance;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.add;
 import static com.google.gerrit.acceptance.GitUtil.amendCommit;
 import static com.google.gerrit.acceptance.GitUtil.createCommit;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 
 import com.google.common.base.Function;
 import com.google.common.base.Strings;
@@ -34,11 +33,15 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
 import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.TagCommand;
 import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.InvalidTagNameException;
@@ -57,7 +60,7 @@
 public class PushOneCommit {
   public static final String SUBJECT = "test commit";
   public static final String FILE_NAME = "a.txt";
-  private static final String FILE_CONTENT = "some content";
+  public static final String FILE_CONTENT = "some content";
 
   public interface Factory {
     PushOneCommit create(
@@ -80,8 +83,28 @@
         @Assisted("changeId") String changeId);
   }
 
+  public static class Tag {
+    public String name;
+
+    public Tag(String name) {
+      this.name = name;
+    }
+  }
+
+  public static class AnnotatedTag extends Tag {
+    public String message;
+    public PersonIdent tagger;
+
+    public AnnotatedTag(String name, String message, PersonIdent tagger) {
+      super(name);
+      this.message = message;
+      this.tagger = tagger;
+    }
+  }
+
   private final ChangeNotes.Factory notesFactory;
   private final ApprovalsUtil approvalsUtil;
+  private final Provider<InternalChangeQuery> queryProvider;
   private final ReviewDb db;
   private final PersonIdent i;
 
@@ -89,30 +112,36 @@
   private final String fileName;
   private final String content;
   private String changeId;
-  private String tagName;
+  private Tag tag;
+  private boolean force;
 
   @AssistedInject
   PushOneCommit(ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
+      Provider<InternalChangeQuery> queryProvider,
       @Assisted ReviewDb db,
       @Assisted PersonIdent i) {
-    this(notesFactory, approvalsUtil, db, i, SUBJECT, FILE_NAME, FILE_CONTENT);
+    this(notesFactory, approvalsUtil, queryProvider,
+        db, i, SUBJECT, FILE_NAME, FILE_CONTENT);
   }
 
   @AssistedInject
   PushOneCommit(ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
+      Provider<InternalChangeQuery> queryProvider,
       @Assisted ReviewDb db,
       @Assisted PersonIdent i,
       @Assisted("subject") String subject,
       @Assisted("fileName") String fileName,
       @Assisted("content") String content) {
-    this(notesFactory, approvalsUtil, db, i, subject, fileName, content, null);
+    this(notesFactory, approvalsUtil, queryProvider,
+        db, i, subject, fileName, content, null);
   }
 
   @AssistedInject
   PushOneCommit(ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
+      Provider<InternalChangeQuery> queryProvider,
       @Assisted ReviewDb db,
       @Assisted PersonIdent i,
       @Assisted("subject") String subject,
@@ -122,6 +151,7 @@
     this.db = db;
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
+    this.queryProvider = queryProvider;
     this.i = i;
     this.subject = subject;
     this.fileName = fileName;
@@ -129,21 +159,18 @@
     this.changeId = changeId;
   }
 
-  public Result to(Git git, String ref)
-      throws GitAPIException, IOException {
+  public Result to(Git git, String ref) throws GitAPIException, IOException {
     add(git, fileName, content);
     return execute(git, ref);
   }
 
-  public Result rm(Git git, String ref)
-      throws GitAPIException, IOException {
+  public Result rm(Git git, String ref) throws GitAPIException {
     GitUtil.rm(git, fileName);
     return execute(git, ref);
   }
 
   private Result execute(Git git, String ref) throws GitAPIException,
-      IOException, ConcurrentRefUpdateException, InvalidTagNameException,
-      NoHeadException {
+      ConcurrentRefUpdateException, InvalidTagNameException, NoHeadException {
     Commit c;
     if (changeId != null) {
       c = amendCommit(git, i, subject, changeId);
@@ -151,33 +178,54 @@
       c = createCommit(git, i, subject);
       changeId = c.getChangeId();
     }
-    if (tagName != null) {
-      git.tag().setName(tagName).setAnnotated(false).call();
+    if (tag != null) {
+      TagCommand tagCommand = git.tag().setName(tag.name);
+      if (tag instanceof AnnotatedTag) {
+        AnnotatedTag annotatedTag = (AnnotatedTag)tag;
+        tagCommand.setAnnotated(true)
+          .setMessage(annotatedTag.message)
+          .setTagger(annotatedTag.tagger);
+      } else {
+        tagCommand.setAnnotated(false);
+      }
+      tagCommand.call();
     }
-    return new Result(ref, pushHead(git, ref, tagName != null), c, subject);
+    return new Result(ref, pushHead(git, ref, tag != null, force), c, subject);
   }
 
-  public void setTag(final String tagName) {
-    this.tagName = tagName;
+  public void setTag(final Tag tag) {
+    this.tag = tag;
+  }
+
+  public void setForce(boolean force) {
+    this.force = force;
   }
 
   public class Result {
     private final String ref;
     private final PushResult result;
     private final Commit commit;
-    private final String subject;
+    private final String resSubj;
 
-    private Result(String ref, PushResult result, Commit commit,
+    private Result(String ref, PushResult resSubj, Commit commit,
         String subject) {
       this.ref = ref;
-      this.result = result;
+      this.result = resSubj;
       this.commit = commit;
-      this.subject = subject;
+      this.resSubj = subject;
+    }
+
+    public ChangeData getChange() throws OrmException {
+      return Iterables.getOnlyElement(
+          queryProvider.get().byKeyPrefix(commit.getChangeId()));
+    }
+
+    public PatchSet getPatchSet() throws OrmException {
+      return getChange().currentPatchSet();
     }
 
     public PatchSet.Id getPatchSetId() throws OrmException {
-      return Iterables.getOnlyElement(
-          db.changes().byKey(new Change.Key(commit.getChangeId()))).currentPatchSetId();
+      return getChange().change().currentPatchSetId();
     }
 
     public String getChangeId() {
@@ -195,11 +243,10 @@
     public void assertChange(Change.Status expectedStatus,
         String expectedTopic, TestAccount... expectedReviewers)
         throws OrmException {
-      Change c =
-          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()));
+      Change c = getChange().change();
+      assertThat(resSubj).isEqualTo(c.getSubject());
+      assertThat(expectedStatus).isEqualTo(c.getStatus());
+      assertThat(expectedTopic).isEqualTo(Strings.emptyToNull(c.getTopic()));
       assertReviewers(c, expectedReviewers);
     }
 
@@ -216,11 +263,13 @@
 
       for (Account.Id accountId
           : approvalsUtil.getReviewers(db, notesFactory.create(c)).values()) {
-        assertTrue("unexpected reviewer " + accountId,
-            expectedReviewerIds.remove(accountId));
+        assertThat(expectedReviewerIds.remove(accountId))
+          .named("unexpected reviewer " + accountId)
+          .isTrue();
       }
-      assertTrue("missing reviewers: " + expectedReviewerIds,
-          expectedReviewerIds.isEmpty());
+      assertThat((Iterable<?>)expectedReviewerIds)
+        .named("missing reviewers: " + expectedReviewerIds)
+        .isEmpty();
     }
 
     public void assertOkStatus() {
@@ -233,15 +282,17 @@
 
     private void assertStatus(Status expectedStatus, String expectedMessage) {
       RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
-      assertEquals(message(refUpdate),
-          expectedStatus, refUpdate.getStatus());
-      assertEquals(expectedMessage, refUpdate.getMessage());
+      assertThat(expectedStatus)
+        .named(message(refUpdate))
+        .isEqualTo(refUpdate.getStatus());
+      assertThat(expectedMessage).isEqualTo(refUpdate.getMessage());
     }
 
     public void assertMessage(String expectedMessage) {
       RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
-      assertTrue(message(refUpdate), message(refUpdate).toLowerCase().contains(
-          expectedMessage.toLowerCase()));
+      assertThat(message(refUpdate).toLowerCase())
+        .named(message(refUpdate))
+        .contains(expectedMessage.toLowerCase());
     }
 
     private String message(RemoteRefUpdate refUpdate) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
index 45befd0..bf6f928 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
@@ -16,9 +16,11 @@
 
 import com.google.common.base.Charsets;
 import com.google.common.base.Preconditions;
+import com.google.common.net.HttpHeaders;
 import com.google.gerrit.extensions.restapi.RawInput;
 import com.google.gerrit.server.OutputFormat;
 
+import org.apache.http.Header;
 import org.apache.http.client.methods.HttpDelete;
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpPost;
@@ -31,6 +33,7 @@
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 
 public class RestSession extends HttpSession {
 
@@ -40,7 +43,20 @@
 
   @Override
   public RestResponse get(String endPoint) throws IOException {
+    return getWithHeader(endPoint, null);
+  }
+
+  public RestResponse getJsonAccept(String endPoint) throws IOException {
+    return getWithHeader(endPoint,
+        new BasicHeader(HttpHeaders.ACCEPT, "application/json"));
+  }
+
+  private RestResponse getWithHeader(String endPoint, Header header)
+      throws IOException {
     HttpGet get = new HttpGet(url + "/a" + endPoint);
+    if (header != null) {
+      get.addHeader(header);
+    }
     return new RestResponse(getClient().execute(get));
   }
 
@@ -91,12 +107,14 @@
   }
 
 
-  public static RawInput newRawInput(final String content) throws IOException {
-    Preconditions.checkNotNull(content);
-    Preconditions.checkArgument(!content.isEmpty());
-    return new RawInput() {
-      byte bytes[] = content.getBytes("UTF-8");
+  public static RawInput newRawInput(String content) {
+    return newRawInput(content.getBytes(StandardCharsets.UTF_8));
+  }
 
+  public static RawInput newRawInput(final byte[] bytes) {
+    Preconditions.checkNotNull(bytes);
+    Preconditions.checkArgument(bytes.length > 0);
+    return new RawInput() {
       @Override
       public InputStream getInputStream() throws IOException {
         return new ByteArrayInputStream(bytes);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java
index dc648fc..701b337 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java
@@ -14,18 +14,19 @@
 
 package com.google.gerrit.acceptance;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.InetSocketAddress;
-import java.util.Scanner;
+import static com.google.common.base.Preconditions.checkState;
 
 import com.jcraft.jsch.ChannelExec;
 import com.jcraft.jsch.JSch;
 import com.jcraft.jsch.JSchException;
 import com.jcraft.jsch.Session;
 
-public class SshSession {
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.util.Scanner;
 
+public class SshSession {
   private final InetSocketAddress addr;
   private final TestAccount account;
   private Session session;
@@ -36,6 +37,10 @@
     this.account = account;
   }
 
+  public void open() throws JSchException {
+    getSession();
+  }
+
   @SuppressWarnings("resource")
   public String exec(String command) throws JSchException, IOException {
     ChannelExec channel = (ChannelExec) getSession().openChannel("exec");
@@ -86,6 +91,7 @@
   }
 
   public String getUrl() {
+    checkState(session != null, "session must be opened");
     StringBuilder b = new StringBuilder();
     b.append("ssh://");
     b.append(session.getUserName());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java
index 31ed136..bd5f19f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java
@@ -16,12 +16,12 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 
-import java.io.ByteArrayOutputStream;
-
 import com.jcraft.jsch.KeyPair;
 
 import org.eclipse.jgit.lib.PersonIdent;
 
+import java.io.ByteArrayOutputStream;
+
 
 public class TestAccount {
   public final Account.Id id;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java
index 88d46a4..f4d156c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.acceptance;
 
-import static org.junit.Assert.assertEquals;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
@@ -31,7 +31,7 @@
   @Test
   @GerritConfig(name="x.y", value="z")
   public void testOne() {
-    assertEquals("z", serverConfig.getString("x", null, "y"));
+    assertThat(serverConfig.getString("x", null, "y")).isEqualTo("z");
   }
 
   @Test
@@ -40,7 +40,7 @@
       @GerritConfig(name="a.b", value="c")
   })
   public void testMultiple() {
-    assertEquals("z", serverConfig.getString("x", null, "y"));
-    assertEquals("c", serverConfig.getString("a", null, "b"));
+    assertThat(serverConfig.getString("x", null, "y")).isEqualTo("z");
+    assertThat(serverConfig.getString("a", null, "b")).isEqualTo("c");
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index c95f8c3..8945a22d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -14,56 +14,49 @@
 
 package com.google.gerrit.acceptance.api.accounts;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.restapi.RestApiException;
 
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.junit.Test;
 
-import java.io.IOException;
-
 public class AccountIT extends AbstractDaemonTest {
 
   @Test
-  public void get() throws RestApiException {
+  public void get() throws Exception {
     AccountInfo info = gApi
         .accounts()
         .id("admin")
         .get();
-    assertEquals("Administrator", info.name);
-    assertEquals("admin@example.com", info.email);
-    assertEquals("admin", info.username);
+    assertThat(info.name).isEqualTo("Administrator");
+    assertThat(info.email).isEqualTo("admin@example.com");
+    assertThat(info.username).isEqualTo("admin");
   }
 
   @Test
-  public void self() throws RestApiException {
+  public void self() throws Exception {
     AccountInfo info = gApi
         .accounts()
         .self()
         .get();
-    assertEquals("Administrator", info.name);
-    assertEquals("admin@example.com", info.email);
-    assertEquals("admin", info.username);
+    assertThat(info.name).isEqualTo("Administrator");
+    assertThat(info.email).isEqualTo("admin@example.com");
+    assertThat(info.username).isEqualTo("admin");
   }
 
   @Test
-  public void starUnstarChange() throws GitAPIException,
-      IOException, RestApiException {
+  public void starUnstarChange() throws Exception {
     PushOneCommit.Result r = createChange();
     String triplet = "p~master~" + r.getChangeId();
     gApi.accounts()
         .self()
         .starChange(triplet);
-    assertTrue(getChange(triplet).starred);
+    assertThat(getChange(triplet).starred).isTrue();
     gApi.accounts()
         .self()
         .unstarChange(triplet);
-    assertNull(getChange(triplet).starred);
+    assertThat(getChange(triplet).starred).isNull();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index c79b198..c13187f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -14,10 +14,7 @@
 
 package com.google.gerrit.acceptance.api.change;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
@@ -26,23 +23,21 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
-import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.ChangeStatus;
 import com.google.gerrit.extensions.common.LabelInfo;
-import com.google.gerrit.extensions.common.ListChangesOption;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.PatchSet;
 
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.lib.Constants;
 import org.junit.Test;
 
-import java.io.IOException;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Set;
@@ -51,72 +46,130 @@
 public class ChangeIT extends AbstractDaemonTest {
 
   @Test
-  public void get() throws GitAPIException,
-      IOException, RestApiException {
+  public void get() throws Exception {
     PushOneCommit.Result r = createChange();
     String triplet = "p~master~" + r.getChangeId();
     ChangeInfo c = info(triplet);
-    assertEquals(triplet, c.id);
-    assertEquals("p", c.project);
-    assertEquals("master", c.branch);
-    assertEquals(ChangeStatus.NEW, c.status);
-    assertEquals("test commit", c.subject);
-    assertEquals(true, c.mergeable);
-    assertEquals(r.getChangeId(), c.changeId);
-    assertEquals(c.created, c.updated);
-    assertEquals(1, c._number);
+    assertThat(c.id).isEqualTo(triplet);
+    assertThat(c.project).isEqualTo("p");
+    assertThat(c.branch).isEqualTo("master");
+    assertThat(c.status).isEqualTo(ChangeStatus.NEW);
+    assertThat(c.subject).isEqualTo("test commit");
+    assertThat(c.mergeable).isTrue();
+    assertThat(c.changeId).isEqualTo(r.getChangeId());
+    assertThat(c.created).isEqualTo(c.updated);
+    assertThat(c._number).is(1);
+
+    assertThat(c.owner._accountId).is(admin.getId().get());
+    assertThat(c.owner.name).isNull();
+    assertThat(c.owner.email).isNull();
+    assertThat(c.owner.username).isNull();
+    assertThat(c.owner.avatars).isNull();
   }
 
   @Test
-  public void abandon() throws GitAPIException,
-      IOException, RestApiException {
+  public void abandon() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes()
-        .id("p~master~" + r.getChangeId())
+        .id(r.getChangeId())
         .abandon();
   }
 
   @Test
-  public void restore() throws GitAPIException,
-      IOException, RestApiException {
+  public void restore() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes()
-        .id("p~master~" + r.getChangeId())
+        .id(r.getChangeId())
         .abandon();
     gApi.changes()
-        .id("p~master~" + r.getChangeId())
+        .id(r.getChangeId())
         .restore();
   }
 
   @Test
-  public void revert() throws GitAPIException,
-      IOException, RestApiException {
+  public void revert() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes()
-        .id("p~master~" + r.getChangeId())
+        .id(r.getChangeId())
         .revision(r.getCommit().name())
         .review(ReviewInput.approve());
     gApi.changes()
-        .id("p~master~" + r.getChangeId())
+        .id(r.getChangeId())
         .revision(r.getCommit().name())
         .submit();
     gApi.changes()
-        .id("p~master~" + r.getChangeId())
+        .id(r.getChangeId())
         .revert();
   }
 
   // Change is already up to date
   @Test(expected = ResourceConflictException.class)
-  public void rebase() throws GitAPIException,
-      IOException, RestApiException {
+  public void rebase() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes()
-        .id("p~master~" + r.getChangeId())
+        .id(r.getChangeId())
         .revision(r.getCommit().name())
         .rebase();
   }
 
-  private static Set<Account.Id> getReviewers(ChangeInfo ci) {
+  @Test
+  public void rebaseChangeBase() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    PushOneCommit.Result r2 = createChange();
+    PushOneCommit.Result r3 = createChange();
+    RebaseInput ri = new RebaseInput();
+
+    // rebase r3 directly onto master (break dep. towards r2)
+    ri.base = "";
+    gApi.changes()
+        .id(r3.getChangeId())
+        .revision(r3.getCommit().name())
+        .rebase(ri);
+    PatchSet ps3 = r3.getPatchSet();
+    assertThat(ps3.getId().get()).is(2);
+
+    // rebase r2 onto r3 (referenced by ref)
+    ri.base = ps3.getId().toRefName();
+    gApi.changes()
+        .id(r2.getChangeId())
+        .revision(r2.getCommit().name())
+        .rebase(ri);
+    PatchSet ps2 = r2.getPatchSet();
+    assertThat(ps2.getId().get()).is(2);
+
+    // rebase r1 onto r2 (referenced by commit)
+    ri.base = ps2.getRevision().get();
+    gApi.changes()
+        .id(r1.getChangeId())
+        .revision(r1.getCommit().name())
+        .rebase(ri);
+    PatchSet ps1 = r1.getPatchSet();
+    assertThat(ps1.getId().get()).is(2);
+
+    // rebase r1 onto r3 (referenced by change number)
+    ri.base = String.valueOf(r3.getChange().getId().get());
+    gApi.changes()
+        .id(r1.getChangeId())
+        .revision(ps1.getRevision().get())
+        .rebase(ri);
+    assertThat(r1.getPatchSetId().get()).is(3);
+  }
+
+  @Test(expected = ResourceConflictException.class)
+  public void rebaseChangeBaseRecursion() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    PushOneCommit.Result r2 = createChange();
+
+    RebaseInput ri = new RebaseInput();
+    ri.base = r2.getCommit().name();
+    gApi.changes()
+        .id(r1.getChangeId())
+        .revision(r1.getCommit().name())
+        .rebase(ri);
+  }
+
+  private Set<Account.Id> getReviewers(String changeId) throws Exception {
+    ChangeInfo ci = gApi.changes().id(changeId).get();
     Set<Account.Id> result = Sets.newHashSet();
     for (LabelInfo li : ci.labels.values()) {
       for (ApprovalInfo ai : li.all) {
@@ -127,39 +180,44 @@
   }
 
   @Test
-  public void addReviewer() throws GitAPIException,
-      IOException, RestApiException {
+  public void addReviewer() throws Exception {
     PushOneCommit.Result r = createChange();
     AddReviewerInput in = new AddReviewerInput();
     in.reviewer = user.email;
-    ChangeApi cApi = gApi.changes().id("p~master~" + r.getChangeId());
-    cApi.addReviewer(in);
-    assertEquals(ImmutableSet.of(user.id), getReviewers(cApi.get()));
+    gApi.changes()
+        .id(r.getChangeId())
+        .addReviewer(in);
+
+    assertThat((Iterable<?>)getReviewers(r.getChangeId()))
+        .containsExactlyElementsIn(ImmutableSet.of(user.id));
   }
 
   @Test
-  public void addReviewerToClosedChange() throws GitAPIException,
-      IOException, RestApiException {
+  public void addReviewerToClosedChange() throws Exception {
     PushOneCommit.Result r = createChange();
-    ChangeApi cApi = gApi.changes().id("p~master~" + r.getChangeId());
-    cApi.revision(r.getCommit().name())
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
         .review(ReviewInput.approve());
-    cApi.revision(r.getCommit().name())
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
         .submit();
 
-    assertEquals(ImmutableSet.of(admin.getId()), getReviewers(cApi.get()));
+    assertThat((Iterable<?>)getReviewers(r.getChangeId()))
+      .containsExactlyElementsIn(ImmutableSet.of(admin.getId()));
 
     AddReviewerInput in = new AddReviewerInput();
     in.reviewer = user.email;
     gApi.changes()
-        .id("p~master~" + r.getChangeId())
+        .id(r.getChangeId())
         .addReviewer(in);
-    assertEquals(ImmutableSet.of(admin.getId(), user.id),
-        getReviewers(cApi.get()));
+    assertThat((Iterable<?>)getReviewers(r.getChangeId()))
+        .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.id));
   }
 
   @Test
-  public void createEmptyChange() throws RestApiException {
+  public void createEmptyChange() throws Exception {
     ChangeInfo in = new ChangeInfo();
     in.branch = Constants.MASTER;
     in.subject = "Create a change from the API";
@@ -168,9 +226,9 @@
         .changes()
         .create(in)
         .get();
-    assertEquals(in.project, info.project);
-    assertEquals(in.branch, info.branch);
-    assertEquals(in.subject, info.subject);
+    assertThat(info.project).isEqualTo(in.project);
+    assertThat(info.branch).isEqualTo(in.branch);
+    assertThat(info.subject).isEqualTo(in.subject);
   }
 
   @Test
@@ -178,18 +236,18 @@
     PushOneCommit.Result r1 = createChange();
     PushOneCommit.Result r2 = createChange();
     List<ChangeInfo> results = gApi.changes().query().get();
-    assertEquals(2, results.size());
-    assertEquals(r2.getChangeId(), results.get(0).changeId);
-    assertEquals(r1.getChangeId(), results.get(1).changeId);
+    assertThat(results).hasSize(2);
+    assertThat(results.get(0).changeId).isEqualTo(r2.getChangeId());
+    assertThat(results.get(1).changeId).isEqualTo(r1.getChangeId());
   }
 
   @Test
   public void queryChangesNoResults() throws Exception {
     createChange();
     List<ChangeInfo> results = query("status:open");
-    assertEquals(1, results.size());
+    assertThat(results).hasSize(1);
     results = query("status:closed");
-    assertTrue(results.isEmpty());
+    assertThat(results).isEmpty();
   }
 
   @Test
@@ -197,9 +255,9 @@
     PushOneCommit.Result r1 = createChange();
     PushOneCommit.Result r2 = createChange();
     List<ChangeInfo> results = query("status:open");
-    assertEquals(2, results.size());
-    assertEquals(r2.getChangeId(), results.get(0).changeId);
-    assertEquals(r1.getChangeId(), results.get(1).changeId);
+    assertThat(results).hasSize(2);
+    assertThat(results.get(0).changeId).isEqualTo(r2.getChangeId());
+    assertThat(results.get(1).changeId).isEqualTo(r1.getChangeId());
   }
 
   @Test
@@ -207,7 +265,8 @@
     PushOneCommit.Result r1 = createChange();
     createChange();
     List<ChangeInfo> results = query("status:open " + r1.getChangeId());
-    assertEquals(r1.getChangeId(), Iterables.getOnlyElement(results).changeId);
+    assertThat(Iterables.getOnlyElement(results).changeId)
+        .isEqualTo(r1.getChangeId());
   }
 
   @Test
@@ -215,8 +274,9 @@
     createChange();
     PushOneCommit.Result r2 = createChange();
     List<ChangeInfo> results = gApi.changes().query().withLimit(1).get();
-    assertEquals(1, results.size());
-    assertEquals(r2.getChangeId(), Iterables.getOnlyElement(results).changeId);
+    assertThat(results).hasSize(1);
+    assertThat(Iterables.getOnlyElement(results).changeId)
+        .isEqualTo(r2.getChangeId());
   }
 
   @Test
@@ -224,17 +284,18 @@
     PushOneCommit.Result r1 = createChange();
     createChange();
     List<ChangeInfo> results = gApi.changes().query().withStart(1).get();
-    assertEquals(r1.getChangeId(), Iterables.getOnlyElement(results).changeId);
+    assertThat(Iterables.getOnlyElement(results).changeId)
+        .isEqualTo(r1.getChangeId());
   }
 
   @Test
   public void queryChangesNoOptions() throws Exception {
     PushOneCommit.Result r = createChange();
     ChangeInfo result = Iterables.getOnlyElement(query(r.getChangeId()));
-    assertNull(result.labels);
-    assertNull(result.messages);
-    assertNull(result.revisions);
-    assertNull(result.actions);
+    assertThat(result.labels).isNull();
+    assertThat((Iterable<?>)result.messages).isNull();
+    assertThat(result.revisions).isNull();
+    assertThat(result.actions).isNull();
   }
 
   @Test
@@ -244,23 +305,26 @@
         .query(r.getChangeId())
         .withOptions(EnumSet.allOf(ListChangesOption.class))
         .get());
-    assertEquals("Code-Review",
-        Iterables.getOnlyElement(result.labels.keySet()));
-    assertEquals(1, result.messages.size());
-    assertFalse(result.actions.isEmpty());
+    assertThat(Iterables.getOnlyElement(result.labels.keySet()))
+        .isEqualTo("Code-Review");
+    assertThat((Iterable<?>)result.messages).hasSize(1);
+    assertThat(result.actions).isNotEmpty();
 
     RevisionInfo rev = Iterables.getOnlyElement(result.revisions.values());
-    assertEquals(r.getPatchSetId().get(), rev._number);
-    assertFalse(rev.actions.isEmpty());
+    assertThat(rev._number).isEqualTo(r.getPatchSetId().get());
+    assertThat(rev.created).isNotNull();
+    assertThat(rev.uploader._accountId).is(admin.getId().get());
+    assertThat(rev.ref).isEqualTo(r.getPatchSetId().toRefName());
+    assertThat(rev.actions).isNotEmpty();
   }
 
   @Test
   public void queryChangesOwnerWithDifferentUsers() throws Exception {
     PushOneCommit.Result r = createChange();
-    assertEquals(r.getChangeId(),
-        Iterables.getOnlyElement(query("owner:self")).changeId);
+    assertThat(Iterables.getOnlyElement(query("owner:self")).changeId)
+        .isEqualTo(r.getChangeId());
     setApiUser(user);
-    assertTrue(query("owner:self").isEmpty());
+    assertThat(query("owner:self")).isEmpty();
   }
 
   @Test
@@ -273,9 +337,42 @@
         .addReviewer(in);
 
     setApiUser(user);
-    assertNull(get(r.getChangeId()).reviewed);
+    assertThat(get(r.getChangeId()).reviewed).isNull();
 
     revision(r).review(ReviewInput.recommend());
-    assertTrue(get(r.getChangeId()).reviewed);
+    assertThat(get(r.getChangeId()).reviewed).isTrue();
+  }
+
+  @Test
+  public void topic() throws Exception {
+    PushOneCommit.Result r = createChange();
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .topic()).isEqualTo("");
+    gApi.changes()
+        .id(r.getChangeId())
+        .topic("mytopic");
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .topic()).isEqualTo("mytopic");
+    gApi.changes()
+        .id(r.getChangeId())
+        .topic("");
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .topic()).isEqualTo("");
+  }
+
+  @Test
+  public void check() throws Exception {
+    PushOneCommit.Result r = createChange();
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .get()
+        .problems).isNull();
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .get(EnumSet.of(ListChangesOption.CHECK))
+        .problems).isEmpty();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/CheckIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/CheckIT.java
new file mode 100644
index 0000000..475803a
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/CheckIT.java
@@ -0,0 +1,90 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.api.changes.FixInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ProblemInfo;
+import com.google.gerrit.reviewdb.client.Change;
+
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.List;
+
+@NoHttpd
+public class CheckIT extends AbstractDaemonTest {
+  // Most types of tests belong in ConsistencyCheckerTest; these mostly just
+  // test paths outside of ConsistencyChecker, like API wiring.
+  @Test
+  public void currentPatchSetMissing() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change c = getChange(r);
+    db.patchSets().deleteKeys(Collections.singleton(c.currentPatchSetId()));
+    indexer.index(db, c);
+
+    List<ProblemInfo> problems = gApi.changes()
+        .id(r.getChangeId())
+        .check()
+        .problems;
+    assertThat(problems).hasSize(1);
+    assertThat(problems.get(0).message)
+        .isEqualTo("Current patch set 1 not found");
+  }
+
+  @Test
+  public void fixReturnsUpdatedValue() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.approve());
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .submit();
+
+    Change c = getChange(r);
+    c.setStatus(Change.Status.NEW);
+    db.changes().update(Collections.singleton(c));
+    indexer.index(db, c);
+
+    ChangeInfo info = gApi.changes()
+        .id(r.getChangeId())
+        .check();
+    assertThat(info.problems).hasSize(1);
+    assertThat(info.problems.get(0).status).isNull();
+    assertThat(info.status).isEqualTo(ChangeStatus.NEW);
+
+    info = gApi.changes()
+        .id(r.getChangeId())
+        .check(new FixInput());
+    assertThat(info.problems).hasSize(1);
+    assertThat(info.problems.get(0).status).isEqualTo(ProblemInfo.Status.FIXED);
+    assertThat(info.status).isEqualTo(ChangeStatus.MERGED);
+  }
+
+  private Change getChange(PushOneCommit.Result r) throws Exception {
+    return db.changes().get(new Change.Id(
+        gApi.changes().id(r.getChangeId()).get()._number));
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
index a10e026..7de4712 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -14,31 +14,29 @@
 
 package com.google.gerrit.acceptance.api.project;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.junit.Test;
 
-import java.io.IOException;
 import java.util.List;
 
 @NoHttpd
 public class ProjectIT extends AbstractDaemonTest  {
 
   @Test
-  public void createProjectFoo() throws RestApiException {
+  public void createProjectFoo() throws Exception {
     String name = "foo";
-    assertEquals(name,
+    assertThat(name).isEqualTo(
         gApi.projects()
             .name(name)
             .create()
@@ -46,8 +44,19 @@
             .name);
   }
 
+  @Test
+  public void createProjectFooWithGitSuffix() throws Exception {
+    String name = "foo";
+    assertThat(name).isEqualTo(
+        gApi.projects()
+            .name(name + ".git")
+            .create()
+            .get()
+            .name);
+  }
+
   @Test(expected = RestApiException.class)
-  public void createProjectFooBar() throws RestApiException {
+  public void createProjectFooBar() throws Exception {
     ProjectInput in = new ProjectInput();
     in.name = "foo";
     gApi.projects()
@@ -56,7 +65,7 @@
   }
 
   @Test(expected = ResourceConflictException.class)
-  public void createProjectDuplicate() throws RestApiException {
+  public void createProjectDuplicate() throws Exception {
     ProjectInput in = new ProjectInput();
     in.name = "baz";
     gApi.projects()
@@ -68,8 +77,8 @@
   }
 
   @Test
-  public void createBranch() throws GitAPIException,
-      IOException, RestApiException {
+  public void createBranch() throws Exception {
+    allow(Permission.READ, ANONYMOUS_USERS, "refs/*");
     gApi.projects()
         .name(project.get())
         .branch("foo")
@@ -84,36 +93,36 @@
     gApi.projects().name("bar").create();
 
     List<ProjectInfo> allProjects = gApi.projects().list().get();
-    assertEquals(initialProjects.size() + 2, allProjects.size());
+    assertThat(allProjects).hasSize(initialProjects.size() + 2);
 
     List<ProjectInfo> projectsWithDescription = gApi.projects().list()
         .withDescription(true)
         .get();
-    assertNotNull(projectsWithDescription.get(0).description);
+    assertThat(projectsWithDescription.get(0).description).isNotNull();
 
     List<ProjectInfo> projectsWithoutDescription = gApi.projects().list()
         .withDescription(false)
         .get();
-    assertNull(projectsWithoutDescription.get(0).description);
+    assertThat(projectsWithoutDescription.get(0).description).isNull();
 
     List<ProjectInfo> noMatchingProjects = gApi.projects().list()
         .withPrefix("fox")
         .get();
-    assertEquals(0, noMatchingProjects.size());
+    assertThat(noMatchingProjects).isEmpty();
 
     List<ProjectInfo> matchingProject = gApi.projects().list()
         .withPrefix("fo")
         .get();
-    assertEquals(1, matchingProject.size());
+    assertThat(matchingProject).hasSize(1);
 
     List<ProjectInfo> limitOneProject = gApi.projects().list()
         .withLimit(1)
         .get();
-    assertEquals(1, limitOneProject.size());
+    assertThat(limitOneProject).hasSize(1);
 
     List<ProjectInfo> startAtOneProjects = gApi.projects().list()
         .withStart(1)
         .get();
-    assertEquals(allProjects.size() - 1, startAtOneProjects.size());
+    assertThat(startAtOneProjects).hasSize(allProjects.size() - 1);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 9653ffa..9cc8f9c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -14,10 +14,13 @@
 
 package com.google.gerrit.acceptance.api.revision;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
+import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.junit.Assert.fail;
 
+import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -25,18 +28,38 @@
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
+import com.google.gerrit.extensions.api.changes.DraftApi;
+import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.common.DiffInfo;
+import com.google.gerrit.extensions.common.MergeableInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Patch;
 
 import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
 import org.junit.Before;
 import org.junit.Test;
 
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
 
 @NoHttpd
 public class RevisionIT extends AbstractDaemonTest {
@@ -49,8 +72,7 @@
   }
 
   @Test
-  public void reviewTriplet() throws GitAPIException,
-      IOException, RestApiException {
+  public void reviewTriplet() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes()
         .id("p~master~" + r.getChangeId())
@@ -59,8 +81,7 @@
   }
 
   @Test
-  public void reviewCurrent() throws GitAPIException,
-      IOException, RestApiException {
+  public void reviewCurrent() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes()
         .id(r.getChangeId())
@@ -69,8 +90,7 @@
   }
 
   @Test
-  public void reviewNumber() throws GitAPIException,
-      IOException, RestApiException {
+  public void reviewNumber() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes()
         .id(r.getChangeId())
@@ -85,8 +105,7 @@
   }
 
   @Test
-  public void submit() throws GitAPIException,
-      IOException, RestApiException {
+  public void submit() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes()
         .id("p~master~" + r.getChangeId())
@@ -99,8 +118,7 @@
   }
 
   @Test(expected = AuthException.class)
-  public void submitOnBehalfOf() throws GitAPIException,
-      IOException, RestApiException {
+  public void submitOnBehalfOf() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes()
         .id("p~master~" + r.getChangeId())
@@ -116,8 +134,7 @@
   }
 
   @Test
-  public void deleteDraft() throws GitAPIException,
-      IOException, RestApiException {
+  public void deleteDraft() throws Exception {
     PushOneCommit.Result r = createDraft();
     gApi.changes()
         .id(r.getChangeId())
@@ -126,8 +143,106 @@
   }
 
   @Test
-  public void cherryPick() throws GitAPIException,
-      IOException, RestApiException {
+  public void cherryPick() throws Exception {
+    PushOneCommit.Result r = pushTo("refs/for/master%topic=someTopic");
+    CherryPickInput in = new CherryPickInput();
+    in.destination = "foo";
+    in.message = "it goes to stable branch";
+    gApi.projects()
+        .name(project.get())
+        .branch(in.destination)
+        .create(new BranchInput());
+    ChangeApi orig = gApi.changes()
+        .id("p~master~" + r.getChangeId());
+
+    assertThat((Iterable<?>)orig.get().messages).hasSize(1);
+    ChangeApi cherry = orig.revision(r.getCommit().name())
+        .cherryPick(in);
+    assertThat((Iterable<?>)orig.get().messages).hasSize(2);
+
+    String cherryPickedRevision = cherry.get().currentRevision;
+    String expectedMessage = String.format(
+        "Patch Set 1: Cherry Picked\n\n" +
+        "This patchset was cherry picked to branch %s as commit %s",
+        in.destination, cherryPickedRevision);
+
+    Iterator<ChangeMessageInfo> origIt = orig.get().messages.iterator();
+    origIt.next();
+    assertThat(origIt.next().message).isEqualTo(expectedMessage);
+
+    assertThat((Iterable<?>)cherry.get().messages).hasSize(1);
+    Iterator<ChangeMessageInfo> cherryIt = cherry.get().messages.iterator();
+    expectedMessage = "Patch Set 1: Cherry Picked from branch master.";
+    assertThat(cherryIt.next().message).isEqualTo(expectedMessage);
+
+    assertThat(cherry.get().subject).contains(in.message);
+    assertThat(cherry.get().topic).isEqualTo("someTopic");
+    cherry.current().review(ReviewInput.approve());
+    cherry.current().submit();
+  }
+
+  @Test
+  public void cherryPickToSameBranch() throws Exception {
+    PushOneCommit.Result r = createChange();
+    CherryPickInput in = new CherryPickInput();
+    in.destination = "master";
+    in.message = "it generates a new patch set\n\nChange-Id: " + r.getChangeId();
+    ChangeInfo cherryInfo = gApi.changes()
+        .id("p~master~" + r.getChangeId())
+        .revision(r.getCommit().name())
+        .cherryPick(in)
+        .get();
+    assertThat((Iterable<?>)cherryInfo.messages).hasSize(2);
+    Iterator<ChangeMessageInfo> cherryIt = cherryInfo.messages.iterator();
+    assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 1.");
+    assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 2.");
+  }
+
+  @Test
+  public void cherryPickToSameBranchWithRebase() throws Exception {
+    // Push a new change, then merge it
+    PushOneCommit.Result baseChange = createChange();
+    RevisionApi baseRevision =
+        gApi.changes().id("p~master~" + baseChange.getChangeId()).current();
+    baseRevision.review(ReviewInput.approve());
+    baseRevision.submit();
+
+    // Push a new change (change 1)
+    PushOneCommit.Result r1 = createChange();
+
+    // Push another new change (change 2)
+    String subject = "Test change\n\n" +
+        "Change-Id: Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), subject,
+            "another_file.txt", "another content");
+    PushOneCommit.Result r2 = push.to(git, "refs/for/master");
+
+    // Change 2's parent should be change 1
+    assertThat(r2.getCommit().getParents()[0].name())
+      .isEqualTo(r1.getCommit().name());
+
+    // Cherry pick change 2 onto the same branch
+    ChangeApi orig = gApi.changes().id("p~master~" + r2.getChangeId());
+    CherryPickInput in = new CherryPickInput();
+    in.destination = "master";
+    in.message = subject;
+    ChangeApi cherry = orig.revision(r2.getCommit().name()).cherryPick(in);
+    ChangeInfo cherryInfo = cherry.get();
+    assertThat((Iterable<?>)cherryInfo.messages).hasSize(2);
+    Iterator<ChangeMessageInfo> cherryIt = cherryInfo.messages.iterator();
+    assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 1.");
+    assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 2.");
+
+    // Parent of change 2 should now be the change that was merged, i.e.
+    // change 2 is rebased onto the head of the master branch.
+    String newParent = cherryInfo.revisions.get(cherryInfo.currentRevision)
+        .commit.parents.get(0).commit;
+    assertThat(newParent).isEqualTo(baseChange.getCommit().name());
+  }
+
+  @Test
+  public void cherryPickIdenticalTree() throws Exception {
     PushOneCommit.Result r = createChange();
     CherryPickInput in = new CherryPickInput();
     in.destination = "foo";
@@ -139,41 +254,74 @@
     ChangeApi orig = gApi.changes()
         .id("p~master~" + r.getChangeId());
 
-    assertEquals(1, orig.get().messages.size());
+    assertThat((Iterable<?>)orig.get().messages).hasSize(1);
     ChangeApi cherry = orig.revision(r.getCommit().name())
         .cherryPick(in);
-    assertEquals(2, orig.get().messages.size());
+    assertThat((Iterable<?>)orig.get().messages).hasSize(2);
 
-    assertTrue(cherry.get().subject.contains(in.message));
-    cherry.current()
-        .review(ReviewInput.approve());
-    cherry.current()
-        .submit();
+    assertThat(cherry.get().subject).contains(in.message);
+    cherry.current().review(ReviewInput.approve());
+    cherry.current().submit();
+
+    try {
+      orig.revision(r.getCommit().name()).cherryPick(in);
+      fail("Cherry-pick identical tree error expected");
+    } catch (RestApiException e) {
+      assertThat(e.getMessage()).isEqualTo("Cherry pick failed: identical tree");
+    }
   }
 
   @Test
-  public void canRebase()
-      throws GitAPIException, IOException, RestApiException, Exception {
+  public void cherryPickConflict() throws Exception {
+    PushOneCommit.Result r = createChange();
+    CherryPickInput in = new CherryPickInput();
+    in.destination = "foo";
+    in.message = "it goes to stable branch";
+    gApi.projects()
+        .name(project.get())
+        .branch(in.destination)
+        .create(new BranchInput());
+
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+            PushOneCommit.FILE_NAME, "another content");
+    push.to(git, "refs/heads/foo");
+
+    ChangeApi orig = gApi.changes().id("p~master~" + r.getChangeId());
+    assertThat((Iterable<?>)orig.get().messages).hasSize(1);
+
+    try {
+      orig.revision(r.getCommit().name()).cherryPick(in);
+      fail("Cherry-pick merge conflict error expected");
+    } catch (RestApiException e) {
+      assertThat(e.getMessage()).isEqualTo("Cherry pick failed: merge conflict");
+    }
+  }
+
+  @Test
+  public void canRebase() throws Exception {
     PushOneCommit push = pushFactory.create(db, admin.getIdent());
     PushOneCommit.Result r1 = push.to(git, "refs/for/master");
     merge(r1);
 
     push = pushFactory.create(db, admin.getIdent());
     PushOneCommit.Result r2 = push.to(git, "refs/for/master");
-    assertFalse(gApi.changes()
+    boolean canRebase = gApi.changes()
         .id(r2.getChangeId())
         .revision(r2.getCommit().name())
-        .canRebase());
+        .canRebase();
+    assertThat(canRebase).isFalse();
     merge(r2);
 
     git.checkout().setName(r1.getCommit().name()).call();
     push = pushFactory.create(db, admin.getIdent());
     PushOneCommit.Result r3 = push.to(git, "refs/for/master");
 
-    assertTrue(gApi.changes()
+    canRebase = gApi.changes()
         .id(r3.getChangeId())
         .revision(r3.getCommit().name())
-        .canRebase());
+        .canRebase();
+    assertThat(canRebase).isTrue();
   }
 
   @Test
@@ -186,30 +334,185 @@
         .current()
         .setReviewed(PushOneCommit.FILE_NAME, true);
 
-    assertEquals(PushOneCommit.FILE_NAME,
-        Iterables.getOnlyElement(
+    assertThat(Iterables.getOnlyElement(
             gApi.changes()
                 .id(r.getChangeId())
                 .current()
-                .reviewed()));
+                .reviewed())).isEqualTo(PushOneCommit.FILE_NAME);
 
     gApi.changes()
         .id(r.getChangeId())
         .current()
         .setReviewed(PushOneCommit.FILE_NAME, false);
 
-    assertTrue(
-        gApi.changes()
-            .id(r.getChangeId())
-            .current()
-            .reviewed()
-            .isEmpty());
+    assertThat((Iterable<?>)gApi.changes().id(r.getChangeId()).current().reviewed())
+        .isEmpty();
   }
 
-  protected RevisionApi revision(PushOneCommit.Result r) throws Exception {
-    return gApi.changes()
+  @Test
+  public void mergeable() throws Exception {
+    ObjectId initial = git.getRepository().getRef(HEAD).getLeaf().getObjectId();
+
+    PushOneCommit push1 =
+        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+            PushOneCommit.FILE_NAME, "push 1 content");
+
+    PushOneCommit.Result r1 = push1.to(git, "refs/for/master");
+    assertMergeable(r1.getChangeId(), true);
+    merge(r1);
+
+    // Reset HEAD to initial so the new change is a merge conflict.
+    RefUpdate ru = git.getRepository().updateRef(HEAD);
+    ru.setNewObjectId(initial);
+    assertThat(ru.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
+
+    PushOneCommit push2 =
+        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+            PushOneCommit.FILE_NAME, "push 2 content");
+    PushOneCommit.Result r2 = push2.to(git, "refs/for/master");
+    assertMergeable(r2.getChangeId(), false);
+    // TODO(dborowitz): Test for other-branches.
+  }
+
+  @Test
+  public void files() throws Exception {
+    PushOneCommit.Result r = createChange();
+    assertThat(Iterables.all(gApi.changes()
         .id(r.getChangeId())
-        .current();
+        .revision(r.getCommit().name())
+        .files()
+        .keySet(), new Predicate<String>() {
+            @Override
+            public boolean apply(String file) {
+              return file.matches(FILE_NAME + '|' + Patch.COMMIT_MSG);
+            }
+         }))
+      .isTrue();
+  }
+
+  @Test
+  public void diff() throws Exception {
+    PushOneCommit.Result r = createChange();
+    DiffInfo diff = gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .file(FILE_NAME)
+        .diff();
+    assertThat(diff.metaA).isNull();
+    assertThat(diff.metaB.lines).isEqualTo(1);
+  }
+
+  @Test
+  public void content() throws Exception {
+    PushOneCommit.Result r = createChange();
+    BinaryResult bin = gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .file(FILE_NAME)
+        .content();
+    ByteArrayOutputStream os = new ByteArrayOutputStream();
+    bin.writeTo(os);
+    String res = new String(os.toByteArray(), StandardCharsets.UTF_8);
+    assertThat(res).isEqualTo(FILE_CONTENT);
+  }
+
+  private void assertMergeable(String id, boolean expected) throws Exception {
+    MergeableInfo m = gApi.changes().id(id).current().mergeable();
+    assertThat(m.mergeable).isEqualTo(expected);
+    assertThat(m.submitType).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
+    assertThat(m.mergeableInto).isNull();
+    ChangeInfo c = gApi.changes().id(id).info();
+    assertThat(c.mergeable).isEqualTo(expected);
+  }
+
+  @Test
+  public void drafts() throws Exception {
+    PushOneCommit.Result r = createChange();
+    DraftInput in = new DraftInput();
+    in.line = 1;
+    in.message = "nit: trailing whitespace";
+    in.path = FILE_NAME;
+
+    DraftApi draftApi = gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .createDraft(in);
+    assertThat(draftApi
+        .get()
+        .message)
+      .isEqualTo(in.message);
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .draft(draftApi.get().id)
+        .get()
+        .message)
+      .isEqualTo(in.message);
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .drafts())
+      .hasSize(1);
+
+    in.message = "good catch!";
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .draft(draftApi.get().id)
+        .update(in)
+        .message)
+      .isEqualTo(in.message);
+
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .draft(draftApi.get().id)
+        .get()
+        .author
+        .email)
+      .isEqualTo(admin.email);
+
+    draftApi.delete();
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .drafts())
+      .isEmpty();
+  }
+
+  @Test
+  public void comments() throws Exception {
+    PushOneCommit.Result r = createChange();
+    CommentInput in = new CommentInput();
+    in.line = 1;
+    in.message = "nit: trailing whitespace";
+    in.path = FILE_NAME;
+    ReviewInput reviewInput = new ReviewInput();
+    Map<String, List<CommentInput>> comments = new HashMap<>();
+    comments.put(FILE_NAME, Collections.singletonList(in));
+    reviewInput.comments = comments;
+    reviewInput.message = "comment test";
+    gApi.changes()
+       .id(r.getChangeId())
+       .current()
+       .review(reviewInput);
+
+    Map<String, List<CommentInfo>> out = gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .comments();
+    assertThat(out).hasSize(1);
+    CommentInfo comment = Iterables.getOnlyElement(out.get(FILE_NAME));
+    assertThat(comment.message).isEqualTo(in.message);
+    assertThat(comment.author.email).isEqualTo(admin.email);
+
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .comment(comment.id)
+        .get()
+        .message)
+      .isEqualTo(in.message);
   }
 
   private void merge(PushOneCommit.Result r) throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUCK
new file mode 100644
index 0000000..be6fcdc
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/BUCK
@@ -0,0 +1,10 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+  srcs = ['ChangeEditIT.java'],
+  labels = ['edit'],
+  deps = [
+    '//lib/commons:codec',
+    '//lib/joda:joda-time',
+  ],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
new file mode 100644
index 0000000..f16c677
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -0,0 +1,745 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.edit;
+
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.cloneProject;
+import static com.google.gerrit.acceptance.GitUtil.createProject;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.apache.http.HttpStatus.SC_CONFLICT;
+import static org.apache.http.HttpStatus.SC_NOT_FOUND;
+import static org.apache.http.HttpStatus.SC_NO_CONTENT;
+import static org.apache.http.HttpStatus.SC_OK;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ApprovalInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.EditInfo;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.ChangeEdits.EditMessage;
+import com.google.gerrit.server.change.ChangeEdits.Post;
+import com.google.gerrit.server.change.ChangeEdits.Put;
+import com.google.gerrit.server.change.FileContentUtil;
+import com.google.gerrit.server.edit.ChangeEdit;
+import com.google.gerrit.server.edit.ChangeEditModifier;
+import com.google.gerrit.server.edit.ChangeEditUtil;
+import com.google.gerrit.server.edit.UnchangedCommitMessageException;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gson.stream.JsonReader;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import org.apache.commons.codec.binary.StringUtils;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeUtils;
+import org.joda.time.DateTimeUtils.MillisProvider;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class ChangeEditIT extends AbstractDaemonTest {
+
+  private static final String FILE_NAME = "foo";
+  private static final String FILE_NAME2 = "foo2";
+  private static final String FILE_NAME3 = "foo3";
+  private static final byte[] CONTENT_OLD = "bar".getBytes(UTF_8);
+  private static final byte[] CONTENT_NEW = "baz".getBytes(UTF_8);
+  private static final String CONTENT_NEW2_STR = "quxÄÜÖßµ";
+  private static final byte[] CONTENT_NEW2 = CONTENT_NEW2_STR.getBytes(UTF_8);
+
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  @Inject
+  ChangeEditUtil editUtil;
+
+  @Inject
+  private ChangeEditModifier modifier;
+
+  @Inject
+  private FileContentUtil fileUtil;
+
+  private Change change;
+  private String changeId;
+  private Change change2;
+  private String changeId2;
+  private PatchSet ps;
+  private PatchSet ps2;
+
+  @Before
+  public void setUp() throws Exception {
+    db = reviewDbProvider.open();
+    changeId = newChange(git, admin.getIdent());
+    ps = getCurrentPatchSet(changeId);
+    amendChange(git, admin.getIdent(), changeId);
+    change = getChange(changeId);
+    assertThat(ps).isNotNull();
+    changeId2 = newChange2(git, admin.getIdent());
+    change2 = getChange(changeId2);
+    assertThat(change2).isNotNull();
+    ps2 = getCurrentPatchSet(changeId2);
+    assertThat(ps2).isNotNull();
+    final long clockStepMs = MILLISECONDS.convert(1, SECONDS);
+    final AtomicLong clockMs = new AtomicLong(
+        new DateTime(2009, 9, 30, 17, 0, 0).getMillis());
+    DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
+      @Override
+      public long getMillis() {
+        return clockMs.getAndAdd(clockStepMs);
+      }
+    });
+  }
+
+  @After
+  public void cleanup() {
+    DateTimeUtils.setCurrentMillisSystem();
+    db.close();
+  }
+
+  @Test
+  public void deleteEdit() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    assertThat(
+        modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
+            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+    editUtil.delete(editUtil.byChange(change).get());
+    assertThat(editUtil.byChange(change).isPresent()).isFalse();
+  }
+
+  @Test
+  public void publishEdit() throws Exception {
+    assertThat(modifier.createEdit(change, getCurrentPatchSet(changeId)))
+        .isEqualTo(RefUpdate.Result.NEW);
+    assertThat(
+        modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
+            RestSession.newRawInput(CONTENT_NEW2))).isEqualTo(RefUpdate.Result.FORCED);
+    editUtil.publish(editUtil.byChange(change).get());
+    assertThat(editUtil.byChange(change).isPresent()).isFalse();
+  }
+
+  @Test
+  public void publishEditRest() throws Exception {
+    PatchSet oldCurrentPatchSet = getCurrentPatchSet(changeId);
+    assertThat(modifier.createEdit(change, oldCurrentPatchSet)).isEqualTo(
+        RefUpdate.Result.NEW);
+    assertThat(
+        modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
+            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    RestResponse r = adminSession.post(urlPublish());
+    assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+    edit = editUtil.byChange(change);
+    assertThat(edit.isPresent()).isFalse();
+    PatchSet newCurrentPatchSet = getCurrentPatchSet(changeId);
+    assertThat(newCurrentPatchSet.getId()).isNotEqualTo(oldCurrentPatchSet.getId());
+  }
+
+  @Test
+  public void deleteEditRest() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    assertThat(
+        modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
+            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    RestResponse r = adminSession.delete(urlEdit());
+    assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+    edit = editUtil.byChange(change);
+    assertThat(edit.isPresent()).isFalse();
+  }
+
+  @Test
+  public void rebaseEdit() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    assertThat(
+        modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
+            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+    ChangeEdit edit = editUtil.byChange(change).get();
+    PatchSet current = getCurrentPatchSet(changeId);
+    assertThat(edit.getBasePatchSet().getPatchSetId()).isEqualTo(
+        current.getPatchSetId() - 1);
+    Date beforeRebase = edit.getEditCommit().getCommitterIdent().getWhen();
+    modifier.rebaseEdit(edit, current);
+    edit = editUtil.byChange(change).get();
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.getChange().getProject()),
+        ObjectId.fromString(edit.getRevision().get()), FILE_NAME), CONTENT_NEW);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.getChange().getProject()),
+        ObjectId.fromString(edit.getRevision().get()), FILE_NAME2), CONTENT_NEW2);
+    assertThat(edit.getBasePatchSet().getPatchSetId()).isEqualTo(
+        current.getPatchSetId());
+    Date afterRebase = edit.getEditCommit().getCommitterIdent().getWhen();
+    assertThat(beforeRebase.equals(afterRebase)).isFalse();
+  }
+
+  @Test
+  public void rebaseEditRest() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    assertThat(
+        modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
+            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+    ChangeEdit edit = editUtil.byChange(change).get();
+    PatchSet current = getCurrentPatchSet(changeId);
+    assertThat(edit.getBasePatchSet().getPatchSetId()).isEqualTo(
+        current.getPatchSetId() - 1);
+    Date beforeRebase = edit.getEditCommit().getCommitterIdent().getWhen();
+    RestResponse r = adminSession.post(urlRebase());
+    assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+    edit = editUtil.byChange(change).get();
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.getChange().getProject()),
+        ObjectId.fromString(edit.getRevision().get()), FILE_NAME), CONTENT_NEW);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.getChange().getProject()),
+        ObjectId.fromString(edit.getRevision().get()), FILE_NAME2), CONTENT_NEW2);
+    assertThat(edit.getBasePatchSet().getPatchSetId()).isEqualTo(
+        current.getPatchSetId());
+    Date afterRebase = edit.getEditCommit().getCommitterIdent().getWhen();
+    assertThat(afterRebase).isNotEqualTo(beforeRebase);
+  }
+
+  @Test
+  public void rebaseEditWithConflictsRest_Conflict() throws Exception {
+    PatchSet current = getCurrentPatchSet(changeId2);
+    assertThat(modifier.createEdit(change2, current)).isEqualTo(RefUpdate.Result.NEW);
+    assertThat(
+        modifier.modifyFile(editUtil.byChange(change2).get(), FILE_NAME,
+            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+    ChangeEdit edit = editUtil.byChange(change2).get();
+    assertThat(edit.getBasePatchSet().getPatchSetId()).isEqualTo(
+        current.getPatchSetId());
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT, FILE_NAME,
+            new String(CONTENT_NEW2), changeId2);
+    push.to(git, "refs/for/master").assertOkStatus();
+    RestResponse r = adminSession.post(urlRebase());
+    assertThat(r.getStatusCode()).isEqualTo(SC_CONFLICT);
+  }
+
+  @Test
+  public void updateExistingFile() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RestSession.newRawInput(CONTENT_NEW)))
+        .isEqualTo(RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW);
+    editUtil.delete(edit.get());
+    edit = editUtil.byChange(change);
+    assertThat(edit.isPresent()).isFalse();
+  }
+
+  @Test
+  public void updateRootCommitMessage() throws Exception {
+    createProject(sshSession, "root-msg-test", null, false);
+    git = cloneProject(sshSession.getUrl() + "/root-msg-test");
+    changeId = newChange(git, admin.getIdent());
+    change = getChange(changeId);
+
+    assertThat(modifier.createEdit(change, getCurrentPatchSet(changeId)))
+        .isEqualTo(RefUpdate.Result.NEW);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertThat(edit.get().getEditCommit().getParentCount()).isEqualTo(0);
+
+    String msg = String.format("New commit message\n\nChange-Id: %s",
+        change.getKey());
+    assertThat(modifier.modifyMessage(edit.get(), msg))
+        .isEqualTo(RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change);
+    assertThat(edit.get().getEditCommit().getFullMessage()).isEqualTo(msg);
+  }
+
+  @Test
+  public void updateMessage() throws Exception {
+    assertThat(modifier.createEdit(change, getCurrentPatchSet(changeId)))
+        .isEqualTo(RefUpdate.Result.NEW);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+
+    try {
+      modifier.modifyMessage(
+          edit.get(),
+          edit.get().getEditCommit().getFullMessage());
+      fail("UnchangedCommitMessageException expected");
+    } catch (UnchangedCommitMessageException ex) {
+      assertThat(ex.getMessage()).isEqualTo(
+          "New commit message cannot be same as existing commit message");
+    }
+
+    String msg = String.format("New commit message\n\nChange-Id: %s",
+        change.getKey());
+    assertThat(modifier.modifyMessage(edit.get(), msg)).isEqualTo(
+        RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change);
+    assertThat(edit.get().getEditCommit().getFullMessage()).isEqualTo(msg);
+
+    editUtil.publish(edit.get());
+    assertThat(editUtil.byChange(change).isPresent()).isFalse();
+
+    ChangeInfo info = get(changeId, ListChangesOption.CURRENT_COMMIT,
+        ListChangesOption.CURRENT_REVISION);
+    assertThat(info.revisions.get(info.currentRevision).commit.message)
+        .isEqualTo(msg);
+  }
+
+  @Test
+  public void updateMessageRest() throws Exception {
+    assertThat(adminSession.get(urlEditMessage()).getStatusCode())
+        .isEqualTo(SC_NOT_FOUND);
+    EditMessage.Input in = new EditMessage.Input();
+    in.message = String.format("New commit message\n\n" +
+        CONTENT_NEW2_STR + "\n\nChange-Id: %s",
+        change.getKey());
+    assertThat(adminSession.put(urlEditMessage(), in).getStatusCode())
+        .isEqualTo(SC_NO_CONTENT);
+    RestResponse r = adminSession.getJsonAccept(urlEditMessage());
+    assertThat(r.getStatusCode()).isEqualTo(SC_OK);
+    assertThat(readContentFromJson(r)).isEqualTo(in.message);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertThat(edit.get().getEditCommit().getFullMessage())
+        .isEqualTo(in.message);
+    in.message = String.format("New commit message2\n\nChange-Id: %s",
+        change.getKey());
+    assertThat(adminSession.put(urlEditMessage(), in).getStatusCode())
+        .isEqualTo(SC_NO_CONTENT);
+    edit = editUtil.byChange(change);
+    assertThat(edit.get().getEditCommit().getFullMessage())
+        .isEqualTo(in.message);
+  }
+
+  @Test
+  public void retrieveEdit() throws Exception {
+    RestResponse r = adminSession.get(urlEdit());
+    assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RestSession.newRawInput(CONTENT_NEW)))
+        .isEqualTo(RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change);
+    EditInfo info = toEditInfo(false);
+    assertThat(info.commit.commit).isEqualTo(edit.get().getRevision().get());
+    assertThat(info.commit.parents).hasSize(1);
+
+    edit = editUtil.byChange(change);
+    editUtil.delete(edit.get());
+
+    r = adminSession.get(urlEdit());
+    assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+  }
+
+  @Test
+  public void retrieveFilesInEdit() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RestSession.newRawInput(CONTENT_NEW)))
+        .isEqualTo(RefUpdate.Result.FORCED);
+
+    EditInfo info = toEditInfo(true);
+    assertThat(info.files).hasSize(2);
+    List<String> l = Lists.newArrayList(info.files.keySet());
+    assertThat(l.get(0)).isEqualTo("/COMMIT_MSG");
+    assertThat(l.get(1)).isEqualTo("foo");
+  }
+
+  @Test
+  public void deleteExistingFile() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertThat(modifier.deleteFile(edit.get(), FILE_NAME)).isEqualTo(
+        RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change);
+    try {
+      fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+          ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
+      fail("ResourceNotFoundException expected");
+    } catch (ResourceNotFoundException rnfe) {
+    }
+  }
+
+  @Test
+  public void renameExistingFile() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertThat(modifier.renameFile(edit.get(), FILE_NAME, FILE_NAME3))
+        .isEqualTo(RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME3), CONTENT_OLD);
+    try {
+      fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+          ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
+      fail("ResourceNotFoundException expected");
+    } catch (ResourceNotFoundException rnfe) {
+    }
+  }
+
+  @Test
+  public void createEditByDeletingExistingFileRest() throws Exception {
+    RestResponse r = adminSession.delete(urlEditFile());
+    assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    try {
+      fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+          ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
+      fail("ResourceNotFoundException expected");
+    } catch (ResourceNotFoundException rnfe) {
+    }
+  }
+
+  @Test
+  public void deletingNonExistingEditRest() throws Exception {
+    RestResponse r = adminSession.delete(urlEdit());
+    assertThat(r.getStatusCode()).isEqualTo(SC_NOT_FOUND);
+  }
+
+  @Test
+  public void deleteExistingFileRest() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    assertThat(adminSession.delete(urlEditFile()).getStatusCode()).isEqualTo(
+        SC_NO_CONTENT);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    try {
+      fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+          ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
+      fail("ResourceNotFoundException expected");
+    } catch (ResourceNotFoundException rnfe) {
+    }
+  }
+
+  @Test
+  public void restoreDeletedFileInPatchSet() throws Exception {
+    assertThat(modifier.createEdit(change2, ps2)).isEqualTo(
+        RefUpdate.Result.NEW);
+    Optional<ChangeEdit> edit = editUtil.byChange(change2);
+    assertThat(modifier.restoreFile(edit.get(), FILE_NAME)).isEqualTo(
+        RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change2);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD);
+  }
+
+  @Test
+  public void renameFileRest() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    Post.Input in = new Post.Input();
+    in.oldPath = FILE_NAME;
+    in.newPath = FILE_NAME3;
+    assertThat(adminSession.post(urlEdit(), in).getStatusCode()).isEqualTo(
+        SC_NO_CONTENT);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME3), CONTENT_OLD);
+    try {
+      fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+          ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
+      fail("ResourceNotFoundException expected");
+    } catch (ResourceNotFoundException rnfe) {
+    }
+  }
+
+  @Test
+  public void restoreDeletedFileInPatchSetRest() throws Exception {
+    Post.Input in = new Post.Input();
+    in.restorePath = FILE_NAME;
+    assertThat(adminSession.post(urlEdit2(), in).getStatusCode()).isEqualTo(
+        SC_NO_CONTENT);
+    Optional<ChangeEdit> edit = editUtil.byChange(change2);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD);
+  }
+
+  @Test
+  public void amendExistingFile() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RestSession.newRawInput(CONTENT_NEW)))
+        .isEqualTo(RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW);
+    assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RestSession.newRawInput(CONTENT_NEW2)))
+        .isEqualTo(RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW2);
+  }
+
+  @Test
+  public void createAndChangeEditInOneRequestRest() throws Exception {
+    Put.Input in = new Put.Input();
+    in.content = RestSession.newRawInput(CONTENT_NEW);
+    assertThat(adminSession.putRaw(urlEditFile(), in.content).getStatusCode())
+        .isEqualTo(SC_NO_CONTENT);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW);
+    in.content = RestSession.newRawInput(CONTENT_NEW2);
+    assertThat(adminSession.putRaw(urlEditFile(), in.content).getStatusCode())
+        .isEqualTo(SC_NO_CONTENT);
+    edit = editUtil.byChange(change);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW2);
+  }
+
+  @Test
+  public void changeEditRest() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    Put.Input in = new Put.Input();
+    in.content = RestSession.newRawInput(CONTENT_NEW);
+    assertThat(adminSession.putRaw(urlEditFile(), in.content).getStatusCode())
+        .isEqualTo(SC_NO_CONTENT);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW);
+  }
+
+  @Test
+  public void emptyPutRequest() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    assertThat(adminSession.put(urlEditFile()).getStatusCode()).isEqualTo(
+        SC_NO_CONTENT);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), "".getBytes());
+  }
+
+  @Test
+  public void createEmptyEditRest() throws Exception {
+    assertThat(adminSession.post(urlEdit()).getStatusCode()).isEqualTo(
+        SC_NO_CONTENT);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD);
+  }
+
+  @Test
+  public void getFileContentRest() throws Exception {
+    Put.Input in = new Put.Input();
+    in.content = RestSession.newRawInput(CONTENT_NEW);
+    assertThat(adminSession.putRaw(urlEditFile(), in.content).getStatusCode())
+        .isEqualTo(SC_NO_CONTENT);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertThat(modifier.modifyFile(edit.get(), FILE_NAME, RestSession.newRawInput(CONTENT_NEW2)))
+        .isEqualTo(RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change);
+    RestResponse r = adminSession.getJsonAccept(urlEditFile());
+    assertThat(r.getStatusCode()).isEqualTo(SC_OK);
+    assertThat(readContentFromJson(r)).isEqualTo(
+        StringUtils.newStringUtf8(CONTENT_NEW2));
+  }
+
+  @Test
+  public void getFileNotFoundRest() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    assertThat(adminSession.delete(urlEditFile()).getStatusCode()).isEqualTo(
+        SC_NO_CONTENT);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    try {
+      fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+          ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME);
+      fail("ResourceNotFoundException expected");
+    } catch (ResourceNotFoundException rnfe) {
+    }
+    RestResponse r = adminSession.get(urlEditFile());
+    assertThat(r.getStatusCode()).isEqualTo(SC_NO_CONTENT);
+  }
+
+  @Test
+  public void addNewFile() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertThat(modifier.modifyFile(edit.get(), FILE_NAME2, RestSession.newRawInput(CONTENT_NEW)))
+        .isEqualTo(RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME2), CONTENT_NEW);
+  }
+
+  @Test
+  public void addNewFileAndAmend() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertThat(modifier.modifyFile(edit.get(), FILE_NAME2, RestSession.newRawInput(CONTENT_NEW)))
+        .isEqualTo(RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME2), CONTENT_NEW);
+    assertThat(modifier.modifyFile(edit.get(), FILE_NAME2, RestSession.newRawInput(CONTENT_NEW2)))
+        .isEqualTo(RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME2), CONTENT_NEW2);
+  }
+
+  @Test
+  public void writeNoChanges() throws Exception {
+    assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
+    try {
+      modifier.modifyFile(
+          editUtil.byChange(change).get(),
+          FILE_NAME,
+          RestSession.newRawInput(CONTENT_OLD));
+      fail();
+    } catch (InvalidChangeOperationException e) {
+      assertThat(e.getMessage()).isEqualTo("no changes were made");
+    }
+  }
+
+  @Test
+  public void editCommitMessageCopiesLabelScores() throws Exception {
+    String cr = "Code-Review";
+    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+    cfg.getLabelSections().get(cr)
+        .setCopyAllScoresIfNoCodeChange(true);
+    saveProjectConfig(allProjects, cfg);
+
+    String changeId = change.getKey().get();
+    ReviewInput r = new ReviewInput();
+    r.labels = ImmutableMap.<String, Short> of(cr, (short) 1);
+    gApi.changes()
+        .id(changeId)
+        .revision(change.currentPatchSetId().get())
+        .review(r);
+
+    assertThat(modifier.createEdit(change, getCurrentPatchSet(changeId)))
+        .isEqualTo(RefUpdate.Result.NEW);
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    String newSubj = "New commit message";
+    String newMsg = newSubj + "\n\nChange-Id: " + changeId + "\n";
+    assertThat(modifier.modifyMessage(edit.get(), newMsg))
+        .isEqualTo(RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change);
+    editUtil.publish(edit.get());
+
+    ChangeInfo info = get(changeId);
+    assertThat(info.subject).isEqualTo(newSubj);
+    List<ApprovalInfo> approvals = info.labels.get(cr).all;
+    assertThat(approvals).hasSize(1);
+    assertThat(approvals.get(0).value).isEqualTo(1);
+  }
+
+  private String newChange(Git git, PersonIdent ident) throws Exception {
+    PushOneCommit push =
+        pushFactory.create(db, ident, PushOneCommit.SUBJECT, FILE_NAME,
+            new String(CONTENT_OLD));
+    return push.to(git, "refs/for/master").getChangeId();
+  }
+
+  private String amendChange(Git git, PersonIdent ident, String changeId) throws Exception {
+    PushOneCommit push =
+        pushFactory.create(db, ident, PushOneCommit.SUBJECT, FILE_NAME2,
+            new String(CONTENT_NEW2), changeId);
+    return push.to(git, "refs/for/master").getChangeId();
+  }
+
+  private String newChange2(Git git, PersonIdent ident) throws Exception {
+    PushOneCommit push =
+        pushFactory.create(db, ident, PushOneCommit.SUBJECT, FILE_NAME,
+            new String(CONTENT_OLD));
+    return push.rm(git, "refs/for/master").getChangeId();
+  }
+
+  private Change getChange(String changeId) throws Exception {
+    return getOnlyElement(queryProvider.get().byKeyPrefix(changeId)).change();
+  }
+
+  private PatchSet getCurrentPatchSet(String changeId) throws Exception {
+    return db.patchSets()
+        .get(getChange(changeId).currentPatchSetId());
+  }
+
+  private static void assertByteArray(BinaryResult result, byte[] expected)
+        throws Exception {
+    ByteArrayOutputStream os = new ByteArrayOutputStream();
+    result.writeTo(os);
+    assertThat(os.toByteArray()).isEqualTo(expected);
+  }
+
+  private String urlEdit() {
+    return "/changes/"
+        + change.getChangeId()
+        + "/edit";
+  }
+
+  private String urlEdit2() {
+    return "/changes/"
+        + change2.getChangeId()
+        + "/edit/";
+  }
+
+  private String urlEditMessage() {
+    return "/changes/"
+        + change.getChangeId()
+        + "/edit:message";
+  }
+
+  private String urlEditFile() {
+    return urlEdit()
+        + "/"
+        + FILE_NAME;
+  }
+
+  private String urlGetFiles() {
+    return urlEdit()
+        + "?list";
+  }
+
+  private String urlPublish() {
+    return "/changes/"
+        + change.getChangeId()
+        + "/edit:publish";
+  }
+
+  private String urlRebase() {
+    return "/changes/"
+        + change.getChangeId()
+        + "/edit:rebase";
+  }
+
+  private EditInfo toEditInfo(boolean files) throws IOException {
+    RestResponse r = adminSession.get(files ? urlGetFiles() : urlEdit());
+    assertThat(r.getStatusCode()).isEqualTo(SC_OK);
+    return newGson().fromJson(r.getReader(), EditInfo.class);
+  }
+
+  private String readContentFromJson(RestResponse r) throws IOException {
+    JsonReader jsonReader = new JsonReader(r.getReader());
+    jsonReader.setLenient(true);
+    return newGson().fromJson(jsonReader, String.class);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 5b3795e..e65d51e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -14,27 +14,43 @@
 
 package com.google.gerrit.acceptance.git;
 
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static org.junit.Assert.assertEquals;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
 
 import com.jcraft.jsch.JSchException;
 
 import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
+import java.util.Set;
 
 public abstract class AbstractPushForReview extends AbstractDaemonTest {
+  @ConfigSuite.Config
+  public static Config noteDbEnabled() {
+    return NotesMigration.allEnabledConfig();
+  }
+
+  @Inject
+  private NotesMigration notesMigration;
+
   protected enum Protocol {
     SSH, HTTP
   }
@@ -148,15 +164,30 @@
   }
 
   @Test
+  public void testPushForMasterAsEdit() throws GitAPIException,
+      IOException, RestApiException {
+    PushOneCommit.Result r = pushTo("refs/for/master");
+    r.assertOkStatus();
+    EditInfo edit = getEdit(r.getChangeId());
+    assertThat(edit).isNull();
+
+    // specify edit as option
+    r = amendChange(r.getChangeId(), "refs/for/master%edit");
+    r.assertOkStatus();
+    edit = getEdit(r.getChangeId());
+    assertThat(edit).isNotNull();
+  }
+
+  @Test
   public void testPushForMasterWithApprovals() throws GitAPIException,
       IOException, RestApiException {
     PushOneCommit.Result r = pushTo("refs/for/master/%l=Code-Review");
     r.assertOkStatus();
     ChangeInfo ci = get(r.getChangeId());
     LabelInfo cr = ci.labels.get("Code-Review");
-    assertEquals(1, cr.all.size());
-    assertEquals("Administrator", cr.all.get(0).name);
-    assertEquals(1, cr.all.get(0).value.intValue());
+    assertThat(cr.all).hasSize(1);
+    assertThat(cr.all.get(0).name).isEqualTo("Administrator");
+    assertThat(cr.all.get(0).value.intValue()).is(1);
 
     PushOneCommit push =
         pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
@@ -165,9 +196,21 @@
 
     ci = get(r.getChangeId());
     cr = ci.labels.get("Code-Review");
-    assertEquals(1, cr.all.size());
-    assertEquals("Administrator", cr.all.get(0).name);
-    assertEquals(2, cr.all.get(0).value.intValue());
+    assertThat(cr.all).hasSize(1);
+    assertThat(cr.all.get(0).name).isEqualTo("Administrator");
+    assertThat(cr.all.get(0).value.intValue()).is(2);
+  }
+
+  @Test
+  public void testPushNewPatchsetToRefsChanges() throws GitAPIException,
+    IOException, OrmException {
+    PushOneCommit.Result r = pushTo("refs/for/master");
+    r.assertOkStatus();
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+            "b.txt", "anotherContent", r.getChangeId());
+    r = push.to(git, "refs/changes/" + r.getChange().change().getId().get());
+    r.assertOkStatus();
   }
 
   @Test
@@ -179,22 +222,87 @@
 
   @Test
   public void testPushForMasterWithApprovals_ValueOutOfRange() throws GitAPIException,
-      IOException, RestApiException {
+      IOException {
     PushOneCommit.Result r = pushTo("refs/for/master/%l=Code-Review-3");
     r.assertErrorStatus("label \"Code-Review\": -3 is not a valid value");
   }
 
   @Test
   public void testPushForNonExistingBranch() throws GitAPIException,
-      OrmException, IOException {
+      IOException {
     String branchName = "non-existing";
     PushOneCommit.Result r = pushTo("refs/for/" + branchName);
     r.assertErrorStatus("branch " + branchName + " not found");
   }
 
-  private PushOneCommit.Result pushTo(String ref) throws GitAPIException,
+  @Test
+  public void testPushForMasterWithHashtags() throws GitAPIException,
+      OrmException, IOException, RestApiException {
+
+    // Hashtags currently only work when noteDB is enabled
+    assume().that(notesMigration.enabled()).isTrue();
+
+    // specify a single hashtag as option
+    String hashtag1 = "tag1";
+    Set<String> expected = ImmutableSet.of(hashtag1);
+    PushOneCommit.Result r = pushTo("refs/for/master%hashtag=#" + hashtag1);
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, null);
+
+    Set<String> hashtags = gApi.changes().id(r.getChangeId()).getHashtags();
+    assertThat((Iterable<?>)hashtags).containsExactlyElementsIn(expected);
+
+    // specify a single hashtag as option in new patch set
+    String hashtag2 = "tag2";
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+            "b.txt", "anotherContent", r.getChangeId());
+    r = push.to(git, "refs/for/master/%hashtag=" + hashtag2);
+    r.assertOkStatus();
+    expected = ImmutableSet.of(hashtag1, hashtag2);
+    hashtags = gApi.changes().id(r.getChangeId()).getHashtags();
+    assertThat((Iterable<?>)hashtags).containsExactlyElementsIn(expected);
+  }
+
+  @Test
+  public void testPushForMasterWithMultipleHashtags() throws GitAPIException,
+      OrmException, IOException, RestApiException {
+
+    // Hashtags currently only work when noteDB is enabled
+    assume().that(notesMigration.enabled()).isTrue();
+
+    // specify multiple hashtags as options
+    String hashtag1 = "tag1";
+    String hashtag2 = "tag2";
+    Set<String> expected = ImmutableSet.of(hashtag1, hashtag2);
+    PushOneCommit.Result r = pushTo("refs/for/master%hashtag=#" + hashtag1
+        + ",hashtag=##" + hashtag2);
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, null);
+
+    Set<String> hashtags = gApi.changes().id(r.getChangeId()).getHashtags();
+    assertThat((Iterable<?>)hashtags).containsExactlyElementsIn(expected);
+
+    // specify multiple hashtags as options in new patch set
+    String hashtag3 = "tag3";
+    String hashtag4 = "tag4";
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+            "b.txt", "anotherContent", r.getChangeId());
+    r = push.to(git,
+        "refs/for/master%hashtag=" + hashtag3 + ",hashtag=" + hashtag4);
+    r.assertOkStatus();
+    expected = ImmutableSet.of(hashtag1, hashtag2, hashtag3, hashtag4);
+    hashtags = gApi.changes().id(r.getChangeId()).getHashtags();
+    assertThat((Iterable<?>)hashtags).containsExactlyElementsIn(expected);
+  }
+
+  @Test
+  public void testPushForMasterWithHashtagsNoteDbDisabled() throws GitAPIException,
       IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, ref);
+    // push with hashtags should fail when noteDb is disabled
+    assume().that(notesMigration.enabled()).isFalse();
+    PushOneCommit.Result r = pushTo("refs/for/master%hashtag=tag1");
+    r.assertErrorStatus("cannot add hashtags; noteDb is disabled");
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK
index 3ead2a1..f36c447 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK
@@ -1,7 +1,12 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
-  srcs = ['DraftChangeBlockedIT.java', 'SubmitOnPushIT.java'],
+  srcs = [
+    'DraftChangeBlockedIT.java',
+    'ForcePushIT.java',
+    'SubmitOnPushIT.java',
+    'VisibleRefFilterIT.java',
+  ],
   labels = ['git'],
 )
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
index 20c8f76..20a698d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
@@ -21,14 +21,9 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
 
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -37,15 +32,6 @@
 @NoHttpd
 public class DraftChangeBlockedIT extends AbstractDaemonTest {
 
-  @Inject
-  private ProjectCache projectCache;
-
-  @Inject
-  private AllProjectsName allProjects;
-
-  @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
-
   @Before
   public void setUp() throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
@@ -55,27 +41,19 @@
   }
 
   @Test
-  public void testPushDraftChange_Blocked() throws GitAPIException,
-      OrmException, IOException {
+  public void testPushDraftChange_Blocked() throws Exception {
     // create draft by pushing to 'refs/drafts/'
     PushOneCommit.Result r = pushTo("refs/drafts/master");
     r.assertErrorStatus("cannot upload drafts");
   }
 
   @Test
-  public void testPushDraftChangeMagic_Blocked() throws GitAPIException,
-      OrmException, IOException {
+  public void testPushDraftChangeMagic_Blocked() throws Exception {
     // create draft by using 'draft' option
     PushOneCommit.Result r = pushTo("refs/for/master%draft");
     r.assertErrorStatus("cannot upload drafts");
   }
 
-  private PushOneCommit.Result pushTo(String ref) throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, ref);
-  }
-
   private void saveProjectConfig(ProjectConfig cfg) throws IOException {
     MetaDataUpdate md = metaDataUpdateFactory.create(allProjects);
     try {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/ForcePushIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/ForcePushIT.java
new file mode 100644
index 0000000..5da524e
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/ForcePushIT.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.eclipse.jgit.lib.Constants.HEAD;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.common.data.Permission;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.junit.Test;
+
+public class ForcePushIT extends AbstractDaemonTest {
+
+  @Test
+  public void forcePushNotAllowed() throws Exception {
+    ObjectId initial = git.getRepository().getRef(HEAD).getLeaf().getObjectId();
+    PushOneCommit push1 =
+        pushFactory.create(db, admin.getIdent(), "change1", "a.txt", "content");
+    PushOneCommit.Result r1 = push1.to(git, "refs/heads/master");
+    r1.assertOkStatus();
+
+    // Reset HEAD to initial so the new change is a non-fast forward
+    RefUpdate ru = git.getRepository().updateRef(HEAD);
+    ru.setNewObjectId(initial);
+    assertThat(ru.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
+
+    PushOneCommit push2 =
+        pushFactory.create(db, admin.getIdent(), "change2", "b.txt", "content");
+    push2.setForce(true);
+    PushOneCommit.Result r2 = push2.to(git, "refs/heads/master");
+    r2.assertErrorStatus("non-fast forward");
+  }
+
+  @Test
+  public void forcePushAllowed() throws Exception {
+    ObjectId initial = git.getRepository().getRef(HEAD).getLeaf().getObjectId();
+    grant(Permission.PUSH, project, "refs/*", true);
+    PushOneCommit push1 =
+        pushFactory.create(db, admin.getIdent(), "change1", "a.txt", "content");
+    PushOneCommit.Result r1 = push1.to(git, "refs/heads/master");
+    r1.assertOkStatus();
+
+    // Reset HEAD to initial so the new change is a non-fast forward
+    RefUpdate ru = git.getRepository().updateRef(HEAD);
+    ru.setNewObjectId(initial);
+    assertThat(ru.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
+
+    PushOneCommit push2 =
+        pushFactory.create(db, admin.getIdent(), "change2", "b.txt", "content");
+    push2.setForce(true);
+    PushOneCommit.Result r2 = push2.to(git, "refs/heads/master");
+    r2.assertOkStatus();
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java
index 71f008a..9f884c8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java
@@ -14,17 +14,13 @@
 
 package com.google.gerrit.acceptance.git;
 
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.transport.CredentialsProvider;
 import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
 import org.junit.Before;
 
-import java.io.IOException;
-import java.net.URISyntaxException;
-
 public class HttpPushForReviewIT extends AbstractPushForReview {
   @Before
-  public void selectHttpUrl() throws GitAPIException, IOException, URISyntaxException {
+  public void selectHttpUrl() throws Exception {
     CredentialsProvider.setDefault(new UsernamePasswordCredentialsProvider(
         admin.username, admin.httpPassword));
     selectProtocol(Protocol.HTTP);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SshPushForReviewIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SshPushForReviewIT.java
index c037e76..c7da993 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SshPushForReviewIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SshPushForReviewIT.java
@@ -16,15 +16,12 @@
 
 import com.google.gerrit.acceptance.NoHttpd;
 
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.junit.Before;
 
-import java.io.IOException;
-
 @NoHttpd
 public class SshPushForReviewIT extends AbstractPushForReview {
   @Before
-  public void selectSshUrl() throws GitAPIException, IOException {
+  public void selectSshUrl() throws Exception {
     selectProtocol(Protocol.SSH);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
index a73169d..cf0945e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -14,108 +14,51 @@
 
 package com.google.gerrit.acceptance.git;
 
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.git.CommitMergeStatus;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTag;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.RefSpec;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
 
 @NoHttpd
 public class SubmitOnPushIT extends AbstractDaemonTest {
-
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
-  private GitRepositoryManager repoManager;
-
   @Inject
   private ApprovalsUtil approvalsUtil;
 
   @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
-
-  @Inject
-  private ProjectCache projectCache;
-
-  @Inject
-  private GroupCache groupCache;
-
-  @Inject
   private ChangeNotes.Factory changeNotesFactory;
 
   @Inject
   private @GerritPersonIdent PersonIdent serverIdent;
 
-  @Inject
-  private PushOneCommit.Factory pushFactory;
-
-  private Project.NameKey project;
-  private Git git;
-  private ReviewDb db;
-
-  @Before
-  public void setUp() throws Exception {
-    project = new Project.NameKey("p");
-    SshSession sshSession = new SshSession(server, admin);
-    createProject(sshSession, project.get());
-    git = cloneProject(sshSession.getUrl() + "/" + project.get());
-    sshSession.close();
-
-    db = reviewDbProvider.open();
-  }
-
-  @After
-  public void cleanup() {
-    db.close();
-  }
-
   @Test
-  public void submitOnPush() throws GitAPIException, OrmException,
-      IOException, ConfigInvalidException {
+  public void submitOnPush() throws Exception {
     grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
     PushOneCommit.Result r = pushTo("refs/for/master%submit");
     r.assertOkStatus();
@@ -125,11 +68,11 @@
   }
 
   @Test
-  public void submitOnPushWithTag() throws GitAPIException, OrmException,
-      IOException, ConfigInvalidException {
+  public void submitOnPushWithTag() throws Exception {
     grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
     grant(Permission.CREATE, project, "refs/tags/*");
-    final String tag = "v1.0";
+    grant(Permission.PUSH, project, "refs/tags/*");
+    PushOneCommit.Tag tag = new PushOneCommit.Tag("v1.0");
     PushOneCommit push = pushFactory.create(db, admin.getIdent());
     push.setTag(tag);
     PushOneCommit.Result r = push.to(git, "refs/for/master%submit");
@@ -141,8 +84,24 @@
   }
 
   @Test
-  public void submitOnPushToRefsMetaConfig() throws GitAPIException,
-      OrmException, IOException, ConfigInvalidException {
+  public void submitOnPushWithAnnotatedTag() throws Exception {
+    grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
+    grant(Permission.CREATE, project, "refs/tags/*");
+    grant(Permission.PUSH, project, "refs/tags/*");
+    PushOneCommit.AnnotatedTag tag =
+        new PushOneCommit.AnnotatedTag("v1.0", "annotation", admin.getIdent());
+    PushOneCommit push = pushFactory.create(db, admin.getIdent());
+    push.setTag(tag);
+    PushOneCommit.Result r = push.to(git, "refs/for/master%submit");
+    r.assertOkStatus();
+    r.assertChange(Change.Status.MERGED, null, admin);
+    assertSubmitApproval(r.getPatchSetId());
+    assertCommit(project, "refs/heads/master");
+    assertTag(project, "refs/heads/master", tag);
+  }
+
+  @Test
+  public void submitOnPushToRefsMetaConfig() throws Exception {
     grant(Permission.SUBMIT, project, "refs/for/refs/meta/config");
 
     git.fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config")).call();
@@ -157,8 +116,7 @@
   }
 
   @Test
-  public void submitOnPushMergeConflict() throws GitAPIException, OrmException,
-      IOException, ConfigInvalidException {
+  public void submitOnPushMergeConflict() throws Exception {
     String master = "refs/heads/master";
     ObjectId objectId = git.getRepository().getRef(master).getObjectId();
     push(master, "one change", "a.txt", "some content");
@@ -173,8 +131,7 @@
   }
 
   @Test
-  public void submitOnPushSuccessfulMerge() throws GitAPIException, OrmException,
-      IOException, ConfigInvalidException {
+  public void submitOnPushSuccessfulMerge() throws Exception {
     String master = "refs/heads/master";
     ObjectId objectId = git.getRepository().getRef(master).getObjectId();
     push(master, "one change", "a.txt", "some content");
@@ -189,8 +146,7 @@
   }
 
   @Test
-  public void submitOnPushNewPatchSet() throws GitAPIException,
-      OrmException, IOException, ConfigInvalidException {
+  public void submitOnPushNewPatchSet() throws Exception {
     PushOneCommit.Result r =
         push("refs/for/master", PushOneCommit.SUBJECT, "a.txt", "some content");
 
@@ -199,23 +155,21 @@
         "other content", r.getChangeId());
     r.assertOkStatus();
     r.assertChange(Change.Status.MERGED, null, admin);
-    Change c = Iterables.getOnlyElement(db.changes().byKey(
-        new Change.Key(r.getChangeId())).toList());
-    assertEquals(2, db.patchSets().byChange(c.getId()).toList().size());
+    Change c = Iterables.getOnlyElement(
+        queryProvider.get().byKeyPrefix(r.getChangeId())).change();
+    assertThat(db.patchSets().byChange(c.getId()).toList()).hasSize(2);
     assertSubmitApproval(r.getPatchSetId());
     assertCommit(project, "refs/heads/master");
   }
 
   @Test
-  public void submitOnPushNotAllowed_Error() throws GitAPIException,
-      OrmException, IOException {
+  public void submitOnPushNotAllowed_Error() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master%submit");
     r.assertErrorStatus("submit not allowed");
   }
 
   @Test
-  public void submitOnPushNewPatchSetNotAllowed_Error() throws GitAPIException,
-      OrmException, IOException, ConfigInvalidException {
+  public void submitOnPushNewPatchSetNotAllowed_Error() throws Exception {
     PushOneCommit.Result r =
         push("refs/for/master", PushOneCommit.SUBJECT, "a.txt", "some content");
 
@@ -225,15 +179,13 @@
   }
 
   @Test
-  public void submitOnPushingDraft_Error() throws GitAPIException,
-      OrmException, IOException {
+  public void submitOnPushingDraft_Error() throws Exception {
     PushOneCommit.Result r = pushTo("refs/for/master%draft,submit");
     r.assertErrorStatus("cannot submit draft");
   }
 
   @Test
-  public void submitOnPushToNonExistingBranch_Error() throws GitAPIException,
-      OrmException, IOException {
+  public void submitOnPushToNonExistingBranch_Error() throws Exception {
     String branchName = "non-existing";
     PushOneCommit.Result r = pushTo("refs/for/" + branchName + "%submit");
     r.assertErrorStatus("branch " + branchName + " not found");
@@ -250,22 +202,28 @@
         .setRefSpecs(new RefSpec(r.getCommitId().name() + ":refs/heads/master"))
         .call();
     assertCommit(project, "refs/heads/master");
-    assertNull(getSubmitter(r.getPatchSetId()));
+    assertThat(getSubmitter(r.getPatchSetId())).isNull();
     Change c = db.changes().get(r.getPatchSetId().getParentKey());
-    assertEquals(Change.Status.MERGED, c.getStatus());
+    assertThat(c.getStatus()).isEqualTo(Change.Status.MERGED);
   }
 
-  private void grant(String permission, Project.NameKey project, String ref)
-      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
-    MetaDataUpdate md = metaDataUpdateFactory.create(project);
-    md.setMessage(String.format("Grant %s on %s", permission, ref));
-    ProjectConfig config = ProjectConfig.read(md);
-    AccessSection s = config.getAccessSection(ref, true);
-    Permission p = s.getPermission(permission, true);
-    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
-    p.add(new PermissionRule(config.resolve(adminGroup)));
-    config.commit(md);
-    projectCache.evict(config.getProject());
+  @Test
+  public void mergeOnPushToBranchWithNewPatchset() throws Exception {
+    grant(Permission.PUSH, project, "refs/heads/master");
+    PushOneCommit.Result r = pushTo("refs/for/master");
+    r.assertOkStatus();
+
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+            "b.txt", "anotherContent", r.getChangeId());
+
+    r = push.to(git, "refs/heads/master");
+    r.assertOkStatus();
+
+    assertCommit(project, "refs/heads/master");
+    assertThat(getSubmitter(r.getPatchSetId())).isNull();
+    Change c = db.changes().get(r.getPatchSetId().getParentKey());
+    assertThat(c.getStatus()).isEqualTo(Change.Status.MERGED);
   }
 
   private PatchSetApproval getSubmitter(PatchSet.Id patchSetId)
@@ -277,64 +235,60 @@
 
   private void assertSubmitApproval(PatchSet.Id patchSetId) throws OrmException {
     PatchSetApproval a = getSubmitter(patchSetId);
-    assertTrue(a.isSubmit());
-    assertEquals(1, a.getValue());
-    assertEquals(admin.id, a.getAccountId());
+    assertThat(a.isSubmit()).isTrue();
+    assertThat(a.getValue()).isEqualTo((short) 1);
+    assertThat(a.getAccountId()).isEqualTo(admin.id);
   }
 
   private void assertCommit(Project.NameKey project, String branch) throws IOException {
-    Repository r = repoManager.openRepository(project);
-    try {
-      RevWalk rw = new RevWalk(r);
-      try {
-        RevCommit c = rw.parseCommit(r.getRef(branch).getObjectId());
-        assertEquals(PushOneCommit.SUBJECT, c.getShortMessage());
-        assertEquals(admin.email, c.getAuthorIdent().getEmailAddress());
-        assertEquals(admin.email, c.getCommitterIdent().getEmailAddress());
-      } finally {
-        rw.release();
-      }
-    } finally {
-      r.close();
+    try (Repository r = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(r)) {
+      RevCommit c = rw.parseCommit(r.getRef(branch).getObjectId());
+      assertThat(c.getShortMessage()).isEqualTo(PushOneCommit.SUBJECT);
+      assertThat(c.getAuthorIdent().getEmailAddress()).isEqualTo(admin.email);
+      assertThat(c.getCommitterIdent().getEmailAddress()).isEqualTo(
+          admin.email);
     }
   }
 
   private void assertMergeCommit(String branch, String subject) throws IOException {
-    Repository r = repoManager.openRepository(project);
-    try {
-      RevWalk rw = new RevWalk(r);
-      try {
-        RevCommit c = rw.parseCommit(r.getRef(branch).getObjectId());
-        assertEquals(2, c.getParentCount());
-        assertEquals("Merge \"" + subject + "\"", c.getShortMessage());
-        assertEquals(admin.email, c.getAuthorIdent().getEmailAddress());
-        assertEquals(serverIdent.getEmailAddress(), c.getCommitterIdent().getEmailAddress());
-      } finally {
-        rw.release();
+    try (Repository r = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(r)) {
+      RevCommit c = rw.parseCommit(r.getRef(branch).getObjectId());
+      assertThat(c.getParentCount()).is(2);
+      assertThat(c.getShortMessage()).isEqualTo("Merge \"" + subject + "\"");
+      assertThat(c.getAuthorIdent().getEmailAddress()).isEqualTo(admin.email);
+      assertThat(c.getCommitterIdent().getEmailAddress()).isEqualTo(
+          serverIdent.getEmailAddress());
+    }
+  }
+
+  private void assertTag(Project.NameKey project, String branch,
+      PushOneCommit.Tag tag) throws IOException {
+    try (Repository repo = repoManager.openRepository(project)) {
+      Ref tagRef = repo.getRef(tag.name);
+      assertThat(tagRef).isNotNull();
+      ObjectId taggedCommit = null;
+      if (tag instanceof PushOneCommit.AnnotatedTag) {
+        PushOneCommit.AnnotatedTag annotatedTag = (PushOneCommit.AnnotatedTag)tag;
+        try (RevWalk rw = new RevWalk(repo)) {
+          RevObject object = rw.parseAny(tagRef.getObjectId());
+          assertThat(object).isInstanceOf(RevTag.class);
+          RevTag tagObject = (RevTag) object;
+          assertThat(tagObject.getFullMessage())
+              .isEqualTo(annotatedTag.message);
+          assertThat(tagObject.getTaggerIdent()).isEqualTo(annotatedTag.tagger);
+          taggedCommit = tagObject.getObject();
+        }
+      } else {
+        taggedCommit = tagRef.getObjectId();
       }
-    } finally {
-      r.close();
+      ObjectId headCommit = repo.getRef(branch).getObjectId();
+      assertThat(taggedCommit).isNotNull();
+      assertThat(taggedCommit).isEqualTo(headCommit);
     }
   }
 
-  private void assertTag(Project.NameKey project, String branch, String tagName)
-      throws IOException {
-    Repository r = repoManager.openRepository(project);
-    try {
-      ObjectId headCommit = r.getRef(branch).getObjectId();
-      ObjectId taggedCommit = r.getRef(tagName).getObjectId();
-      assertEquals(headCommit, taggedCommit);
-    } finally {
-      r.close();
-    }
-  }
-
-  private PushOneCommit.Result pushTo(String ref) throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, ref);
-  }
-
   private PushOneCommit.Result push(String ref, String subject,
       String fileName, String content) throws GitAPIException, IOException {
     PushOneCommit push =
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
new file mode 100644
index 0000000..4a66a4169
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
@@ -0,0 +1,265 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Ordering;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.edit.ChangeEditModifier;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.project.Util;
+import com.google.gerrit.testutil.ConfigSuite;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(ConfigSuite.class)
+@NoHttpd
+public class VisibleRefFilterIT extends AbstractDaemonTest {
+  @ConfigSuite.Config
+  public static Config noteDbWriteEnabled() {
+    Config cfg = new Config();
+    cfg.setBoolean("notedb", "changes", "write", true);
+    return cfg;
+  }
+
+  @Inject
+  private NotesMigration notesMigration;
+
+  @Inject
+  private ChangeEditModifier editModifier;
+
+  private AccountGroup.UUID admins;
+
+  @Before
+  public void setUp() throws Exception {
+    admins = groupCache.get(new AccountGroup.NameKey("Administrators"))
+        .getGroupUUID();
+    setUpChanges();
+    setUpPermissions();
+  }
+
+  private void setUpPermissions() throws Exception {
+    ProjectConfig pc = projectCache.checkedGet(allProjects).getConfig();
+    for (AccessSection sec : pc.getAccessSections()) {
+      sec.removePermission(Permission.READ);
+    }
+    saveProjectConfig(allProjects, pc);
+  }
+
+  private void setUpChanges() throws Exception {
+    gApi.projects()
+        .name(project.get())
+        .branch("branch")
+        .create(new BranchInput());
+
+    allow(Permission.SUBMIT, admins, "refs/for/refs/heads/*");
+    PushOneCommit.Result mr = pushFactory.create(db, admin.getIdent())
+        .to(git, "refs/for/master%submit");
+    mr.assertOkStatus();
+    PushOneCommit.Result br = pushFactory.create(db, admin.getIdent())
+        .to(git, "refs/for/branch%submit");
+    br.assertOkStatus();
+
+    Repository repo = repoManager.openRepository(project);
+    try {
+      // master-tag -> master
+      RefUpdate mtu = repo.updateRef("refs/tags/master-tag");
+      mtu.setExpectedOldObjectId(ObjectId.zeroId());
+      mtu.setNewObjectId(repo.getRef("refs/heads/master").getObjectId());
+      assertThat(mtu.update()).isEqualTo(RefUpdate.Result.NEW);
+
+      // branch-tag -> branch
+      RefUpdate btu = repo.updateRef("refs/tags/branch-tag");
+      btu.setExpectedOldObjectId(ObjectId.zeroId());
+      btu.setNewObjectId(repo.getRef("refs/heads/branch").getObjectId());
+      assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW);
+    } finally {
+      repo.close();
+    }
+  }
+
+  @Test
+  public void allRefsVisibleNoRefsMetaConfig() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    Util.allow(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
+    Util.allow(cfg, Permission.READ, admins, "refs/meta/config");
+    Util.doNotInherit(cfg, Permission.READ, "refs/meta/config");
+    saveProjectConfig(project, cfg);
+
+    assertRefs(
+        "HEAD",
+        "refs/changes/01/1/1",
+        "refs/changes/01/1/meta",
+        "refs/changes/02/2/1",
+        "refs/changes/02/2/meta",
+        "refs/heads/branch",
+        "refs/heads/master",
+        "refs/tags/branch-tag",
+        "refs/tags/master-tag");
+  }
+
+  @Test
+  public void allRefsVisibleWithRefsMetaConfig() throws Exception {
+    allow(Permission.READ, REGISTERED_USERS, "refs/*");
+    allow(Permission.READ, REGISTERED_USERS, "refs/meta/config");
+
+    assertRefs(
+        "HEAD",
+        "refs/changes/01/1/1",
+        "refs/changes/01/1/meta",
+        "refs/changes/02/2/1",
+        "refs/changes/02/2/meta",
+        "refs/heads/branch",
+        "refs/heads/master",
+        "refs/meta/config",
+        "refs/tags/branch-tag",
+        "refs/tags/master-tag");
+  }
+
+  @Test
+  public void subsetOfBranchesVisibleIncludingHead() throws Exception {
+    allow(Permission.READ, REGISTERED_USERS, "refs/heads/master");
+    deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
+
+    assertRefs(
+        "HEAD",
+        "refs/changes/01/1/1",
+        "refs/changes/01/1/meta",
+        "refs/heads/master",
+        "refs/tags/master-tag");
+  }
+
+  @Test
+  public void subsetOfBranchesVisibleNotIncludingHead() throws Exception {
+    deny(Permission.READ, REGISTERED_USERS, "refs/heads/master");
+    allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
+
+    assertRefs(
+        "refs/changes/02/2/1",
+        "refs/changes/02/2/meta",
+        "refs/heads/branch",
+        "refs/tags/branch-tag",
+        // master branch is not visible but master-tag is reachable from branch
+        // (since PushOneCommit always bases changes on each other).
+        "refs/tags/master-tag");
+  }
+
+  @Test
+  public void subsetOfBranchesVisibleWithEdit() throws Exception {
+    allow(Permission.READ, REGISTERED_USERS, "refs/heads/master");
+    deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
+
+    Change c1 = db.changes().get(new Change.Id(1));
+    PatchSet ps1 = db.patchSets().get(new PatchSet.Id(c1.getId(), 1));
+
+    // Admin's edit is not visible.
+    setApiUser(admin);
+    editModifier.createEdit(c1, ps1);
+
+    // User's edit is visible.
+    setApiUser(user);
+    editModifier.createEdit(c1, ps1);
+
+    assertRefs(
+        "HEAD",
+        "refs/changes/01/1/1",
+        "refs/changes/01/1/meta",
+        "refs/heads/master",
+        "refs/tags/master-tag",
+        "refs/users/01/1000001/edit-1/1");
+  }
+
+  @Test
+  public void subsetOfRefsVisibleWithAccessDatabase() throws Exception {
+    deny(Permission.READ, REGISTERED_USERS, "refs/heads/master");
+    allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
+    allowGlobalCapability(GlobalCapability.ACCESS_DATABASE, REGISTERED_USERS);
+
+    Change c1 = db.changes().get(new Change.Id(1));
+    PatchSet ps1 = db.patchSets().get(new PatchSet.Id(c1.getId(), 1));
+    setApiUser(admin);
+    editModifier.createEdit(c1, ps1);
+    setApiUser(user);
+    editModifier.createEdit(c1, ps1);
+
+    assertRefs(
+        // Change 1 is visible due to accessDatabase capability, even though
+        // refs/heads/master is not.
+        "refs/changes/01/1/1",
+        "refs/changes/01/1/meta",
+        "refs/changes/02/2/1",
+        "refs/changes/02/2/meta",
+        "refs/heads/branch",
+        "refs/tags/branch-tag",
+        // See comment in subsetOfBranchesVisibleNotIncludingHead.
+        "refs/tags/master-tag",
+        // All edits are visible due to accessDatabase capability.
+        "refs/users/00/1000000/edit-1/1",
+        "refs/users/01/1000001/edit-1/1");
+  }
+
+  /**
+   * Assert that refs seen by a non-admin user match expected.
+   *
+   * @param expected expected refs, in order. If notedb is disabled by the
+   *     configuration, any notedb refs (i.e. ending in "/meta") are removed
+   *     from the expected list before comparing to the actual results.
+   * @throws Exception
+   */
+  private void assertRefs(String... expected) throws Exception {
+    String out = sshSession.exec(String.format(
+        "gerrit ls-user-refs -p %s -u %s",
+        project.get(), user.getId().get()));
+    assert_().withFailureMessage(sshSession.getError())
+      .that(sshSession.hasError()).isFalse();
+
+    List<String> filtered = new ArrayList<>(expected.length);
+    for (String r : expected) {
+      if (notesMigration.writeChanges() || !r.endsWith(RefNames.META_SUFFIX)) {
+        filtered.add(r);
+      }
+    }
+
+    Splitter s = Splitter.on(CharMatcher.WHITESPACE).omitEmptyStrings();
+    assertThat(filtered).containsSequence(
+        Ordering.natural().sortedCopy(s.split(out)));
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNotedbIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNotedbIT.java
new file mode 100644
index 0000000..c538b85
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNotedbIT.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.pgm;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.io.Files;
+import com.google.gerrit.launcher.GerritLauncher;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.testutil.TempFileUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+
+public class RebuildNotedbIT {
+  private File sitePath;
+
+  @Before
+  public void createTempDirectory() throws Exception {
+    sitePath = TempFileUtil.createTempDirectory();
+  }
+
+  @After
+  public void destroySite() throws Exception {
+    if (sitePath != null) {
+      TempFileUtil.cleanup();
+    }
+  }
+
+  @Test
+  public void rebuildEmptySite() throws Exception {
+    initSite();
+    Files.append(NotesMigration.allEnabledConfig().toText(),
+        new File(sitePath.toString(), "etc/gerrit.config"),
+        StandardCharsets.UTF_8);
+    runGerrit("RebuildNotedb", "-d", sitePath.toString(),
+        "--show-stack-trace");
+  }
+
+  private void initSite() throws Exception {
+    runGerrit("init", "-d", sitePath.getPath(),
+        "--batch", "--no-auto-start", "--skip-plugins", "--show-stack-trace");
+  }
+
+  private static void runGerrit(String... args) throws Exception {
+    assertThat(GerritLauncher.mainImpl(args)).isEqualTo(0);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java
index 4f9ef14..968456b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.acceptance.pgm;
 
-import static org.junit.Assert.assertEquals;
+import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.acceptance.TempFileUtil;
 import com.google.gerrit.launcher.GerritLauncher;
+import com.google.gerrit.testutil.TempFileUtil;
 
 import org.junit.After;
 import org.junit.Before;
@@ -43,24 +43,16 @@
   @Test
   public void reindexEmptySite() throws Exception {
     initSite();
-    runGerrit("reindex", "-d", sitePath.getPath(),
+    runGerrit("reindex", "-d", sitePath.toString(),
         "--show-stack-trace");
   }
 
-  @Test
-  public void reindexEmptySiteWithRecheckMergeable() throws Exception {
-    initSite();
-    runGerrit("reindex", "-d", sitePath.getPath(),
-        "--show-stack-trace",
-        "--recheck-mergeable");
-  }
-
   private void initSite() throws Exception {
     runGerrit("init", "-d", sitePath.getPath(),
         "--batch", "--no-auto-start", "--skip-plugins", "--show-stack-trace");
   }
 
   private static void runGerrit(String... args) throws Exception {
-    assertEquals(0, GerritLauncher.mainImpl(args));
+    assertThat(GerritLauncher.mainImpl(args)).isEqualTo(0);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/AccountAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/AccountAssert.java
index 2c41f9a..c90924d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/AccountAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/AccountAssert.java
@@ -14,17 +14,16 @@
 
 package com.google.gerrit.acceptance.rest.account;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.TestAccount;
-import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.extensions.common.AccountInfo;
 
 public class AccountAssert {
 
   public static void assertAccountInfo(TestAccount a, AccountInfo ai) {
-    assertTrue(a.id.get() == ai._accountId);
-    assertEquals(a.fullName, ai.name);
-    assertEquals(a.email, ai.email);
+    assertThat(a.id.get()).isEqualTo(ai._accountId);
+    assertThat(a.fullName).isEqualTo(ai.name);
+    assertThat(a.email).isEqualTo(ai.email);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
index 9d22ee4..6ce96ee 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
@@ -14,26 +14,31 @@
 
 package com.google.gerrit.acceptance.rest.account;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.common.data.GlobalCapability.ACCESS_DATABASE;
+import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
+import static com.google.gerrit.common.data.GlobalCapability.BATCH_CHANGES_LIMIT;
+import static com.google.gerrit.common.data.GlobalCapability.DEFAULT_MAX_BATCH_CHANGES_LIMIT;
+import static com.google.gerrit.common.data.GlobalCapability.DEFAULT_MAX_QUERY_LIMIT;
+import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
+import static com.google.gerrit.common.data.GlobalCapability.QUERY_LIMIT;
+import static com.google.gerrit.common.data.GlobalCapability.RUN_AS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
-import com.google.inject.Inject;
 
+import org.apache.http.HttpStatus;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.junit.Test;
 
@@ -41,71 +46,66 @@
 
 public class CapabilitiesIT extends AbstractDaemonTest {
 
-  @Inject
-  private AllProjectsName allProjects;
-
-  @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
-
-  @Inject
-  private ProjectCache projectCache;
-
   @Test
-  public void testCapabilitiesUser() throws IOException,
-      ConfigInvalidException, IllegalArgumentException,
-      IllegalAccessException, NoSuchFieldException,
-      SecurityException {
+  public void testCapabilitiesUser() throws Exception {
     grantAllCapabilities();
     RestResponse r =
         userSession.get("/accounts/self/capabilities");
-    int code = r.getStatusCode();
-    assertEquals(code, 200);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     CapabilityInfo info = (new Gson()).fromJson(r.getReader(),
         new TypeToken<CapabilityInfo>() {}.getType());
-    for (String c: GlobalCapability.getAllNames()) {
-      if (GlobalCapability.ADMINISTRATE_SERVER.equals(c)) {
-        assertFalse(info.administrateServer);
-      } else if (GlobalCapability.PRIORITY.equals(c)) {
-        assertFalse(info.priority);
-      } else if (GlobalCapability.QUERY_LIMIT.equals(c)) {
-        assertEquals(0, info.queryLimit.min);
-        assertEquals(0, info.queryLimit.max);
+    for (String c : GlobalCapability.getAllNames()) {
+      if (ADMINISTRATE_SERVER.equals(c)) {
+        assertThat(info.administrateServer).isFalse();
+      } else if (BATCH_CHANGES_LIMIT.equals(c)) {
+        assertThat(info.batchChangesLimit.min).isEqualTo((short) 0);
+        assertThat(info.batchChangesLimit.max).isEqualTo((short) DEFAULT_MAX_BATCH_CHANGES_LIMIT);
+      } else if (PRIORITY.equals(c)) {
+        assertThat(info.priority).isFalse();
+      } else if (QUERY_LIMIT.equals(c)) {
+        assertThat(info.queryLimit.min).isEqualTo((short) 0);
+        assertThat(info.queryLimit.max).isEqualTo((short) DEFAULT_MAX_QUERY_LIMIT);
       } else {
-        assertTrue(String.format("capability %s was not granted", c),
-            (Boolean)CapabilityInfo.class.getField(c).get(info));
+        assert_().withFailureMessage(String.format("capability %s was not granted", c))
+          .that((Boolean) CapabilityInfo.class.getField(c).get(info)).isTrue();
       }
     }
   }
 
   @Test
-  public void testCapabilitiesAdmin() throws IOException,
-      ConfigInvalidException, IllegalArgumentException,
-      IllegalAccessException, NoSuchFieldException,
-      SecurityException {
+  public void testCapabilitiesAdmin() throws Exception {
     RestResponse r =
         adminSession.get("/accounts/self/capabilities");
-    int code = r.getStatusCode();
-    assertEquals(code, 200);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     CapabilityInfo info = (new Gson()).fromJson(r.getReader(),
         new TypeToken<CapabilityInfo>() {}.getType());
-    for (String c: GlobalCapability.getAllNames()) {
-      if (GlobalCapability.PRIORITY.equals(c)) {
-        assertFalse(info.priority);
-      } else if (GlobalCapability.QUERY_LIMIT.equals(c)) {
-        assertNotNull("missing queryLimit", info.queryLimit);
-        assertEquals(0, info.queryLimit.min);
-        assertEquals(500, info.queryLimit.max);
-      } else if (GlobalCapability.ACCESS_DATABASE.equals(c)) {
-        assertFalse(info.accessDatabase);
-      } else if (GlobalCapability.RUN_AS.equals(c)) {
-        assertFalse(info.runAs);
+    for (String c : GlobalCapability.getAllNames()) {
+      if (BATCH_CHANGES_LIMIT.equals(c)) {
+        // It does not have default value for any user as it can override the
+        // 'receive.batchChangesLimit'. It needs to be granted explicitly.
+        assertThat(info.batchChangesLimit).isNull();
+      } else if (PRIORITY.equals(c)) {
+        assertThat(info.priority).isFalse();
+      } else if (QUERY_LIMIT.equals(c)) {
+        assert_().withFailureMessage("missing queryLimit")
+          .that(info.queryLimit).isNotNull();
+        assertThat(info.queryLimit.min).isEqualTo((short) 0);
+        assertThat(info.queryLimit.max).isEqualTo((short) DEFAULT_MAX_QUERY_LIMIT);
+      } else if (ACCESS_DATABASE.equals(c)) {
+        assertThat(info.accessDatabase).isFalse();
+      } else if (RUN_AS.equals(c)) {
+        assertThat(info.runAs).isFalse();
       } else {
-        assertTrue(String.format("capability %s was not granted", c),
-            (Boolean)CapabilityInfo.class.getField(c).get(info));
+        assert_().withFailureMessage(String.format("capability %s was not granted", c))
+          .that((Boolean) CapabilityInfo.class.getField(c).get(info)).isTrue();
       }
     }
   }
 
+  /**
+   * Grant all global capabilities except ADMINISTRATE_SERVER and PRIORITY.
+   * Set the default ranges for range permissions.
+   */
   private void grantAllCapabilities() throws IOException,
       ConfigInvalidException {
     MetaDataUpdate md = metaDataUpdateFactory.create(allProjects);
@@ -113,14 +113,21 @@
     ProjectConfig config = ProjectConfig.read(md);
     AccessSection s = config.getAccessSection(
         AccessSection.GLOBAL_CAPABILITIES);
-    for (String c: GlobalCapability.getAllNames()) {
-      if (GlobalCapability.ADMINISTRATE_SERVER.equals(c)) {
+    for (String c : GlobalCapability.getAllNames()) {
+      if (ADMINISTRATE_SERVER.equals(c) || PRIORITY.equals(c)) {
         continue;
       }
       Permission p = s.getPermission(c, true);
-      p.add(new PermissionRule(
+      PermissionRule rule = new PermissionRule(
           config.resolve(SystemGroupBackend.getGroup(
-              SystemGroupBackend.REGISTERED_USERS))));
+              SystemGroupBackend.REGISTERED_USERS)));
+      if (GlobalCapability.hasRange(c)) {
+        PermissionRange.WithDefaults range = GlobalCapability.getRange(c);
+        if (range != null) {
+          rule.setRange(range.getDefaultMin(), range.getDefaultMax());
+        }
+      }
+      p.add(rule);
     }
     config.commit(md);
     projectCache.evict(config.getProject());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilityInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilityInfo.java
index adbf10a..e537373 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilityInfo.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilityInfo.java
@@ -17,13 +17,14 @@
 class CapabilityInfo {
   public boolean accessDatabase;
   public boolean administrateServer;
+  public BatchChangesLimit batchChangesLimit;
   public boolean createAccount;
   public boolean createGroup;
   public boolean createProject;
   public boolean emailReviewers;
   public boolean flushCaches;
-  public boolean generateHttpPassword;
   public boolean killTask;
+  public boolean modifyAccount;
   public boolean priority;
   public QueryLimit queryLimit;
   public boolean runAs;
@@ -39,4 +40,9 @@
     short min;
     short max;
   }
+
+  static class BatchChangesLimit {
+    short min;
+    short max;
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
index 1ac9cdf..63c6493 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
@@ -14,14 +14,14 @@
 
 package com.google.gerrit.acceptance.rest.account;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfo;
-import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.server.account.AccountInfo;
 
 import org.apache.http.HttpStatus;
 import org.junit.Test;
@@ -30,13 +30,13 @@
 
 public class GetAccountIT extends AbstractDaemonTest {
   @Test
-  public void getNonExistingAccount_NotFound() throws IOException {
-    assertEquals(HttpStatus.SC_NOT_FOUND,
-        adminSession.get("/accounts/non-existing").getStatusCode());
+  public void getNonExistingAccount_NotFound() throws Exception {
+    assertThat(adminSession.get("/accounts/non-existing").getStatusCode())
+      .isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 
   @Test
-  public void getAccount() throws IOException {
+  public void getAccount() throws Exception {
     // by formatted string
     testGetAccount("/accounts/"
         + Url.encode(admin.fullName + " <" + admin.email + ">"), admin);
@@ -60,7 +60,7 @@
   private void testGetAccount(String url, TestAccount expectedAccount)
       throws IOException {
     RestResponse r = adminSession.get(url);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     assertAccountInfo(expectedAccount, newGson()
         .fromJson(r.getReader(), AccountInfo.class));
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetDiffPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetDiffPreferencesIT.java
index 3850513..02a94f8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetDiffPreferencesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetDiffPreferencesIT.java
@@ -14,51 +14,48 @@
 
 package com.google.gerrit.acceptance.rest.account;
 
-import static org.junit.Assert.assertEquals;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.server.account.GetDiffPreferences.DiffPreferencesInfo;
-import com.google.gwtorm.server.OrmException;
 
 import org.apache.http.HttpStatus;
 import org.junit.Test;
 
-import java.io.IOException;
-
 public class GetDiffPreferencesIT extends AbstractDaemonTest {
   @Test
   public void getDiffPreferencesOfNonExistingAccount_NotFound()
-      throws IOException {
-    assertEquals(HttpStatus.SC_NOT_FOUND,
-        adminSession.get("/accounts/non-existing/preferences.diff").getStatusCode());
+      throws Exception {
+    assertThat(adminSession.get("/accounts/non-existing/preferences.diff").getStatusCode())
+      .isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 
   @Test
-  public void getDiffPreferences() throws IOException, OrmException {
+  public void getDiffPreferences() throws Exception {
     RestResponse r = adminSession.get("/accounts/" + admin.email + "/preferences.diff");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     DiffPreferencesInfo diffPreferences =
         newGson().fromJson(r.getReader(), DiffPreferencesInfo.class);
     assertDiffPreferences(new AccountDiffPreference(admin.id), diffPreferences);
   }
 
   private static void assertDiffPreferences(AccountDiffPreference expected, DiffPreferencesInfo actual) {
-    assertEquals(expected.getContext(), actual.context);
-    assertEquals(expected.isExpandAllComments(), toBoolean(actual.expandAllComments));
-    assertEquals(expected.getIgnoreWhitespace(), actual.ignoreWhitespace);
-    assertEquals(expected.isIntralineDifference(), toBoolean(actual.intralineDifference));
-    assertEquals(expected.getLineLength(), actual.lineLength);
-    assertEquals(expected.isManualReview(), toBoolean(actual.manualReview));
-    assertEquals(expected.isRetainHeader(), toBoolean(actual.retainHeader));
-    assertEquals(expected.isShowLineEndings(), toBoolean(actual.showLineEndings));
-    assertEquals(expected.isShowTabs(), toBoolean(actual.showTabs));
-    assertEquals(expected.isShowWhitespaceErrors(), toBoolean(actual.showWhitespaceErrors));
-    assertEquals(expected.isSkipDeleted(), toBoolean(actual.skipDeleted));
-    assertEquals(expected.isSkipUncommented(), toBoolean(actual.skipUncommented));
-    assertEquals(expected.isSyntaxHighlighting(), toBoolean(actual.syntaxHighlighting));
-    assertEquals(expected.getTabSize(), actual.tabSize);
+    assertThat(actual.context).isEqualTo(expected.getContext());
+    assertThat(toBoolean(actual.expandAllComments)).isEqualTo(expected.isExpandAllComments());
+    assertThat(actual.ignoreWhitespace).isEqualTo(expected.getIgnoreWhitespace());
+    assertThat(toBoolean(actual.intralineDifference)).isEqualTo(expected.isIntralineDifference());
+    assertThat(actual.lineLength).isEqualTo(expected.getLineLength());
+    assertThat(toBoolean(actual.manualReview)).isEqualTo(expected.isManualReview());
+    assertThat(toBoolean(actual.retainHeader)).isEqualTo(expected.isRetainHeader());
+    assertThat(toBoolean(actual.showLineEndings)).isEqualTo(expected.isShowLineEndings());
+    assertThat(toBoolean(actual.showTabs)).isEqualTo(expected.isShowTabs());
+    assertThat(toBoolean(actual.showWhitespaceErrors)).isEqualTo(expected.isShowWhitespaceErrors());
+    assertThat(toBoolean(actual.skipDeleted)).isEqualTo(expected.isSkipDeleted());
+    assertThat(toBoolean(actual.skipUncommented)).isEqualTo(expected.isSkipUncommented());
+    assertThat(toBoolean(actual.syntaxHighlighting)).isEqualTo(expected.isSyntaxHighlighting());
+    assertThat(actual.tabSize).isEqualTo(expected.getTabSize());
   }
 
   private static boolean toBoolean(Boolean b) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/StarredChangesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/StarredChangesIT.java
index 9470df0..face299a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/StarredChangesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/StarredChangesIT.java
@@ -14,17 +14,14 @@
 
 package com.google.gerrit.acceptance.rest.account;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwtorm.server.OrmException;
 
-import org.eclipse.jgit.api.errors.GitAPIException;
+import org.apache.http.HttpStatus;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -32,30 +29,29 @@
 public class StarredChangesIT extends AbstractDaemonTest {
 
   @Test
-  public void starredChangeState() throws GitAPIException, IOException,
-      OrmException {
+  public void starredChangeState() throws Exception {
     Result c1 = createChange();
     Result c2 = createChange();
-    assertNull(getChange(c1.getChangeId()).starred);
-    assertNull(getChange(c2.getChangeId()).starred);
+    assertThat(getChange(c1.getChangeId()).starred).isNull();
+    assertThat(getChange(c2.getChangeId()).starred).isNull();
     starChange(true, c1.getPatchSetId().getParentKey());
     starChange(true, c2.getPatchSetId().getParentKey());
-    assertTrue(getChange(c1.getChangeId()).starred);
-    assertTrue(getChange(c2.getChangeId()).starred);
+    assertThat(getChange(c1.getChangeId()).starred).isTrue();
+    assertThat(getChange(c2.getChangeId()).starred).isTrue();
     starChange(false, c1.getPatchSetId().getParentKey());
     starChange(false, c2.getPatchSetId().getParentKey());
-    assertNull(getChange(c1.getChangeId()).starred);
-    assertNull(getChange(c2.getChangeId()).starred);
+    assertThat(getChange(c1.getChangeId()).starred).isNull();
+    assertThat(getChange(c2.getChangeId()).starred).isNull();
   }
 
   private void starChange(boolean on, Change.Id id) throws IOException {
     String url = "/accounts/self/starred.changes/" + id.get();
     if (on) {
       RestResponse r = adminSession.put(url);
-      assertEquals(204, r.getStatusCode());
+      assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
     } else {
       RestResponse r = adminSession.delete(url);
-      assertEquals(204, r.getStatusCode());
+      assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
     }
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index ab8a5fc..dec3e65 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -14,35 +14,45 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.extensions.common.ListChangesOption.CURRENT_REVISION;
-import static com.google.gerrit.extensions.common.ListChangesOption.DETAILED_LABELS;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertTrue;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
+import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.common.EventListener;
+import com.google.gerrit.common.EventSource;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
-import com.google.gerrit.extensions.common.InheritableBoolean;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.ApprovalsUtil;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
-import com.google.gerrit.server.change.ChangeJson.LabelInfo;
-import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.events.ChangeMergedEvent;
+import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.ListBranches.BranchInfo;
 import com.google.gerrit.server.project.PutConfig;
+import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gson.reflect.TypeToken;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -53,7 +63,9 @@
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -66,11 +78,15 @@
 import java.sql.Timestamp;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 public abstract class AbstractSubmit extends AbstractDaemonTest {
+  @ConfigSuite.Config
+  public static Config submitWholeTopicEnabled() {
+    return submitWholeTopicEnabledConfig();
+  }
 
-  @Inject
-  private GitRepositoryManager repoManager;
+  private Map<String, String> mergeResults;
 
   @Inject
   private ChangeNotes.Factory notesFactory;
@@ -78,9 +94,28 @@
   @Inject
   private ApprovalsUtil approvalsUtil;
 
+  @Inject
+  private IdentifiedUser.GenericFactory factory;
+
+  @Inject
+  EventSource source;
 
   @Before
   public void setUp() throws Exception {
+    mergeResults = Maps.newHashMap();
+    CurrentUser listenerUser = factory.create(user.id);
+    source.addEventListener(new EventListener() {
+
+      @Override
+      public void onEvent(Event event) {
+        if (event instanceof ChangeMergedEvent) {
+          ChangeMergedEvent changeMergedEvent = (ChangeMergedEvent) event;
+          mergeResults.put(changeMergedEvent.change.number,
+              changeMergedEvent.newRev);
+        }
+      }
+
+    }, listenerUser);
     project = new Project.NameKey("p2");
   }
 
@@ -97,7 +132,22 @@
     Git git = createProject(false);
     PushOneCommit.Result change = createChange(git);
     submit(change.getChangeId());
-    assertEquals(change.getCommitId(), getRemoteHead().getId());
+    assertThat(getRemoteHead().getId()).isEqualTo(change.getCommitId());
+  }
+
+  @Test
+  public void submitWholeTopic() throws Exception {
+    assume().that(isSubmitWholeTopicEnabled()).isTrue();
+    Git git = createProject();
+    PushOneCommit.Result change1 =
+        createChange(git, "Change 1", "a.txt", "content", "test-topic");
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "b.txt", "content", "test-topic");
+    approve(change1.getChangeId());
+    approve(change2.getChangeId());
+    submit(change2.getChangeId());
+    change1.assertChange(Change.Status.MERGED, "test-topic", admin);
+    change2.assertChange(Change.Status.MERGED, "test-topic", admin);
   }
 
   protected Git createProject() throws JSchException, IOException,
@@ -123,7 +173,7 @@
     in.useContentMerge = InheritableBoolean.FALSE;
     RestResponse r =
         adminSession.put("/projects/" + project.get() + "/config", in);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     r.consume();
   }
 
@@ -132,7 +182,7 @@
     in.useContentMerge = InheritableBoolean.TRUE;
     RestResponse r =
         adminSession.put("/projects/" + project.get() + "/config", in);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     r.consume();
   }
 
@@ -149,6 +199,14 @@
     return push.to(git, "refs/for/master");
   }
 
+  protected PushOneCommit.Result createChange(Git git, String subject,
+      String fileName, String content, String topic)
+          throws GitAPIException, IOException {
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), subject, fileName, content);
+    return push.to(git, "refs/for/master/" + topic);
+  }
+
   protected void submit(String changeId) throws IOException {
     submit(changeId, HttpStatus.SC_OK);
   }
@@ -160,7 +218,7 @@
   protected void submitStatusOnly(String changeId)
       throws IOException, OrmException {
     approve(changeId);
-    Change c = db.changes().byKey(new Change.Key(changeId)).toList().get(0);
+    Change c = queryProvider.get().byKeyPrefix(changeId).get(0).change();
     c.setStatus(Change.Status.SUBMITTED);
     db.changes().update(Collections.singleton(c));
     db.patchSetApprovals().insert(Collections.singleton(
@@ -168,9 +226,10 @@
             new PatchSetApproval.Key(
                 c.currentPatchSetId(),
                 admin.id,
-                PatchSetApproval.LabelId.SUBMIT),
+                LabelId.SUBMIT),
             (short) 1,
             new Timestamp(System.currentTimeMillis()))));
+    indexer.index(db, c);
   }
 
   private void submit(String changeId, int expectedStatus) throws IOException {
@@ -179,55 +238,83 @@
     subm.waitForMerge = true;
     RestResponse r =
         adminSession.post("/changes/" + changeId + "/submit", subm);
-    assertEquals(expectedStatus, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(expectedStatus);
     if (expectedStatus == HttpStatus.SC_OK) {
       ChangeInfo change =
           newGson().fromJson(r.getReader(),
               new TypeToken<ChangeInfo>() {}.getType());
-      assertEquals(Change.Status.MERGED, change.status);
+      assertThat(change.status).isEqualTo(ChangeStatus.MERGED);
+
+      checkMergeResult(change);
     }
     r.consume();
   }
 
+  private void checkMergeResult(ChangeInfo change) throws IOException {
+    // Get the revision of the branch after the submit to compare with the
+    // newRev of the ChangeMergedEvent.
+    RestResponse b =
+        adminSession.get("/projects/" + project.get() + "/branches/"
+            + change.branch);
+    if (b.getStatusCode() == HttpStatus.SC_OK) {
+      BranchInfo branch =
+          newGson().fromJson(b.getReader(),
+              new TypeToken<BranchInfo>() {}.getType());
+      assertThat(branch.revision).isEqualTo(
+          mergeResults.get(Integer.toString(change._number)));
+    }
+    b.consume();
+  }
+
   private void approve(String changeId) throws IOException {
     RestResponse r = adminSession.post(
         "/changes/" + changeId + "/revisions/current/review",
         new ReviewInput().label("Code-Review", 2));
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     r.consume();
   }
 
   protected void assertCurrentRevision(String changeId, int expectedNum,
       ObjectId expectedId) throws IOException {
     ChangeInfo c = getChange(changeId, CURRENT_REVISION);
-    assertEquals(expectedId.name(), c.currentRevision);
-    assertEquals(expectedNum, c.revisions.get(expectedId.name())._number);
+    assertThat(c.currentRevision).isEqualTo(expectedId.name());
+    assertThat(c.revisions.get(expectedId.name())._number).isEqualTo(expectedNum);
+    Repository repo =
+        repoManager.openRepository(new Project.NameKey(c.project));
+    try {
+      Ref ref = repo.getRef(
+          new PatchSet.Id(new Change.Id(c._number), expectedNum).toRefName());
+      assertThat(ref).isNotNull();
+      assertThat(ref.getObjectId()).isEqualTo(expectedId);
+    } finally {
+      repo.close();
+    }
   }
 
   protected void assertApproved(String changeId) throws IOException {
     ChangeInfo c = getChange(changeId, DETAILED_LABELS);
     LabelInfo cr = c.labels.get("Code-Review");
-    assertEquals(1, cr.all.size());
-    assertEquals(2, cr.all.get(0).value.intValue());
-    assertEquals("Administrator", cr.all.get(0).name);
+    assertThat(cr.all).hasSize(1);
+    assertThat(cr.all.get(0).value.intValue()).isEqualTo(2);
+    assertThat(new Account.Id(cr.all.get(0)._accountId)).isEqualTo(admin.getId());
   }
 
   protected void assertSubmitter(String changeId, int psId)
-      throws OrmException, IOException {
+      throws OrmException {
     ChangeNotes cn = notesFactory.create(
-        Iterables.getOnlyElement(db.changes().byKey(new Change.Key(changeId))));
+        getOnlyElement(queryProvider.get().byKeyPrefix(changeId)).change());
     PatchSetApproval submitter = approvalsUtil.getSubmitter(
         db, cn, new PatchSet.Id(cn.getChangeId(), psId));
-    assertTrue(submitter.isSubmit());
-    assertEquals(admin.getId(), submitter.getAccountId());
+    assertThat(submitter.isSubmit()).isTrue();
+    assertThat(submitter.getAccountId()).isEqualTo(admin.getId());
   }
 
   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());
+    assertThat(remoteHead.getFooterLines("Reviewed-On")).isNotEmpty();
+    assertThat(remoteHead.getFooterLines("Reviewed-By")).isNotEmpty();
   }
 
   protected void assertRebase(Git localGit, boolean contentMerge)
@@ -235,12 +322,14 @@
     Repository repo = localGit.getRepository();
     RevCommit localHead = getHead(repo);
     RevCommit remoteHead = getRemoteHead();
-    assertNotEquals(localHead.getId(), remoteHead.getId());
-    assertEquals(1, remoteHead.getParentCount());
+    assert_().withFailureMessage(
+        String.format("%s not equal %s", localHead.name(), remoteHead.name()))
+          .that(localHead.getId()).isNotEqualTo(remoteHead.getId());
+    assertThat(remoteHead.getParentCount()).isEqualTo(1);
     if (!contentMerge) {
-      assertEquals(getLatestDiff(repo), getLatestRemoteDiff());
+      assertThat(getLatestRemoteDiff()).isEqualTo(getLatestDiff(repo));
     }
-    assertEquals(localHead.getShortMessage(), remoteHead.getShortMessage());
+    assertThat(remoteHead.getShortMessage()).isEqualTo(localHead.getShortMessage());
   }
 
   private RevCommit getHead(Repository repo) throws IOException {
@@ -248,40 +337,23 @@
   }
 
   protected RevCommit getRemoteHead() throws IOException {
-    Repository repo = repoManager.openRepository(project);
-    try {
+    try (Repository repo = repoManager.openRepository(project)) {
       return getHead(repo, "refs/heads/master");
-    } finally {
-      repo.close();
     }
   }
 
   protected List<RevCommit> getRemoteLog() throws IOException {
-    Repository repo = repoManager.openRepository(project);
-    try {
-      RevWalk rw = new RevWalk(repo);
-      try {
-        rw.markStart(rw.parseCommit(
-            repo.getRef("refs/heads/master").getObjectId()));
-        return Lists.newArrayList(rw);
-      } finally {
-        rw.release();
-      }
-    } finally {
-      repo.close();
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      rw.markStart(rw.parseCommit(
+          repo.getRef("refs/heads/master").getObjectId()));
+      return Lists.newArrayList(rw);
     }
   }
 
   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();
+    try (RevWalk rw = new RevWalk(repo)) {
+      return rw.parseCommit(repo.getRef(name).getObjectId());
     }
   }
 
@@ -292,28 +364,22 @@
   }
 
   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();
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      ObjectId oldTreeId = repo.resolve("refs/heads/master~1^{tree}");
+      ObjectId newTreeId = repo.resolve("refs/heads/master^{tree}");
+      return getLatestDiff(repo, oldTreeId, newTreeId);
     }
   }
 
   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();
+    try (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
index eb1a6be..1d60607 100644
--- 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
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.checkout;
-import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.PushOneCommit;
 
@@ -45,9 +45,9 @@
         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));
+    assertThat(head.getParentCount()).isEqualTo(2);
+    assertThat(head.getParent(0)).isEqualTo(oldHead);
+    assertThat(head.getParent(1)).isEqualTo(change2.getCommitId());
   }
 
   @Test
@@ -68,9 +68,9 @@
         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));
+    assertThat(head.getParentCount()).isEqualTo(2);
+    assertThat(head.getParent(0)).isEqualTo(oldHead);
+    assertThat(head.getParent(1)).isEqualTo(change3.getCommitId());
   }
 
   @Test
@@ -88,6 +88,6 @@
     PushOneCommit.Result change2 =
         createChange(git, "Change 2", "a.txt", "other content");
     submitWithConflict(change2.getChangeId());
-    assertEquals(oldHead, getRemoteHead());
+    assertThat(getRemoteHead()).isEqualTo(oldHead);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
new file mode 100644
index 0000000..afcd5d0
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -0,0 +1,144 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gson.reflect.TypeToken;
+
+import org.apache.http.HttpStatus;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Map;
+
+public class ActionsIT extends AbstractDaemonTest {
+  @ConfigSuite.Config
+  public static Config submitWholeTopicEnabled() {
+    return submitWholeTopicEnabledConfig();
+  }
+
+  @Test
+  public void revisionActionsOneChangePerTopicUnapproved() throws Exception {
+    String changeId = createChangeWithTopic("foo1").getChangeId();
+    Map<String, ActionInfo> actions = getActions(changeId);
+    assertThat(actions).containsKey("cherrypick");
+    assertThat(actions).containsKey("rebase");
+    assertThat(actions).hasSize(2);
+  }
+
+  @Test
+  public void revisionActionsOneChangePerTopic() throws Exception {
+    String changeId = createChangeWithTopic("foo1").getChangeId();
+    approve(changeId);
+    Map<String, ActionInfo> actions = getActions(changeId);
+    commonActionsAssertions(actions);
+    if (isSubmitWholeTopicEnabled()) {
+      ActionInfo info = actions.get("submit");
+      assertThat(info.enabled).isTrue();
+      assertThat(info.label).isEqualTo("Submit whole topic");
+      assertThat(info.method).isEqualTo("POST");
+      assertThat(info.title).isEqualTo("Submit all 1 changes of the same topic");
+    } else {
+      noSubmitWholeTopicAssertions(actions);
+    }
+  }
+
+  @Test
+  public void revisionActionsTwoChangeChangesInTopic() throws Exception {
+    String changeId = createChangeWithTopic("foo2").getChangeId();
+    approve(changeId);
+    // create another change with the same topic
+    createChangeWithTopic("foo2").getChangeId();
+    Map<String, ActionInfo> actions = getActions(changeId);
+    commonActionsAssertions(actions);
+    if (isSubmitWholeTopicEnabled()) {
+      ActionInfo info = actions.get("submit");
+      assertThat(info.enabled).isNull();
+      assertThat(info.label).isEqualTo("Submit whole topic");
+      assertThat(info.method).isEqualTo("POST");
+      assertThat(info.title).isEqualTo("Other changes in this topic are not ready");
+    } else {
+      noSubmitWholeTopicAssertions(actions);
+    }
+  }
+
+  @Test
+  public void revisionActionsTwoChangeChangesInTopicReady() throws Exception {
+    String changeId = createChangeWithTopic("foo2").getChangeId();
+    approve(changeId);
+    // create another change with the same topic
+    String changeId2 = createChangeWithTopic("foo2").getChangeId();
+    approve(changeId2);
+    Map<String, ActionInfo> actions = getActions(changeId);
+    commonActionsAssertions(actions);
+    if (isSubmitWholeTopicEnabled()) {
+      ActionInfo info = actions.get("submit");
+      assertThat(info.enabled).isTrue();
+      assertThat(info.label).isEqualTo("Submit whole topic");
+      assertThat(info.method).isEqualTo("POST");
+      assertThat(info.title).isEqualTo("Submit all 2 changes of the same topic");
+    } else {
+      noSubmitWholeTopicAssertions(actions);
+    }
+  }
+
+  private Map<String, ActionInfo> getActions(String changeId)
+      throws IOException {
+    return newGson().fromJson(
+        adminSession.get("/changes/"
+            + changeId
+            + "/revisions/1/actions").getReader(),
+        new TypeToken<Map<String, ActionInfo>>() {}.getType());
+  }
+
+  private void noSubmitWholeTopicAssertions(Map<String, ActionInfo> actions) {
+    ActionInfo info = actions.get("submit");
+    assertThat(info.enabled).isTrue();
+    assertThat(info.label).isEqualTo("Submit");
+    assertThat(info.method).isEqualTo("POST");
+    assertThat(info.title).isEqualTo("Submit patch set 1 into master");
+  }
+
+  private void commonActionsAssertions(Map<String, ActionInfo> actions) {
+    assertThat(actions).hasSize(3);
+    assertThat(actions).containsKey("cherrypick");
+    assertThat(actions).containsKey("submit");
+    assertThat(actions).containsKey("rebase");
+  }
+
+  private PushOneCommit.Result createChangeWithTopic(String topic) throws GitAPIException,
+      IOException {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent());
+    assertThat(topic).isNotEmpty();
+    return push.to(git, "refs/for/master/" + topic);
+  }
+
+  private void approve(String changeId) throws IOException {
+    RestResponse r = adminSession.post(
+        "/changes/" + changeId + "/revisions/current/review",
+        new ReviewInput().label("Code-Review", 2));
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    r.consume();
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
index acf50db..3c76355 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
@@ -14,20 +14,17 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
+import static com.google.common.truth.Truth.assertThat;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
-import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.testutil.ConfigSuite;
 
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.lib.Config;
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeUtils;
@@ -37,7 +34,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.io.IOException;
 import java.util.Iterator;
 import java.util.concurrent.atomic.AtomicLong;
 
@@ -48,10 +44,7 @@
 
   @ConfigSuite.Config
   public static Config noteDbEnabled() {
-    Config cfg = new Config();
-    cfg.setBoolean("notedb", null, "write", true);
-    cfg.setBoolean("notedb", "changeMessages", "read", true);
-    return cfg;
+    return NotesMigration.allEnabledConfig();
   }
 
   @Before
@@ -80,17 +73,17 @@
     String changeId = createChange().getChangeId();
     postMessage(changeId, "Some nits need to be fixed.");
     ChangeInfo c = info(changeId);
-    assertNull(c.messages);
+    assertThat((Iterable<?>)c.messages).isNull();
   }
 
   @Test
-  public void defaultMessage() throws GitAPIException, IOException,
-      RestApiException {
+  public void defaultMessage() throws Exception {
     String changeId = createChange().getChangeId();
     ChangeInfo c = get(changeId);
-    assertNotNull(c.messages);
-    assertEquals(1, c.messages.size());
-    assertEquals("Uploaded patch set 1.", c.messages.iterator().next().message);
+    assertThat((Iterable<?>)c.messages).isNotNull();
+    assertThat((Iterable<?>)c.messages).hasSize(1);
+    assertThat(c.messages.iterator().next().message)
+      .isEqualTo("Uploaded patch set 1.");
   }
 
   @Test
@@ -101,16 +94,16 @@
     String secondMessage = "I like this feature.";
     postMessage(changeId, secondMessage);
     ChangeInfo c = get(changeId);
-    assertNotNull(c.messages);
-    assertEquals(3, c.messages.size());
+    assertThat((Iterable<?>)c.messages).isNotNull();
+    assertThat((Iterable<?>)c.messages).hasSize(3);
     Iterator<ChangeMessageInfo> it = c.messages.iterator();
-    assertEquals("Uploaded patch set 1.", it.next().message);
+    assertThat(it.next().message).isEqualTo("Uploaded patch set 1.");
     assertMessage(firstMessage, it.next().message);
     assertMessage(secondMessage, it.next().message);
   }
 
   private void assertMessage(String expected, String actual) {
-    assertEquals("Patch Set 1:\n\n" + expected, actual);
+    assertThat(actual).isEqualTo("Patch Set 1:\n\n" + expected);
   }
 
   private void postMessage(String changeId, String msg) throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
index 83efd30..8e7daec 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
@@ -14,11 +14,10 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static com.google.gerrit.acceptance.GitUtil.initSsh;
 import static com.google.gerrit.common.data.Permission.LABEL;
-import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -33,9 +32,6 @@
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
 
 import org.apache.http.HttpStatus;
 import org.eclipse.jgit.api.errors.GitAPIException;
@@ -47,12 +43,6 @@
 
 public class ChangeOwnerIT extends AbstractDaemonTest {
 
-  @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
-
-  @Inject
-  private ProjectCache projectCache;
-
   private TestAccount user2;
 
   private RestSession sessionOwner;
@@ -63,8 +53,7 @@
     sessionOwner = new RestSession(server, user);
     SshSession sshSession = new SshSession(server, user);
     initSsh(user);
-    // need to initialize intern session
-    createProject(sshSession, "foo");
+    sshSession.open();
     git = cloneProject(sshSession.getUrl() + "/" + project.get());
     sshSession.close();
     user2 = accounts.user2();
@@ -72,21 +61,18 @@
   }
 
   @Test
-  public void testChangeOwner_OwnerACLNotGranted() throws GitAPIException,
-      IOException, OrmException, ConfigInvalidException {
+  public void testChangeOwner_OwnerACLNotGranted() throws Exception {
     approve(sessionOwner, createMyChange(), HttpStatus.SC_FORBIDDEN);
   }
 
   @Test
-  public void testChangeOwner_OwnerACLGranted() throws GitAPIException,
-      IOException, OrmException, ConfigInvalidException {
+  public void testChangeOwner_OwnerACLGranted() throws Exception {
     grantApproveToChangeOwner();
     approve(sessionOwner, createMyChange(), HttpStatus.SC_OK);
   }
 
   @Test
-  public void testChangeOwner_NotOwnerACLGranted() throws GitAPIException,
-      IOException, OrmException, ConfigInvalidException {
+  public void testChangeOwner_NotOwnerACLGranted() throws Exception {
     grantApproveToChangeOwner();
     approve(sessionDev, createMyChange(), HttpStatus.SC_FORBIDDEN);
   }
@@ -96,7 +82,7 @@
     RestResponse r =
         s.post("/changes/" + changeId + "/revisions/current/review",
             new ReviewInput().label("Code-Review", 2));
-    assertEquals(expected, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(expected);
     r.consume();
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConflictsOperatorIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConflictsOperatorIT.java
index 3370cb6..6a1c0a6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConflictsOperatorIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConflictsOperatorIT.java
@@ -14,9 +14,8 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.checkout;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 
 import com.google.common.base.Function;
 import com.google.common.collect.ImmutableSet;
@@ -24,11 +23,9 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gson.reflect.TypeToken;
 
-import com.jcraft.jsch.JSchException;
-
 import org.apache.http.HttpStatus;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
@@ -42,18 +39,16 @@
   private int count;
 
   @Test
-  public void noConflictingChanges() throws JSchException, IOException,
-      GitAPIException {
+  public void noConflictingChanges() throws Exception {
     PushOneCommit.Result change = createChange(git, true);
     createChange(git, false);
 
     Set<String> changes = queryConflictingChanges(change);
-    assertEquals(0, changes.size());
+    assertThat((Iterable<?>)changes).isEmpty();
   }
 
   @Test
-  public void conflictingChanges() throws JSchException, IOException,
-      GitAPIException {
+  public void conflictingChanges() throws Exception {
     PushOneCommit.Result change = createChange(git, true);
     PushOneCommit.Result conflictingChange1 = createChange(git, true);
     PushOneCommit.Result conflictingChange2 = createChange(git, true);
@@ -78,7 +73,7 @@
       throws IOException {
     RestResponse r =
         adminSession.get("/changes/?q=conflicts:" + change.getChangeId());
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     Set<ChangeInfo> changes =
         newGson().fromJson(r.getReader(),
             new TypeToken<Set<ChangeInfo>>() {}.getType());
@@ -94,9 +89,9 @@
 
   private void assertChanges(Set<String> actualChanges,
       PushOneCommit.Result... expectedChanges) {
-    assertEquals(expectedChanges.length, actualChanges.size());
+    assertThat((Iterable<?>)actualChanges).hasSize(expectedChanges.length);
     for (PushOneCommit.Result c : expectedChanges) {
-      assertTrue(actualChanges.contains(id(c)));
+      assertThat(actualChanges.contains(id(c))).isTrue();
     }
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 2026ac1..311161a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -14,27 +14,33 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
 
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.ChangeStatus;
-import com.google.gerrit.server.change.ChangeJson;
+import com.google.gerrit.testutil.ConfigSuite;
 
 import org.apache.http.HttpStatus;
+import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 
 public class CreateChangeIT extends AbstractDaemonTest {
+  @ConfigSuite.Config
+  public static Config allowDraftsDisabled() {
+    return allowDraftsDisabledConfig();
+  }
 
   @Test
   public void createEmptyChange_MissingBranch() throws Exception {
     ChangeInfo ci = new ChangeInfo();
     ci.project = project.get();
     RestResponse r = adminSession.post("/changes/", ci);
-    assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode());
-    assertTrue(r.getEntityContent().contains("branch must be non-empty"));
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+    assertThat(r.getEntityContent()).contains("branch must be non-empty");
   }
 
   @Test
@@ -43,16 +49,16 @@
     ci.project = project.get();
     ci.branch = "master";
     RestResponse r = adminSession.post("/changes/", ci);
-    assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode());
-    assertTrue(r.getEntityContent().contains("commit message must be non-empty"));
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+    assertThat(r.getEntityContent()).contains("commit message must be non-empty");
   }
 
   @Test
   public void createEmptyChange_InvalidStatus() throws Exception {
     ChangeInfo ci = newChangeInfo(ChangeStatus.SUBMITTED);
     RestResponse r = adminSession.post("/changes/", ci);
-    assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode());
-    assertTrue(r.getEntityContent().contains("unsupported change status"));
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
+    assertThat(r.getEntityContent()).contains("unsupported change status");
   }
 
   @Test
@@ -62,9 +68,19 @@
 
   @Test
   public void createDraftChange() throws Exception {
+    assume().that(isAllowDrafts()).isTrue();
     assertChange(newChangeInfo(ChangeStatus.DRAFT));
   }
 
+  @Test
+  public void createDraftChangeNotAllowed() throws Exception {
+    assume().that(isAllowDrafts()).isFalse();
+    ChangeInfo ci = newChangeInfo(ChangeStatus.DRAFT);
+    RestResponse r = adminSession.post("/changes/", ci);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_METHOD_NOT_ALLOWED);
+    assertThat(r.getEntityContent()).contains("draft workflow is disabled");
+  }
+
   private ChangeInfo newChangeInfo(ChangeStatus status) {
     ChangeInfo in = new ChangeInfo();
     in.project = project.get();
@@ -77,15 +93,24 @@
 
   private void assertChange(ChangeInfo in) throws Exception {
     RestResponse r = adminSession.post("/changes/", in);
-    assertEquals(HttpStatus.SC_CREATED, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
 
-    ChangeJson.ChangeInfo info = newGson().fromJson(r.getReader(),
-        ChangeJson.ChangeInfo.class);
+    ChangeInfo info = newGson().fromJson(r.getReader(), ChangeInfo.class);
     ChangeInfo out = get(info.changeId);
 
-    assertEquals(in.branch, out.branch);
-    assertEquals(in.subject, out.subject);
-    assertEquals(in.topic, out.topic);
-    assertEquals(in.status, out.status);
+    assertThat(out.branch).isEqualTo(in.branch);
+    assertThat(out.subject).isEqualTo(in.subject);
+    assertThat(out.topic).isEqualTo(in.topic);
+    assertThat(out.status).isEqualTo(in.status);
+    assertThat(out.revisions).hasSize(1);
+    Boolean draft = Iterables.getOnlyElement(out.revisions.values()).draft;
+    assertThat(booleanToDraftStatus(draft)).isEqualTo(in.status);
+  }
+
+  private ChangeStatus booleanToDraftStatus(Boolean draft) {
+    if (draft == null) {
+      return ChangeStatus.NEW;
+    }
+    return draft ? ChangeStatus.DRAFT : ChangeStatus.NEW;
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftChangeIT.java
deleted file mode 100644
index 3ba5ed7..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftChangeIT.java
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.rest.change;
-
-import static org.junit.Assert.assertEquals;
-
-import com.google.common.collect.Iterables;
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.RestSession;
-import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.ChangeStatus;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwtorm.server.OrmException;
-
-import org.eclipse.jgit.api.errors.GitAPIException;
-import org.junit.Test;
-
-import java.io.IOException;
-
-public class DeleteDraftChangeIT extends AbstractDaemonTest {
-
-  @Test
-  public void deleteChange() throws GitAPIException,
-      IOException, RestApiException {
-    String changeId = createChange().getChangeId();
-    String triplet = "p~master~" + changeId;
-    ChangeInfo c = get(triplet);
-    assertEquals(triplet, c.id);
-    assertEquals(ChangeStatus.NEW, c.status);
-    RestResponse r = deleteChange(changeId, adminSession);
-    assertEquals("Change is not a draft", r.getEntityContent());
-    assertEquals(409, r.getStatusCode());
-  }
-
-  @Test
-  public void deleteDraftChange() throws GitAPIException,
-      IOException, RestApiException, OrmException {
-    String changeId = createDraftChange();
-    String triplet = "p~master~" + changeId;
-    ChangeInfo c = get(triplet);
-    assertEquals(triplet, c.id);
-    assertEquals(ChangeStatus.DRAFT, c.status);
-    RestResponse r = deleteChange(changeId, adminSession);
-    assertEquals(204, r.getStatusCode());
-  }
-
-  @Test
-  public void publishDraftChange() throws GitAPIException,
-      IOException, RestApiException {
-    String changeId = createDraftChange();
-    String triplet = "p~master~" + changeId;
-    ChangeInfo c = get(triplet);
-    assertEquals(triplet, c.id);
-    assertEquals(ChangeStatus.DRAFT, c.status);
-    RestResponse r = publishChange(changeId);
-    assertEquals(204, r.getStatusCode());
-    c = get(triplet);
-    assertEquals(ChangeStatus.NEW, c.status);
-  }
-
-  @Test
-  public void publishDraftPatchSet() throws GitAPIException,
-      IOException, OrmException, RestApiException {
-    String changeId = createDraftChange();
-    String triplet = "p~master~" + changeId;
-    ChangeInfo c = get(triplet);
-    assertEquals(triplet, c.id);
-    assertEquals(ChangeStatus.DRAFT, c.status);
-    RestResponse r = publishPatchSet(changeId);
-    assertEquals(204, r.getStatusCode());
-    assertEquals(ChangeStatus.NEW, get(triplet).status);
-  }
-
-  private String createDraftChange() throws GitAPIException, IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, "refs/drafts/master").getChangeId();
-  }
-
-  private static RestResponse deleteChange(String changeId,
-      RestSession s) throws IOException {
-    return s.delete("/changes/" + changeId);
-  }
-
-  private RestResponse publishChange(String changeId) throws IOException {
-    return adminSession.post("/changes/" + changeId + "/publish");
-  }
-
-  private RestResponse publishPatchSet(String changeId) throws IOException,
-    OrmException {
-    PatchSet patchSet = db.patchSets()
-        .get(Iterables.getOnlyElement(db.changes()
-            .byKey(new Change.Key(changeId)))
-            .currentPatchSetId());
-    return adminSession.post("/changes/"
-        + changeId
-        + "/revisions/"
-        + patchSet.getRevision().get()
-        + "/publish");
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
index 10fbed0..871b1cc 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
-import static org.junit.Assert.assertEquals;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -22,12 +22,13 @@
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.ChangeStatus;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 
+import org.apache.http.HttpStatus;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.junit.Test;
 
@@ -41,11 +42,11 @@
     PatchSet ps = getCurrentPatchSet(changeId);
     String triplet = "p~master~" + changeId;
     ChangeInfo c = get(triplet);
-    assertEquals(triplet, c.id);
-    assertEquals(ChangeStatus.NEW, c.status);
+    assertThat(c.id).isEqualTo(triplet);
+    assertThat(c.status).isEqualTo(ChangeStatus.NEW);
     RestResponse r = deletePatchSet(changeId, ps, adminSession);
-    assertEquals("Patch set is not a draft.", r.getEntityContent());
-    assertEquals(409, r.getStatusCode());
+    assertThat(r.getEntityContent()).isEqualTo("Patch set is not a draft");
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
   }
 
   @Test
@@ -54,11 +55,11 @@
     PatchSet ps = getCurrentPatchSet(changeId);
     String triplet = "p~master~" + changeId;
     ChangeInfo c = get(triplet);
-    assertEquals(triplet, c.id);
-    assertEquals(ChangeStatus.DRAFT, c.status);
+    assertThat(c.id).isEqualTo(triplet);
+    assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
     RestResponse r = deletePatchSet(changeId, ps, userSession);
-    assertEquals("Not found", r.getEntityContent());
-    assertEquals(404, r.getStatusCode());
+    assertThat(r.getEntityContent()).isEqualTo("Not found: " + changeId);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 
   @Test
@@ -67,19 +68,15 @@
     PatchSet ps = getCurrentPatchSet(changeId);
     String triplet = "p~master~" + changeId;
     ChangeInfo c = get(triplet);
-    assertEquals(triplet, c.id);
-    assertEquals(ChangeStatus.DRAFT, c.status);
+    assertThat(c.id).isEqualTo(triplet);
+    assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
     RestResponse r = deletePatchSet(changeId, ps, adminSession);
-    assertEquals(204, r.getStatusCode());
-    Change change = Iterables.getOnlyElement(db.changes().byKey(
-        new Change.Key(changeId)).toList());
-    assertEquals(1, db.patchSets().byChange(change.getId())
-        .toList().size());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+    assertThat(getChange(changeId).patches().size()).isEqualTo(1);
     ps = getCurrentPatchSet(changeId);
     r = deletePatchSet(changeId, ps, adminSession);
-    assertEquals(204, r.getStatusCode());
-    assertEquals(0, db.changes().byKey(new Change.Key(changeId))
-        .toList().size());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+    assertThat(queryProvider.get().byKeyPrefix(changeId)).isEmpty();
   }
 
   private String createDraftChangeWith2PS() throws GitAPIException,
@@ -92,10 +89,11 @@
   }
 
   private PatchSet getCurrentPatchSet(String changeId) throws OrmException {
-    return db.patchSets()
-        .get(Iterables.getOnlyElement(db.changes()
-            .byKey(new Change.Key(changeId)))
-            .currentPatchSetId());
+    return getChange(changeId).currentPatchSet();
+  }
+
+  private ChangeData getChange(String changeId) throws OrmException {
+    return Iterables.getOnlyElement(queryProvider.get().byKeyPrefix(changeId));
   }
 
   private static RestResponse deletePatchSet(String changeId,
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
new file mode 100644
index 0000000..a3809a9
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
@@ -0,0 +1,132 @@
+// 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.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gwtorm.server.OrmException;
+
+import org.apache.http.HttpStatus;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class DraftChangeIT extends AbstractDaemonTest {
+  @ConfigSuite.Config
+  public static Config allowDraftsDisabled() {
+    return allowDraftsDisabledConfig();
+  }
+
+  @Test
+  public void deleteChange() throws Exception {
+    PushOneCommit.Result result = createChange();
+    result.assertOkStatus();
+    String changeId = result.getChangeId();
+    String triplet = "p~master~" + changeId;
+    ChangeInfo c = get(triplet);
+    assertThat(c.id).isEqualTo(triplet);
+    assertThat(c.status).isEqualTo(ChangeStatus.NEW);
+    RestResponse response = deleteChange(changeId, adminSession);
+    assertThat(response.getEntityContent()).isEqualTo("Change is not a draft");
+    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
+  }
+
+  @Test
+  public void deleteDraftChange() throws Exception {
+    assume().that(isAllowDrafts()).isTrue();
+    PushOneCommit.Result result = createDraftChange();
+    result.assertOkStatus();
+    String changeId = result.getChangeId();
+    String triplet = "p~master~" + changeId;
+    ChangeInfo c = get(triplet);
+    assertThat(c.id).isEqualTo(triplet);
+    assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
+    RestResponse response = deleteChange(changeId, adminSession);
+    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+  }
+
+  @Test
+  public void publishDraftChange() throws Exception {
+    assume().that(isAllowDrafts()).isTrue();
+    PushOneCommit.Result result = createDraftChange();
+    result.assertOkStatus();
+    String changeId = result.getChangeId();
+    String triplet = "p~master~" + changeId;
+    ChangeInfo c = get(triplet);
+    assertThat(c.id).isEqualTo(triplet);
+    assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
+    RestResponse response = publishChange(changeId);
+    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+    c = get(triplet);
+    assertThat(c.status).isEqualTo(ChangeStatus.NEW);
+  }
+
+  @Test
+  public void publishDraftPatchSet() throws Exception {
+    assume().that(isAllowDrafts()).isTrue();
+    PushOneCommit.Result result = createDraftChange();
+    result.assertOkStatus();
+    String changeId = result.getChangeId();
+    String triplet = "p~master~" + changeId;
+    ChangeInfo c = get(triplet);
+    assertThat(c.id).isEqualTo(triplet);
+    assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
+    RestResponse response = publishPatchSet(changeId);
+    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+    assertThat(get(triplet).status).isEqualTo(ChangeStatus.NEW);
+  }
+
+  @Test
+  public void createDraftChangeWhenDraftsNotAllowed() throws Exception {
+    assume().that(isAllowDrafts()).isFalse();
+    PushOneCommit.Result r = createDraftChange();
+    r.assertErrorStatus("draft workflow is disabled");
+  }
+
+  private PushOneCommit.Result createDraftChange() throws Exception {
+    return pushTo("refs/drafts/master");
+  }
+
+  private static RestResponse deleteChange(String changeId,
+      RestSession s) throws IOException {
+    return s.delete("/changes/" + changeId);
+  }
+
+  private RestResponse publishChange(String changeId) throws IOException {
+    return adminSession.post("/changes/" + changeId + "/publish");
+  }
+
+  private RestResponse publishPatchSet(String changeId) throws IOException,
+      OrmException {
+    PatchSet patchSet = Iterables.getOnlyElement(
+        queryProvider.get().byKeyPrefix(changeId)).currentPatchSet();
+    return adminSession.post("/changes/"
+        + changeId
+        + "/revisions/"
+        + patchSet.getRevision().get()
+        + "/publish");
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
new file mode 100644
index 0000000..26d6a1e
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
@@ -0,0 +1,233 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.api.changes.HashtagsInput;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gson.reflect.TypeToken;
+
+import org.apache.http.HttpStatus;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+public class HashtagsIT extends AbstractDaemonTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return NotesMigration.allEnabledConfig();
+  }
+
+  private void assertResult(RestResponse r, List<String> expected)
+      throws IOException {
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    List<String> result = toHashtagList(r);
+    assertThat(result).containsExactlyElementsIn(expected);
+  }
+
+  @Test
+  public void testGetNoHashtags() throws Exception {
+    // GET hashtags on a change with no hashtags returns an empty list
+    String changeId = createChange().getChangeId();
+    assertResult(GET(changeId), ImmutableList.<String>of());
+  }
+
+  @Test
+  public void testAddSingleHashtag() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    // POST adding a single hashtag returns a single hashtag
+    List<String> expected = Arrays.asList("tag2");
+    assertResult(POST(changeId, "tag2", null), expected);
+    assertResult(GET(changeId), expected);
+
+    // POST adding another single hashtag to change that already has one
+    // hashtag returns a sorted list of hashtags with existing and new
+    expected = Arrays.asList("tag1", "tag2");
+    assertResult(POST(changeId, "tag1", null), expected);
+    assertResult(GET(changeId), expected);
+  }
+
+  @Test
+  public void testAddMultipleHashtags() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    // POST adding multiple hashtags returns a sorted list of hashtags
+    List<String> expected = Arrays.asList("tag1", "tag3");
+    assertResult(POST(changeId, "tag3, tag1", null), expected);
+    assertResult(GET(changeId), expected);
+
+    // POST adding multiple hashtags to change that already has hashtags
+    // returns a sorted list of hashtags with existing and new
+    expected = Arrays.asList("tag1", "tag2", "tag3", "tag4");
+    assertResult(POST(changeId, "tag2, tag4", null), expected);
+    assertResult(GET(changeId), expected);
+  }
+
+  @Test
+  public void testAddAlreadyExistingHashtag() throws Exception {
+    // POST adding a hashtag that already exists on the change returns a
+    // sorted list of hashtags without duplicates
+    String changeId = createChange().getChangeId();
+    List<String> expected = Arrays.asList("tag2");
+    assertResult(POST(changeId, "tag2", null), expected);
+    assertResult(GET(changeId), expected);
+    assertResult(POST(changeId, "tag2", null), expected);
+    assertResult(GET(changeId), expected);
+    expected = Arrays.asList("tag1", "tag2");
+    assertResult(POST(changeId, "tag2, tag1", null), expected);
+    assertResult(GET(changeId), expected);
+  }
+
+  @Test
+  public void testHashtagsWithPrefix() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    // Leading # is stripped from added tag
+    List<String> expected = Arrays.asList("tag1");
+    assertResult(POST(changeId, "#tag1", null), expected);
+    assertResult(GET(changeId), expected);
+
+    // Leading # is stripped from multiple added tags
+    expected = Arrays.asList("tag1", "tag2", "tag3");
+    assertResult(POST(changeId, "#tag2, #tag3", null), expected);
+    assertResult(GET(changeId), expected);
+
+    // Leading # is stripped from removed tag
+    expected = Arrays.asList("tag1", "tag3");
+    assertResult(POST(changeId, null, "#tag2"), expected);
+    assertResult(GET(changeId), expected);
+
+    // Leading # is stripped from multiple removed tags
+    expected = Collections.emptyList();
+    assertResult(POST(changeId, null, "#tag1, #tag3"), expected);
+    assertResult(GET(changeId), expected);
+
+    // Leading # and space are stripped from added tag
+    expected = Arrays.asList("tag1");
+    assertResult(POST(changeId, "# tag1", null), expected);
+    assertResult(GET(changeId), expected);
+
+    // Multiple leading # are stripped from added tag
+    expected = Arrays.asList("tag1", "tag2");
+    assertResult(POST(changeId, "##tag2", null), expected);
+    assertResult(GET(changeId), expected);
+
+    // Multiple leading spaces and # are stripped from added tag
+    expected = Arrays.asList("tag1", "tag2", "tag3");
+    assertResult(POST(changeId, " # # tag3", null), expected);
+    assertResult(GET(changeId), expected);
+  }
+
+  @Test
+  public void testRemoveSingleHashtag() throws Exception {
+    // POST removing a single tag from a change that only has that tag
+    // returns an empty list
+    String changeId = createChange().getChangeId();
+    List<String> expected = Arrays.asList("tag1");
+    assertResult(POST(changeId, "tag1", null), expected);
+    assertResult(POST(changeId, null, "tag1"), ImmutableList.<String>of());
+    assertResult(GET(changeId), ImmutableList.<String>of());
+
+    // POST removing a single tag from a change that has multiple tags
+    // returns a sorted list of remaining tags
+    expected = Arrays.asList("tag1", "tag2", "tag3");
+    assertResult(POST(changeId, "tag1, tag2, tag3", null), expected);
+    expected = Arrays.asList("tag1", "tag3");
+    assertResult(POST(changeId, null, "tag2"), expected);
+    assertResult(GET(changeId), expected);
+  }
+
+  @Test
+  public void testRemoveMultipleHashtags() throws Exception {
+    // POST removing multiple tags from a change that only has those tags
+    // returns an empty list
+    String changeId = createChange().getChangeId();
+    List<String> expected = Arrays.asList("tag1", "tag2");
+    assertResult(POST(changeId, "tag1, tag2", null), expected);
+    assertResult(POST(changeId, null, "tag1, tag2"), ImmutableList.<String>of());
+    assertResult(GET(changeId), ImmutableList.<String>of());
+
+    // POST removing multiple tags from a change that has multiple changes
+    // returns a sorted list of remaining changes
+    expected = Arrays.asList("tag1", "tag2", "tag3", "tag4");
+    assertResult(POST(changeId, "tag1, tag2, tag3, tag4", null), expected);
+    expected = Arrays.asList("tag2", "tag4");
+    assertResult(POST(changeId, null, "tag1, tag3"), expected);
+    assertResult(GET(changeId), expected);
+  }
+
+  @Test
+  public void testRemoveNotExistingHashtag() throws Exception {
+    // POST removing a single hashtag from change that has no hashtags
+    // returns an empty list
+    String changeId = createChange().getChangeId();
+    assertResult(POST(changeId, null, "tag1"), ImmutableList.<String>of());
+    assertResult(GET(changeId), ImmutableList.<String>of());
+
+    // POST removing a single non-existing tag from a change that only
+    // has one other tag returns a list of only one tag
+    List<String> expected = Arrays.asList("tag1");
+    assertResult(POST(changeId, "tag1", null), expected);
+    assertResult(POST(changeId, null, "tag4"), expected);
+    assertResult(GET(changeId), expected);
+
+    // POST removing a single non-existing tag from a change that has multiple
+    // tags returns a sorted list of tags without any deleted
+    expected = Arrays.asList("tag1", "tag2", "tag3");
+    assertResult(POST(changeId, "tag1, tag2, tag3", null), expected);
+    assertResult(POST(changeId, null, "tag4"), expected);
+    assertResult(GET(changeId), expected);
+  }
+
+  private RestResponse GET(String changeId) throws IOException {
+    return adminSession.get("/changes/" + changeId + "/hashtags/");
+  }
+
+  private RestResponse POST(String changeId, String toAdd, String toRemove)
+      throws IOException {
+    HashtagsInput input = new HashtagsInput();
+    if (toAdd != null) {
+      input.add = new HashSet<>(
+          Lists.newArrayList(Splitter.on(CharMatcher.anyOf(",")).split(toAdd)));
+    }
+    if (toRemove != null) {
+      input.remove = new HashSet<>(
+          Lists.newArrayList(Splitter.on(CharMatcher.anyOf(",")).split(toRemove)));
+    }
+    return adminSession.post("/changes/" + changeId + "/hashtags/", input);
+  }
+
+  private static List<String> toHashtagList(RestResponse r)
+      throws IOException {
+    List<String> result =
+        newGson().fromJson(r.getReader(),
+            new TypeToken<List<String>>() {}.getType());
+    return result;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
new file mode 100644
index 0000000..1b00c39
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+
+public class IndexChangeIT extends AbstractDaemonTest {
+  @Test
+  public void indexChange() throws Exception {
+    String changeId = createChange().getChangeId();
+    RestResponse r = userSession.post("/changes/" + changeId + "/index/");
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+  }
+
+  @Test
+  public void indexChangeOnNonVisibleBranch() throws Exception {
+    String changeId = createChange().getChangeId();
+    blockRead(project, "refs/heads/master");
+    RestResponse r = userSession.post("/changes/" + changeId + "/index/");
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
index 02ecbd8..e649415 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
@@ -14,11 +14,10 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
-import static com.google.gerrit.extensions.common.ListChangesOption.ALL_REVISIONS;
-import static com.google.gerrit.extensions.common.ListChangesOption.CURRENT_REVISION;
-import static com.google.gerrit.extensions.common.ListChangesOption.MESSAGES;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
+import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
@@ -61,36 +60,38 @@
   @Test
   public void noRevisionOptions() throws Exception {
     ChangeInfo c = info(changeId);
-    assertNull(c.currentRevision);
-    assertNull(c.revisions);
+    assertThat(c.currentRevision).isNull();
+    assertThat(c.revisions).isNull();
   }
 
   @Test
   public void currentRevision() throws Exception {
     ChangeInfo c = get(changeId, CURRENT_REVISION);
-    assertEquals(commitId(2), c.currentRevision);
-    assertEquals(ImmutableSet.of(commitId(2)), c.revisions.keySet());
-    assertEquals(3, c.revisions.get(commitId(2))._number);
+    assertThat(c.currentRevision).isEqualTo(commitId(2));
+    assertThat((Iterable<?>)c.revisions.keySet()).containsAllIn(
+        ImmutableSet.of(commitId(2)));
+    assertThat(c.revisions.get(commitId(2))._number).isEqualTo(3);
   }
 
   @Test
   public void currentRevisionAndMessages() throws Exception {
     ChangeInfo c = get(changeId, CURRENT_REVISION, MESSAGES);
-    assertEquals(1, c.revisions.size());
-    assertEquals(commitId(2), c.currentRevision);
-    assertEquals(ImmutableSet.of(commitId(2)), c.revisions.keySet());
-    assertEquals(3, c.revisions.get(commitId(2))._number);
+    assertThat(c.revisions).hasSize(1);
+    assertThat(c.currentRevision).isEqualTo(commitId(2));
+    assertThat((Iterable<?>)c.revisions.keySet()).containsAllIn(
+        ImmutableSet.of(commitId(2)));
+    assertThat(c.revisions.get(commitId(2))._number).isEqualTo(3);
   }
 
   @Test
   public void allRevisions() throws Exception {
     ChangeInfo c = get(changeId, ALL_REVISIONS);
-    assertEquals(commitId(2), c.currentRevision);
-    assertEquals(ImmutableSet.of(commitId(0), commitId(1), commitId(2)),
-        c.revisions.keySet());
-    assertEquals(1, c.revisions.get(commitId(0))._number);
-    assertEquals(2, c.revisions.get(commitId(1))._number);
-    assertEquals(3, c.revisions.get(commitId(2))._number);
+    assertThat(c.currentRevision).isEqualTo(commitId(2));
+    assertThat((Iterable<?>)c.revisions.keySet()).containsAllIn(
+        ImmutableSet.of(commitId(0), commitId(1), commitId(2)));
+    assertThat(c.revisions.get(commitId(0))._number).isEqualTo(1);
+    assertThat(c.revisions.get(commitId(1))._number).isEqualTo(2);
+    assertThat(c.revisions.get(commitId(2))._number).isEqualTo(3);
   }
 
   private String commitId(int i) {
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
index 12d002e..4863c3e 100644
--- 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
@@ -14,22 +14,20 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.checkout;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertSame;
 
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gwtorm.server.OrmException;
-import com.google.gerrit.extensions.common.SubmitType;
-
-import com.jcraft.jsch.JSchException;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.ChangeInfo;
 
 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;
 import java.util.List;
 
 public class SubmitByCherryPickIT extends AbstractSubmit {
@@ -40,14 +38,13 @@
   }
 
   @Test
-  public void submitWithCherryPickIfFastForwardPossible() throws JSchException,
-      IOException, GitAPIException {
+  public void submitWithCherryPickIfFastForwardPossible() throws Exception {
     Git git = createProject();
     PushOneCommit.Result change = createChange(git);
     submit(change.getChangeId());
     assertCherryPick(git, false);
-    assertEquals(change.getCommit().getParent(0),
-        getRemoteHead().getParent(0));
+    assertThat(getRemoteHead().getParent(0))
+      .isEqualTo(change.getCommit().getParent(0));
   }
 
   @Test
@@ -65,8 +62,8 @@
     submit(change2.getChangeId());
     assertCherryPick(git, false);
     RevCommit newHead = getRemoteHead();
-    assertEquals(1, newHead.getParentCount());
-    assertEquals(oldHead, newHead.getParent(0));
+    assertThat(newHead.getParentCount()).isEqualTo(1);
+    assertThat(newHead.getParent(0)).isEqualTo(oldHead);
     assertCurrentRevision(change2.getChangeId(), 2, newHead);
     assertSubmitter(change2.getChangeId(), 1);
     assertSubmitter(change2.getChangeId(), 2);
@@ -90,7 +87,7 @@
     submit(change3.getChangeId());
     assertCherryPick(git, true);
     RevCommit newHead = getRemoteHead();
-    assertEquals(oldHead, newHead.getParent(0));
+    assertThat(newHead.getParent(0)).isEqualTo(oldHead);
     assertApproved(change3.getChangeId());
     assertCurrentRevision(change3.getChangeId(), 2, newHead);
     assertSubmitter(change2.getChangeId(), 1);
@@ -111,7 +108,7 @@
     PushOneCommit.Result change2 =
         createChange(git, "Change 2", "a.txt", "other content");
     submitWithConflict(change2.getChangeId());
-    assertEquals(oldHead, getRemoteHead());
+    assertThat(getRemoteHead()).isEqualTo(oldHead);
     assertCurrentRevision(change2.getChangeId(), 1, change2.getCommitId());
     assertSubmitter(change2.getChangeId(), 1);
   }
@@ -132,7 +129,7 @@
     submit(change3.getChangeId());
     assertCherryPick(git, false);
     RevCommit newHead = getRemoteHead();
-    assertEquals(oldHead, newHead.getParent(0));
+    assertThat(newHead.getParent(0)).isEqualTo(oldHead);
     assertApproved(change3.getChangeId());
     assertCurrentRevision(change3.getChangeId(), 2, newHead);
     assertSubmitter(change3.getChangeId(), 1);
@@ -153,14 +150,13 @@
     PushOneCommit.Result change3 =
         createChange(git, "Change 3", "b.txt", "different content");
     submitWithConflict(change3.getChangeId());
-    assertEquals(oldHead, getRemoteHead());
+    assertThat(getRemoteHead()).isEqualTo(oldHead);
     assertCurrentRevision(change3.getChangeId(), 1, change3.getCommitId());
     assertSubmitter(change3.getChangeId(), 1);
   }
 
   @Test
-  public void submitMultipleChanges()
-      throws JSchException, IOException, GitAPIException, OrmException {
+  public void submitMultipleChanges() throws Exception {
     Git git = createProject();
     RevCommit initialHead = getRemoteHead();
 
@@ -178,21 +174,130 @@
     submit(change4.getChangeId());
 
     List<RevCommit> log = getRemoteLog();
-    assertEquals(
-        change4.getCommit().getShortMessage(),
-        log.get(0).getShortMessage());
-    assertSame(log.get(1), log.get(0).getParent(0));
+    assertThat(log.get(0).getShortMessage()).isEqualTo(
+        change4.getCommit().getShortMessage());
+    assertThat(log.get(0).getParent(0)).isEqualTo(log.get(1));
 
-    assertEquals(
-        change3.getCommit().getShortMessage(),
-        log.get(1).getShortMessage());
-    assertSame(log.get(2), log.get(1).getParent(0));
+    assertThat(log.get(1).getShortMessage()).isEqualTo(
+        change3.getCommit().getShortMessage());
+    assertThat(log.get(1).getParent(0)).isEqualTo(log.get(2));
 
-    assertEquals(
-        change2.getCommit().getShortMessage(),
-        log.get(2).getShortMessage());
-    assertSame(log.get(3), log.get(2).getParent(0));
+    assertThat(log.get(2).getShortMessage()).isEqualTo(
+        change2.getCommit().getShortMessage());
+    assertThat(log.get(2).getParent(0)).isEqualTo(log.get(3));
 
-    assertEquals(initialHead.getId(), log.get(3).getId());
+    assertThat(log.get(3).getId()).isEqualTo(initialHead.getId());
+  }
+
+  @Test
+  public void submitDependentNonConflictingChangesOutOfOrder() throws Exception {
+    Git git = createProject();
+    RevCommit initialHead = getRemoteHead();
+
+    checkout(git, initialHead.getId().getName());
+    PushOneCommit.Result change2 = createChange(git, "Change 2", "b", "b");
+    PushOneCommit.Result change3 = createChange(git, "Change 3", "c", "c");
+    assertThat(change3.getCommit().getParent(0)).isEqualTo(change2.getCommit());
+
+    // Submit succeeds; change3 is successfully cherry-picked onto head.
+    submit(change3.getChangeId());
+    // Submit succeeds; change2 is successfully cherry-picked onto head
+    // (which was change3's cherry-pick).
+    submit(change2.getChangeId());
+
+    // change2 is the new tip.
+    List<RevCommit> log = getRemoteLog();
+    assertThat(log.get(0).getShortMessage()).isEqualTo(
+        change2.getCommit().getShortMessage());
+    assertThat(log.get(0).getParent(0)).isEqualTo(log.get(1));
+
+    assertThat(log.get(1).getShortMessage()).isEqualTo(
+        change3.getCommit().getShortMessage());
+    assertThat(log.get(1).getParent(0)).isEqualTo(log.get(2));
+
+    assertThat(log.get(2).getId()).isEqualTo(initialHead.getId());
+  }
+
+  @Test
+  public void submitDependentConflictingChangesOutOfOrder() throws Exception {
+    Git git = createProject();
+    RevCommit initialHead = getRemoteHead();
+
+    checkout(git, initialHead.getId().getName());
+    PushOneCommit.Result change2 = createChange(git, "Change 2", "b", "b1");
+    PushOneCommit.Result change3 = createChange(git, "Change 3", "b", "b2");
+    assertThat(change3.getCommit().getParent(0)).isEqualTo(change2.getCommit());
+
+    // Submit fails; change3 contains the delta "b1" -> "b2", which cannot be
+    // applied against tip.
+    submitWithConflict(change3.getChangeId());
+
+    ChangeInfo info3 = get(change3.getChangeId(), ListChangesOption.MESSAGES);
+    assertThat(info3.status).isEqualTo(ChangeStatus.NEW);
+    assertThat(Iterables.getLast(info3.messages).message.toLowerCase())
+        .contains("path conflict");
+
+    // Tip has not changed.
+    List<RevCommit> log = getRemoteLog();
+    assertThat(log.get(0)).isEqualTo(initialHead.getId());
+  }
+
+  @Test
+  public void submitSubsetOfDependentChanges() throws Exception {
+    Git git = createProject();
+    RevCommit initialHead = getRemoteHead();
+
+    checkout(git, initialHead.getId().getName());
+    createChange(git, "Change 2", "b", "b");
+    PushOneCommit.Result change3 = createChange(git, "Change 3", "c", "c");
+    createChange(git, "Change 4", "d", "d");
+    PushOneCommit.Result change5 = createChange(git, "Change 5", "e", "e");
+
+    // Out of the above, only submit 3 and 5.
+    submitStatusOnly(change3.getChangeId());
+    submit(change5.getChangeId());
+
+    ChangeInfo info3 = get(change3.getChangeId());
+    assertThat(info3.status).isEqualTo(ChangeStatus.MERGED);
+
+    List<RevCommit> log = getRemoteLog();
+    assertThat(log.get(0).getShortMessage())
+        .isEqualTo(change5.getCommit().getShortMessage());
+    assertThat(log.get(1).getShortMessage())
+        .isEqualTo(change3.getCommit().getShortMessage());
+    assertThat(log.get(2).getShortMessage())
+        .isEqualTo(initialHead.getShortMessage());
+  }
+
+  @Test
+  public void submitChangeAfterParentFailsDueToConflict() throws Exception {
+    Git git = createProject();
+    RevCommit initialHead = getRemoteHead();
+
+    checkout(git, initialHead.getId().getName());
+    PushOneCommit.Result change2 = createChange(git, "Change 2", "b", "b1");
+    submit(change2.getChangeId());
+
+    checkout(git, initialHead.getId().getName());
+    PushOneCommit.Result change3 = createChange(git, "Change 3", "b", "b2");
+    assertThat(change3.getCommit().getParent(0)).isEqualTo(initialHead);
+    PushOneCommit.Result change4 = createChange(git, "Change 3", "c", "c3");
+
+    submitStatusOnly(change3.getChangeId());
+    submitStatusOnly(change4.getChangeId());
+
+    // Merge fails; change3 contains the delta "b1" -> "b2", which cannot be
+    // applied against tip.
+    submitWithConflict(change3.getChangeId());
+
+    // change4 is a clean merge, so should succeed in the same run where change3
+    // failed.
+    ChangeInfo info4 = get(change4.getChangeId());
+    assertThat(info4.status).isEqualTo(ChangeStatus.MERGED);
+    List<RevCommit> log = getRemoteLog();
+    assertThat(log.get(0).getShortMessage())
+        .isEqualTo(change4.getCommit().getShortMessage());
+    assertThat(log.get(1).getShortMessage())
+        .isEqualTo(change2.getCommit().getShortMessage());
   }
 }
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
index 2ef4ecc..d4e7e849 100644
--- 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
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.checkout;
-import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.SubmitType;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -38,8 +38,8 @@
     PushOneCommit.Result change = createChange(git);
     submit(change.getChangeId());
     RevCommit head = getRemoteHead();
-    assertEquals(change.getCommitId(), head.getId());
-    assertEquals(oldHead, head.getParent(0));
+    assertThat(head.getId()).isEqualTo(change.getCommitId());
+    assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertSubmitter(change.getChangeId(), 1);
   }
 
@@ -56,7 +56,7 @@
     PushOneCommit.Result change2 =
         createChange(git, "Change 2", "b.txt", "other content");
     submitWithConflict(change2.getChangeId());
-    assertEquals(oldHead, getRemoteHead());
+    assertThat(getRemoteHead()).isEqualTo(oldHead);
     assertSubmitter(change.getChangeId(), 1);
   }
 }
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
index 9512c7a..fa913d9 100644
--- 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
@@ -14,21 +14,16 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.checkout;
-import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.extensions.common.SubmitType;
-import com.google.gwtorm.server.OrmException;
-
-import com.jcraft.jsch.JSchException;
+import com.google.gerrit.extensions.client.SubmitType;
 
 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;
 import java.util.List;
 
 public class SubmitByMergeAlwaysIT extends AbstractSubmitByMerge {
@@ -45,15 +40,14 @@
     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));
+    assertThat(head.getParentCount()).isEqualTo(2);
+    assertThat(head.getParent(0)).isEqualTo(oldHead);
+    assertThat(head.getParent(1)).isEqualTo(change.getCommitId());
     assertSubmitter(change.getChangeId(), 1);
   }
 
   @Test
-  public void submitMultipleChanges()
-      throws JSchException, IOException, GitAPIException, OrmException {
+  public void submitMultipleChanges() throws Exception {
     Git git = createProject();
     RevCommit initialHead = getRemoteHead();
 
@@ -72,20 +66,17 @@
 
     List<RevCommit> log = getRemoteLog();
     RevCommit tip = log.get(0);
-    assertEquals(
-        change4.getCommit().getShortMessage(),
-        tip.getParent(1).getShortMessage());
+    assertThat(tip.getParent(1).getShortMessage()).isEqualTo(
+        change4.getCommit().getShortMessage());
 
     tip = tip.getParent(0);
-    assertEquals(
-        change3.getCommit().getShortMessage(),
-        tip.getParent(1).getShortMessage());
+    assertThat(tip.getParent(1).getShortMessage()).isEqualTo(
+        change3.getCommit().getShortMessage());
 
     tip = tip.getParent(0);
-    assertEquals(
-        change2.getCommit().getShortMessage(),
-        tip.getParent(1).getShortMessage());
+    assertThat(tip.getParent(1).getShortMessage()).isEqualTo(
+        change2.getCommit().getShortMessage());
 
-    assertEquals(initialHead.getId(), tip.getParent(0).getId());
+    assertThat(tip.getParent(0).getId()).isEqualTo(initialHead.getId());
   }
 }
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
index fde462b..c1ece45 100644
--- 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
@@ -1,20 +1,15 @@
 package com.google.gerrit.acceptance.rest.change;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.checkout;
-import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.extensions.common.SubmitType;
-import com.google.gwtorm.server.OrmException;
-
-import com.jcraft.jsch.JSchException;
+import com.google.gerrit.extensions.client.SubmitType;
 
 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;
 import java.util.List;
 
 public class SubmitByMergeIfNecessaryIT extends AbstractSubmitByMerge {
@@ -31,14 +26,13 @@
     PushOneCommit.Result change = createChange(git);
     submit(change.getChangeId());
     RevCommit head = getRemoteHead();
-    assertEquals(change.getCommitId(), head.getId());
-    assertEquals(oldHead, head.getParent(0));
+    assertThat(head.getId()).isEqualTo(change.getCommitId());
+    assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertSubmitter(change.getChangeId(), 1);
   }
 
   @Test
-  public void submitMultipleChanges()
-      throws JSchException, IOException, GitAPIException, OrmException {
+  public void submitMultipleChanges() throws Exception {
     Git git = createProject();
     RevCommit initialHead = getRemoteHead();
 
@@ -57,20 +51,17 @@
 
     List<RevCommit> log = getRemoteLog();
     RevCommit tip = log.get(0);
-    assertEquals(
-        change4.getCommit().getShortMessage(),
-        tip.getParent(1).getShortMessage());
+    assertThat(tip.getParent(1).getShortMessage()).isEqualTo(
+        change4.getCommit().getShortMessage());
 
     tip = tip.getParent(0);
-    assertEquals(
-        change3.getCommit().getShortMessage(),
-        tip.getParent(1).getShortMessage());
+    assertThat(tip.getParent(1).getShortMessage()).isEqualTo(
+        change3.getCommit().getShortMessage());
 
     tip = tip.getParent(0);
-    assertEquals(
-        change2.getCommit().getShortMessage(),
-        tip.getShortMessage());
+    assertThat(tip.getShortMessage()).isEqualTo(
+        change2.getCommit().getShortMessage());
 
-    assertEquals(initialHead.getId(), tip.getParent(0).getId());
+    assertThat(tip.getParent(0).getId()).isEqualTo(initialHead.getId());
   }
 }
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
index 7b4d23d..d3e8cb5 100644
--- 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
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.checkout;
-import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.SubmitType;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -38,8 +38,8 @@
     PushOneCommit.Result change = createChange(git);
     submit(change.getChangeId());
     RevCommit head = getRemoteHead();
-    assertEquals(change.getCommitId(), head.getId());
-    assertEquals(oldHead, head.getParent(0));
+    assertThat(head.getId()).isEqualTo(change.getCommitId());
+    assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertApproved(change.getChangeId());
     assertCurrentRevision(change.getChangeId(), 1, head);
     assertSubmitter(change.getChangeId(), 1);
@@ -60,7 +60,7 @@
     submit(change2.getChangeId());
     assertRebase(git, false);
     RevCommit head = getRemoteHead();
-    assertEquals(oldHead, head.getParent(0));
+    assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertApproved(change2.getChangeId());
     assertCurrentRevision(change2.getChangeId(), 2, head);
     assertSubmitter(change2.getChangeId(), 1);
@@ -85,7 +85,7 @@
     submit(change3.getChangeId());
     assertRebase(git, true);
     RevCommit head = getRemoteHead();
-    assertEquals(oldHead, head.getParent(0));
+    assertThat(head.getParent(0)).isEqualTo(oldHead);
     assertApproved(change3.getChangeId());
     assertCurrentRevision(change3.getChangeId(), 2, head);
     assertSubmitter(change3.getChangeId(), 1);
@@ -107,7 +107,7 @@
         createChange(git, "Change 2", "a.txt", "other content");
     submitWithConflict(change2.getChangeId());
     RevCommit head = getRemoteHead();
-    assertEquals(oldHead, head);
+    assertThat(head).isEqualTo(oldHead);
     assertCurrentRevision(change2.getChangeId(), 1, change2.getCommitId());
     assertSubmitter(change2.getChangeId(), 1);
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index 7266c97..ec1e299 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -14,8 +14,7 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -27,18 +26,15 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.CreateGroupArgs;
 import com.google.gerrit.server.account.PerformCreateGroup;
-import com.google.gerrit.server.change.SuggestReviewers.SuggestedReviewerInfo;
-import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
 
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -50,15 +46,6 @@
   @Inject
   private PerformCreateGroup.Factory createGroupFactory;
 
-  @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
-
-  @Inject
-  private AllProjectsName allProjects;
-
-  @Inject
-  private ProjectCache projectCache;
-
   private AccountGroup group1;
   private TestAccount user1;
   private TestAccount user2;
@@ -78,11 +65,10 @@
 
   @Test
   @GerritConfig(name = "suggest.accounts", value = "false")
-  public void suggestReviewersNoResult1() throws GitAPIException, IOException,
-      Exception {
+  public void suggestReviewersNoResult1() throws Exception {
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
-    assertEquals(reviewers.size(), 0);
+    assertThat(reviewers).isEmpty();
   }
 
   @Test
@@ -91,32 +77,29 @@
        @GerritConfig(name = "suggest.from", value = "1"),
        @GerritConfig(name = "accounts.visibility", value = "NONE")
       })
-  public void suggestReviewersNoResult2() throws GitAPIException, IOException,
-      Exception {
+  public void suggestReviewersNoResult2() throws Exception {
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
-    assertEquals(reviewers.size(), 0);
+    assertThat(reviewers).isEmpty();
   }
 
   @Test
   @GerritConfig(name = "suggest.from", value = "2")
-  public void suggestReviewersNoResult3() throws GitAPIException, IOException,
-      Exception {
+  public void suggestReviewersNoResult3() throws Exception {
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
-    assertEquals(reviewers.size(), 0);
+    assertThat(reviewers).isEmpty();
   }
 
   @Test
-  public void suggestReviewersChange() throws GitAPIException,
-      IOException, Exception {
+  public void suggestReviewersChange() throws Exception {
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
-    assertEquals(reviewers.size(), 6);
+    assertThat(reviewers).hasSize(6);
     reviewers = suggestReviewers(changeId, "u", 5);
-    assertEquals(reviewers.size(), 5);
+    assertThat(reviewers).hasSize(5);
     reviewers = suggestReviewers(changeId, "users3", 10);
-    assertEquals(reviewers.size(), 1);
+    assertThat(reviewers).hasSize(1);
   }
 
   @Test
@@ -126,22 +109,22 @@
     List<SuggestedReviewerInfo> reviewers;
 
     reviewers = suggestReviewers(changeId, "user2", 2);
-    assertEquals(1, reviewers.size());
-    assertEquals("User2", Iterables.getOnlyElement(reviewers).account.name);
+    assertThat(reviewers).hasSize(1);
+    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo("User2");
 
     reviewers = suggestReviewers(new RestSession(server, user1),
         changeId, "user2", 2);
-    assertTrue(reviewers.isEmpty());
+    assertThat(reviewers).isEmpty();
 
     reviewers = suggestReviewers(new RestSession(server, user2),
         changeId, "user2", 2);
-    assertEquals(1, reviewers.size());
-    assertEquals("User2", Iterables.getOnlyElement(reviewers).account.name);
+    assertThat(reviewers).hasSize(1);
+    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo("User2");
 
     reviewers = suggestReviewers(new RestSession(server, user3),
         changeId, "user2", 2);
-    assertEquals(1, reviewers.size());
-    assertEquals("User2", Iterables.getOnlyElement(reviewers).account.name);
+    assertThat(reviewers).hasSize(1);
+    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo("User2");
   }
 
   @Test
@@ -152,13 +135,58 @@
 
     reviewers = suggestReviewers(new RestSession(server, user1),
         changeId, "user2", 2);
-    assertTrue(reviewers.isEmpty());
+    assertThat(reviewers).isEmpty();
 
     grantCapability(GlobalCapability.VIEW_ALL_ACCOUNTS, group1);
     reviewers = suggestReviewers(new RestSession(server, user1),
         changeId, "user2", 2);
-    assertEquals(1, reviewers.size());
-    assertEquals("User2", Iterables.getOnlyElement(reviewers).account.name);
+    assertThat(reviewers).hasSize(1);
+    assertThat(Iterables.getOnlyElement(reviewers).account.name).isEqualTo("User2");
+  }
+
+  @Test
+  @GerritConfig(name = "suggest.maxSuggestedReviewers", value = "2")
+  public void suggestReviewersMaxNbrSuggestions() throws Exception {
+    String changeId = createChange().getChangeId();
+    List<SuggestedReviewerInfo> reviewers =
+        suggestReviewers(changeId, "user", 5);
+    assertThat(reviewers).hasSize(2);
+  }
+
+  @Test
+  @GerritConfig(name = "suggest.fullTextSearch", value = "true")
+  public void suggestReviewersFullTextSearch() throws Exception {
+    String changeId = createChange().getChangeId();
+    List<SuggestedReviewerInfo> reviewers =
+        suggestReviewers(changeId, "ser", 5);
+    assertThat(reviewers).hasSize(4);
+  }
+
+  @Test
+  @GerritConfigs(
+      {@GerritConfig(name = "suggest.fulltextsearch", value = "true"),
+       @GerritConfig(name = "suggest.fullTextSearchMaxMatches", value = "2")
+  })
+  public void suggestReviewersFullTextSearchLimitMaxMatches() throws Exception {
+    String changeId = createChange().getChangeId();
+    List<SuggestedReviewerInfo> reviewers =
+        suggestReviewers(changeId, "ser", 3);
+    assertThat(reviewers).hasSize(2);
+  }
+
+  @Test
+  public void suggestReviewersWithoutLimitOptionSpecified() throws Exception {
+    String changeId = createChange().getChangeId();
+    String query = "users3";
+    List<SuggestedReviewerInfo> suggestedReviewerInfos = newGson().fromJson(
+        adminSession.get("/changes/"
+            + changeId
+            + "/suggest_reviewers?q="
+            + query)
+            .getReader(),
+        new TypeToken<List<SuggestedReviewerInfo>>() {}
+        .getType());
+    assertThat(suggestedReviewerInfos).hasSize(1);
   }
 
   private List<SuggestedReviewerInfo> suggestReviewers(RestSession session,
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
index d2174bc..304abc8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/CacheOperationsIT.java
@@ -14,26 +14,21 @@
 
 package com.google.gerrit.acceptance.rest.config;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.config.PostCaches.Operation.FLUSH;
 import static com.google.gerrit.server.config.PostCaches.Operation.FLUSH_ALL;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.Util.allow;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.config.ListCaches.CacheInfo;
-import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.PostCaches;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.inject.Inject;
+import com.google.gerrit.server.project.Util;
 
 import org.apache.http.HttpStatus;
 import org.junit.Test;
@@ -43,115 +38,106 @@
 
 public class CacheOperationsIT extends AbstractDaemonTest {
 
-  @Inject
-  private ProjectCache projectCache;
-
-  @Inject
-  private AllProjectsName allProjects;
-
-  @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
-
   @Test
-  public void flushAll() throws IOException {
+  public void flushAll() throws Exception {
     RestResponse r = adminSession.get("/config/server/caches/project_list");
     CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
-    assertTrue(cacheInfo.entries.mem.longValue() > 0);
+    assertThat(cacheInfo.entries.mem.longValue()).isGreaterThan((long) 0);
 
     r = adminSession.post("/config/server/caches/", new PostCaches.Input(FLUSH_ALL));
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     r.consume();
 
     r = adminSession.get("/config/server/caches/project_list");
     cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
-    assertNull(cacheInfo.entries.mem);
+    assertThat(cacheInfo.entries.mem).isNull();
   }
 
   @Test
-  public void flushAll_Forbidden() throws IOException {
+  public void flushAll_Forbidden() throws Exception {
     RestResponse r = userSession.post("/config/server/caches/",
         new PostCaches.Input(FLUSH_ALL));
-    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
   }
 
   @Test
-  public void flushAll_BadRequest() throws IOException {
+  public void flushAll_BadRequest() throws Exception {
     RestResponse r = adminSession.post("/config/server/caches/",
         new PostCaches.Input(FLUSH_ALL, Arrays.asList("projects")));
-    assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
   }
 
   @Test
-  public void flush() throws IOException {
+  public void flush() throws Exception {
     RestResponse r = adminSession.get("/config/server/caches/project_list");
     CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
-    assertTrue(cacheInfo.entries.mem.longValue() > 0);
+    assertThat(cacheInfo.entries.mem.longValue()).isGreaterThan((long)0);
 
     r = adminSession.get("/config/server/caches/projects");
     cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
-    assertTrue(cacheInfo.entries.mem.longValue() > 1);
+    assertThat(cacheInfo.entries.mem.longValue()).isGreaterThan((long)1);
 
     r = adminSession.post("/config/server/caches/",
         new PostCaches.Input(FLUSH, Arrays.asList("accounts", "project_list")));
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     r.consume();
 
     r = adminSession.get("/config/server/caches/project_list");
     cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
-    assertNull(cacheInfo.entries.mem);
+    assertThat(cacheInfo.entries.mem).isNull();
 
     r = adminSession.get("/config/server/caches/projects");
     cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
-    assertTrue(cacheInfo.entries.mem.longValue() > 1);
+    assertThat(cacheInfo.entries.mem.longValue()).isGreaterThan((long)1);
   }
 
   @Test
-  public void flush_Forbidden() throws IOException {
+  public void flush_Forbidden() throws Exception {
     RestResponse r = userSession.post("/config/server/caches/",
         new PostCaches.Input(FLUSH, Arrays.asList("projects")));
-    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
   }
 
   @Test
-  public void flush_BadRequest() throws IOException {
+  public void flush_BadRequest() throws Exception {
     RestResponse r = adminSession.post("/config/server/caches/",
         new PostCaches.Input(FLUSH));
-    assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
   }
 
   @Test
-  public void flush_UnprocessableEntity() throws IOException {
+  public void flush_UnprocessableEntity() throws Exception {
     RestResponse r = adminSession.get("/config/server/caches/projects");
     CacheInfo cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
-    assertTrue(cacheInfo.entries.mem.longValue() > 0);
+    assertThat(cacheInfo.entries.mem.longValue()).isGreaterThan((long)0);
 
     r = adminSession.post("/config/server/caches/",
         new PostCaches.Input(FLUSH, Arrays.asList("projects", "unprocessable")));
-    assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
     r.consume();
 
     r = adminSession.get("/config/server/caches/projects");
     cacheInfo = newGson().fromJson(r.getReader(), CacheInfo.class);
-    assertTrue(cacheInfo.entries.mem.longValue() > 0);
+    assertThat(cacheInfo.entries.mem.longValue()).isGreaterThan((long)0);
   }
 
   @Test
-  public void flushWebSessions_Forbidden() throws IOException {
+  public void flushWebSessions_Forbidden() throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
     AccountGroup.UUID registeredUsers =
         SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
-    allow(cfg, GlobalCapability.VIEW_CACHES, registeredUsers);
-    allow(cfg, GlobalCapability.FLUSH_CACHES, registeredUsers);
+    Util.allow(cfg, GlobalCapability.VIEW_CACHES, registeredUsers);
+    Util.allow(cfg, GlobalCapability.FLUSH_CACHES, registeredUsers);
     saveProjectConfig(cfg);
 
     RestResponse r = userSession.post("/config/server/caches/",
         new PostCaches.Input(FLUSH, Arrays.asList("projects")));
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     r.consume();
 
     r = userSession.post("/config/server/caches/",
         new PostCaches.Input(FLUSH, Arrays.asList("web_sessions")));
-    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
   }
 
   private void saveProjectConfig(ProjectConfig cfg) throws IOException {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
index aa8d7ba..9f7b419 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
@@ -14,23 +14,18 @@
 
 package com.google.gerrit.acceptance.rest.config;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.Util.allow;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.ListCaches.CacheInfo;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.inject.Inject;
+import com.google.gerrit.server.project.Util;
 
 import org.apache.http.HttpStatus;
 import org.junit.Test;
@@ -39,69 +34,60 @@
 
 public class FlushCacheIT extends AbstractDaemonTest {
 
-  @Inject
-  private ProjectCache projectCache;
-
-  @Inject
-  private AllProjectsName allProjects;
-
-  @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
-
   @Test
-  public void flushCache() throws IOException {
+  public void flushCache() throws Exception {
     RestResponse r = adminSession.get("/config/server/caches/groups");
     CacheInfo result = newGson().fromJson(r.getReader(), CacheInfo.class);
-    assertTrue(result.entries.mem.longValue() > 0);
+    assertThat(result.entries.mem.longValue()).isGreaterThan((long)0);
 
     r = adminSession.post("/config/server/caches/groups/flush");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     r.consume();
 
     r = adminSession.get("/config/server/caches/groups");
     result = newGson().fromJson(r.getReader(), CacheInfo.class);
-    assertNull(result.entries.mem);
+    assertThat(result.entries.mem).isNull();
   }
 
   @Test
-  public void flushCache_Forbidden() throws IOException {
+  public void flushCache_Forbidden() throws Exception {
     RestResponse r = userSession.post("/config/server/caches/accounts/flush");
-    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
   }
 
   @Test
-  public void flushCache_NotFound() throws IOException {
+  public void flushCache_NotFound() throws Exception {
     RestResponse r = adminSession.post("/config/server/caches/nonExisting/flush");
-    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 
   @Test
-  public void flushCacheWithGerritPrefix() throws IOException {
+  public void flushCacheWithGerritPrefix() throws Exception {
     RestResponse r = adminSession.post("/config/server/caches/gerrit-accounts/flush");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
   }
 
   @Test
-  public void flushWebSessionsCache() throws IOException {
+  public void flushWebSessionsCache() throws Exception {
     RestResponse r = adminSession.post("/config/server/caches/web_sessions/flush");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
   }
 
   @Test
-  public void flushWebSessionsCache_Forbidden() throws IOException {
+  public void flushWebSessionsCache_Forbidden() throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
     AccountGroup.UUID registeredUsers =
         SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
-    allow(cfg, GlobalCapability.VIEW_CACHES, registeredUsers);
-    allow(cfg, GlobalCapability.FLUSH_CACHES, registeredUsers);
+    Util.allow(cfg, GlobalCapability.VIEW_CACHES, registeredUsers);
+    Util.allow(cfg, GlobalCapability.FLUSH_CACHES, registeredUsers);
     saveProjectConfig(cfg);
 
     RestResponse r = userSession.post("/config/server/caches/accounts/flush");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     r.consume();
 
     r = userSession.post("/config/server/caches/web_sessions/flush");
-    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
   }
 
   private void saveProjectConfig(ProjectConfig cfg) throws IOException {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetCacheIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetCacheIT.java
index 398d0c8..f59752c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetCacheIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetCacheIT.java
@@ -14,10 +14,7 @@
 
 package com.google.gerrit.acceptance.rest.config;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
@@ -27,49 +24,47 @@
 import org.apache.http.HttpStatus;
 import org.junit.Test;
 
-import java.io.IOException;
-
 public class GetCacheIT extends AbstractDaemonTest {
 
   @Test
-  public void getCache() throws IOException {
+  public void getCache() throws Exception {
     RestResponse r = adminSession.get("/config/server/caches/accounts");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     CacheInfo result = newGson().fromJson(r.getReader(), CacheInfo.class);
 
-    assertEquals("accounts", result.name);
-    assertEquals(CacheType.MEM, result.type);
-    assertEquals(1, result.entries.mem.longValue());
-    assertNotNull(result.averageGet);
-    assertTrue(result.averageGet.endsWith("s"));
-    assertNull(result.entries.disk);
-    assertNull(result.entries.space);
-    assertTrue(result.hitRatio.mem >= 0);
-    assertTrue(result.hitRatio.mem <= 100);
-    assertNull(result.hitRatio.disk);
+    assertThat(result.name).isEqualTo("accounts");
+    assertThat(result.type).isEqualTo(CacheType.MEM);
+    assertThat(result.entries.mem.longValue()).isEqualTo(1);
+    assertThat(result.averageGet).isNotNull();
+    assertThat(result.averageGet).endsWith("s");
+    assertThat(result.entries.disk).isNull();
+    assertThat(result.entries.space).isNull();
+    assertThat(result.hitRatio.mem).isAtLeast(0);
+    assertThat(result.hitRatio.mem).isAtMost(100);
+    assertThat(result.hitRatio.disk).isNull();
 
     userSession.get("/config/server/version").consume();
     r = adminSession.get("/config/server/caches/accounts");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     result = newGson().fromJson(r.getReader(), CacheInfo.class);
-    assertEquals(2, result.entries.mem.longValue());
+    assertThat(result.entries.mem.longValue()).isEqualTo(2);
   }
 
   @Test
-  public void getCache_Forbidden() throws IOException {
+  public void getCache_Forbidden() throws Exception {
     RestResponse r = userSession.get("/config/server/caches/accounts");
-    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
   }
 
   @Test
-  public void getCache_NotFound() throws IOException {
+  public void getCache_NotFound() throws Exception {
     RestResponse r = adminSession.get("/config/server/caches/nonExisting");
-    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 
   @Test
-  public void getCacheWithGerritPrefix() throws IOException {
+  public void getCacheWithGerritPrefix() throws Exception {
     RestResponse r = adminSession.get("/config/server/caches/gerrit-accounts");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetTaskIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
index 2feeab8..acd900c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/GetTaskIT.java
@@ -14,8 +14,7 @@
 
 package com.google.gerrit.acceptance.rest.config;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
@@ -25,33 +24,32 @@
 import org.apache.http.HttpStatus;
 import org.junit.Test;
 
-import java.io.IOException;
 import java.util.List;
 
 public class GetTaskIT extends AbstractDaemonTest {
 
   @Test
-  public void getTask() throws IOException {
+  public void getTask() throws Exception {
     RestResponse r =
         adminSession.get("/config/server/tasks/" + getLogFileCompressorTaskId());
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     TaskInfo info =
         newGson().fromJson(r.getReader(),
             new TypeToken<TaskInfo>() {}.getType());
-    assertNotNull(info.id);
+    assertThat(info.id).isNotNull();
     Long.parseLong(info.id, 16);
-    assertEquals("Log File Compressor", info.command);
-    assertNotNull(info.startTime);
+    assertThat(info.command).isEqualTo("Log File Compressor");
+    assertThat(info.startTime).isNotNull();
   }
 
   @Test
-  public void getTask_NotFound() throws IOException {
+  public void getTask_NotFound() throws Exception {
     RestResponse r =
         userSession.get("/config/server/tasks/" + getLogFileCompressorTaskId());
-    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 
-  private String getLogFileCompressorTaskId() throws IOException {
+  private String getLogFileCompressorTaskId() throws Exception {
     RestResponse r = adminSession.get("/config/server/tasks/");
     List<TaskInfo> result =
         newGson().fromJson(r.getReader(),
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
index 90cb7cc..26299e2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
@@ -14,8 +14,7 @@
 
 package com.google.gerrit.acceptance.rest.config;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
@@ -25,40 +24,39 @@
 import org.apache.http.HttpStatus;
 import org.junit.Test;
 
-import java.io.IOException;
 import java.util.List;
 
 public class KillTaskIT extends AbstractDaemonTest {
 
   @Test
-  public void killTask() throws IOException {
+  public void killTask() throws Exception {
     RestResponse r = adminSession.get("/config/server/tasks/");
     List<TaskInfo> result = newGson().fromJson(r.getReader(),
         new TypeToken<List<TaskInfo>>() {}.getType());
     r.consume();
     int taskCount = result.size();
-    assertTrue(taskCount > 0);
+    assertThat(taskCount).isGreaterThan(0);
 
     r = adminSession.delete("/config/server/tasks/" + result.get(0).id);
-    assertEquals(HttpStatus.SC_NO_CONTENT, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
     r.consume();
 
     r = adminSession.get("/config/server/tasks/");
     result = newGson().fromJson(r.getReader(),
         new TypeToken<List<TaskInfo>>() {}.getType());
     r.consume();
-    assertEquals(taskCount - 1, result.size());
+    assertThat(result).hasSize(taskCount - 1);
   }
 
   @Test
-  public void killTask_NotFound() throws IOException {
+  public void killTask_NotFound() throws Exception {
     RestResponse r = adminSession.get("/config/server/tasks/");
     List<TaskInfo> result = newGson().fromJson(r.getReader(),
         new TypeToken<List<TaskInfo>>() {}.getType());
     r.consume();
-    assertTrue(result.size() > 0);
+    assertThat(result.size()).isGreaterThan(0);
 
     r = userSession.delete("/config/server/tasks/" + result.get(0).id);
-    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListCachesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
index db7fb7f..0d0a94e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListCachesIT.java
@@ -14,11 +14,8 @@
 
 package com.google.gerrit.acceptance.rest.config;
 
+import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
 
 import com.google.common.collect.Ordering;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -31,7 +28,6 @@
 import org.eclipse.jgit.util.Base64;
 import org.junit.Test;
 
-import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -39,65 +35,65 @@
 public class ListCachesIT extends AbstractDaemonTest {
 
   @Test
-  public void listCaches() throws IOException {
+  public void listCaches() throws Exception {
     RestResponse r = adminSession.get("/config/server/caches/");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     Map<String, CacheInfo> result =
         newGson().fromJson(r.getReader(),
             new TypeToken<Map<String, CacheInfo>>() {}.getType());
 
-    assertTrue(result.containsKey("accounts"));
+    assertThat(result).containsKey("accounts");
     CacheInfo accountsCacheInfo = result.get("accounts");
-    assertEquals(CacheType.MEM, accountsCacheInfo.type);
-    assertEquals(1, accountsCacheInfo.entries.mem.longValue());
-    assertNotNull(accountsCacheInfo.averageGet);
-    assertTrue(accountsCacheInfo.averageGet.endsWith("s"));
-    assertNull(accountsCacheInfo.entries.disk);
-    assertNull(accountsCacheInfo.entries.space);
-    assertTrue(accountsCacheInfo.hitRatio.mem >= 0);
-    assertTrue(accountsCacheInfo.hitRatio.mem <= 100);
-    assertNull(accountsCacheInfo.hitRatio.disk);
+    assertThat(accountsCacheInfo.type).isEqualTo(CacheType.MEM);
+    assertThat(accountsCacheInfo.entries.mem.longValue()).isEqualTo(1);
+    assertThat(accountsCacheInfo.averageGet).isNotNull();
+    assertThat(accountsCacheInfo.averageGet).endsWith("s");
+    assertThat(accountsCacheInfo.entries.disk).isNull();
+    assertThat(accountsCacheInfo.entries.space).isNull();
+    assertThat(accountsCacheInfo.hitRatio.mem).isAtLeast(0);
+    assertThat(accountsCacheInfo.hitRatio.mem).isAtMost(100);
+    assertThat(accountsCacheInfo.hitRatio.disk).isNull();
 
     userSession.get("/config/server/version").consume();
     r = adminSession.get("/config/server/caches/");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     result = newGson().fromJson(r.getReader(),
         new TypeToken<Map<String, CacheInfo>>() {}.getType());
-    assertEquals(2, result.get("accounts").entries.mem.longValue());
+    assertThat(result.get("accounts").entries.mem.longValue()).isEqualTo(2);
   }
 
   @Test
-  public void listCaches_Forbidden() throws IOException {
+  public void listCaches_Forbidden() throws Exception {
     RestResponse r = userSession.get("/config/server/caches/");
-    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
   }
 
   @Test
-  public void listCacheNames() throws IOException {
+  public void listCacheNames() throws Exception {
     RestResponse r = adminSession.get("/config/server/caches/?format=LIST");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     List<String> result =
         newGson().fromJson(r.getReader(),
             new TypeToken<List<String>>() {}.getType());
-    assertTrue(result.contains("accounts"));
-    assertTrue(result.contains("projects"));
-    assertTrue(Ordering.natural().isOrdered(result));
+    assertThat(result).contains("accounts");
+    assertThat(result).contains("projects");
+    assertThat(Ordering.natural().isOrdered(result)).isTrue();
   }
 
   @Test
-  public void listCacheNamesTextList() throws IOException {
+  public void listCacheNamesTextList() throws Exception {
     RestResponse r = adminSession.get("/config/server/caches/?format=TEXT_LIST");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     String result = new String(Base64.decode(r.getEntityContent()), UTF_8.name());
     List<String> list = Arrays.asList(result.split("\n"));
-    assertTrue(list.contains("accounts"));
-    assertTrue(list.contains("projects"));
-    assertTrue(Ordering.natural().isOrdered(list));
+    assertThat(list).contains("accounts");
+    assertThat(list).contains("projects");
+    assertThat(Ordering.natural().isOrdered(list)).isTrue();
   }
 
   @Test
-  public void listCaches_BadRequest() throws IOException {
+  public void listCaches_BadRequest() throws Exception {
     RestResponse r = adminSession.get("/config/server/caches/?format=NONSENSE");
-    assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListTasksIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListTasksIT.java
index 205a701..58f3361 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListTasksIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ListTasksIT.java
@@ -14,9 +14,7 @@
 
 package com.google.gerrit.acceptance.rest.config;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
@@ -26,40 +24,39 @@
 import org.apache.http.HttpStatus;
 import org.junit.Test;
 
-import java.io.IOException;
 import java.util.List;
 
 public class ListTasksIT extends AbstractDaemonTest {
 
   @Test
-  public void listTasks() throws IOException {
+  public void listTasks() throws Exception {
     RestResponse r = adminSession.get("/config/server/tasks/");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     List<TaskInfo> result =
         newGson().fromJson(r.getReader(),
             new TypeToken<List<TaskInfo>>() {}.getType());
-    assertTrue(result.size() > 0);
+    assertThat(result).isNotEmpty();
     boolean foundLogFileCompressorTask = false;
     for (TaskInfo info : result) {
       if ("Log File Compressor".equals(info.command)) {
         foundLogFileCompressorTask = true;
       }
-      assertNotNull(info.id);
+      assertThat(info.id).isNotNull();
       Long.parseLong(info.id, 16);
-      assertNotNull(info.command);
-      assertNotNull(info.startTime);
+      assertThat(info.command).isNotNull();
+      assertThat(info.startTime).isNotNull();
     }
-    assertTrue(foundLogFileCompressorTask);
+    assertThat(foundLogFileCompressorTask).isTrue();
   }
 
   @Test
-  public void listTasksWithoutViewQueueCapability() throws IOException {
+  public void listTasksWithoutViewQueueCapability() throws Exception {
     RestResponse r = userSession.get("/config/server/tasks/");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     List<TaskInfo> result =
         newGson().fromJson(r.getReader(),
             new TypeToken<List<TaskInfo>>() {}.getType());
 
-    assertTrue(result.isEmpty());
+    assertThat(result).isEmpty();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java
index 3646e57..b9778b8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java
@@ -14,12 +14,9 @@
 
 package com.google.gerrit.acceptance.rest.group;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfo;
 import static com.google.gerrit.acceptance.rest.group.GroupAssert.assertGroupInfo;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -27,13 +24,11 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.account.AccountInfo;
-import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.group.AddIncludedGroups;
 import com.google.gerrit.server.group.AddMembers;
 import com.google.gerrit.server.group.CreateGroup;
@@ -41,12 +36,8 @@
 import com.google.gson.reflect.TypeToken;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
 
 import org.apache.http.HttpStatus;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -56,55 +47,36 @@
 import java.util.Set;
 
 public class AddRemoveGroupMembersIT extends AbstractDaemonTest {
-
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
-  private GroupCache groupCache;
-
-  private ReviewDb db;
-
-  @Before
-  public void setUp() throws Exception {
-    db = reviewDbProvider.open();
-  }
-
-  @After
-  public void tearDown() {
-    db.close();
+  @Test
+  public void addToNonExistingGroup_NotFound() throws Exception {
+    assertThat(PUT("/groups/non-existing/members/admin").getStatusCode())
+      .isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 
   @Test
-  public void addToNonExistingGroup_NotFound() throws IOException {
-    assertEquals(HttpStatus.SC_NOT_FOUND,
-        PUT("/groups/non-existing/members/admin").getStatusCode());
-  }
-
-  @Test
-  public void removeFromNonExistingGroup_NotFound() throws IOException {
-    assertEquals(HttpStatus.SC_NOT_FOUND,
-        DELETE("/groups/non-existing/members/admin"));
+  public void removeFromNonExistingGroup_NotFound() throws Exception {
+    assertThat(DELETE("/groups/non-existing/members/admin"))
+      .isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 
   @Test
   public void addRemoveMember() throws Exception {
     RestResponse r = PUT("/groups/Administrators/members/user");
-    assertEquals(HttpStatus.SC_CREATED, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
     AccountInfo ai = newGson().fromJson(r.getReader(), AccountInfo.class);
     assertAccountInfo(user, ai);
     assertMembers("Administrators", admin, user);
     r.consume();
 
-    assertEquals(HttpStatus.SC_NO_CONTENT,
-        DELETE("/groups/Administrators/members/user"));
+    assertThat(DELETE("/groups/Administrators/members/user"))
+      .isEqualTo(HttpStatus.SC_NO_CONTENT);
     assertMembers("Administrators", admin);
   }
 
   @Test
-  public void addExistingMember_OK() throws IOException {
-    assertEquals(HttpStatus.SC_OK,
-        PUT("/groups/Administrators/members/admin").getStatusCode());
+  public void addExistingMember_OK() throws Exception {
+    assertThat(PUT("/groups/Administrators/members/admin").getStatusCode())
+      .isEqualTo(HttpStatus.SC_OK);
   }
 
   @Test
@@ -126,14 +98,14 @@
   public void includeRemoveGroup() throws Exception {
     group("newGroup");
     RestResponse r = PUT("/groups/Administrators/groups/newGroup");
-    assertEquals(HttpStatus.SC_CREATED, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
     GroupInfo i = newGson().fromJson(r.getReader(), GroupInfo.class);
     r.consume();
     assertGroupInfo(groupCache.get(new AccountGroup.NameKey("newGroup")), i);
     assertIncludes("Administrators", "newGroup");
 
-    assertEquals(HttpStatus.SC_NO_CONTENT,
-        DELETE("/groups/Administrators/groups/newGroup"));
+    assertThat(DELETE("/groups/Administrators/groups/newGroup"))
+      .isEqualTo(HttpStatus.SC_NO_CONTENT);
     assertNoIncludes("Administrators");
   }
 
@@ -141,8 +113,8 @@
   public void includeExistingGroup_OK() throws Exception {
     group("newGroup");
     PUT("/groups/Administrators/groups/newGroup").consume();
-    assertEquals(HttpStatus.SC_OK,
-        PUT("/groups/Administrators/groups/newGroup").getStatusCode());
+    assertThat(PUT("/groups/Administrators/groups/newGroup").getStatusCode())
+      .isEqualTo(HttpStatus.SC_OK);
   }
 
   @Test
@@ -193,9 +165,9 @@
     for (AccountGroupMember m : all) {
       ids.add(m.getAccountId());
     }
-    assertTrue(ids.size() == members.length);
+    assertThat((Iterable<?>)ids).hasSize(members.length);
     for (TestAccount a : members) {
-      assertTrue(ids.contains(a.id));
+      assertThat((Iterable<?>)ids).contains(a.id);
     }
   }
 
@@ -207,10 +179,10 @@
 
     for (TestAccount a : members) {
       AccountInfo i = infoById.get(a.id.get());
-      assertNotNull(i);
+      assertThat(i).isNotNull();
       assertAccountInfo(a, i);
     }
-    assertEquals(ai.size(), members.length);
+    assertThat(ai).hasSize(members.length);
   }
 
   private void assertIncludes(String group, String... includes)
@@ -222,11 +194,11 @@
     for (AccountGroupById m : all) {
       ids.add(m.getIncludeUUID());
     }
-    assertTrue(ids.size() == includes.length);
+    assertThat((Iterable<?>)ids).hasSize(includes.length);
     for (String i : includes) {
       AccountGroup.UUID id = groupCache.get(
           new AccountGroup.NameKey(i)).getGroupUUID();
-      assertTrue(ids.contains(id));
+      assertThat((Iterable<?>)ids).contains(id);
     }
   }
 
@@ -238,16 +210,16 @@
 
     for (String name : includes) {
       GroupInfo i = groupsByName.get(name);
-      assertNotNull(i);
+      assertThat(i).isNotNull();
       assertGroupInfo(groupCache.get(new AccountGroup.NameKey(name)), i);
     }
-    assertEquals(gi.size(), includes.length);
+    assertThat(gi).hasSize(includes.length);
   }
 
   private void assertNoIncludes(String group) throws OrmException {
     AccountGroup g = groupCache.get(new AccountGroup.NameKey(group));
     Iterator<AccountGroupById> it =
         db.accountGroupById().byGroup(g.getId()).iterator();
-    assertFalse(it.hasNext());
+    assertThat(it.hasNext()).isFalse();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK
index da34c1d..726c7cf 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK
@@ -16,7 +16,9 @@
     '//gerrit-extension-api:api',
     '//gerrit-reviewdb:server',
     '//gerrit-server:server',
+    '//lib:guava',
     '//lib:gwtorm',
     '//lib:junit',
+    '//lib:truth',
   ],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java
index a6954b2..7de450b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java
@@ -14,45 +14,33 @@
 
 package com.google.gerrit.acceptance.rest.group;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.group.GroupAssert.assertGroupInfo;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.RestSession;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.group.CreateGroup;
 import com.google.gerrit.server.group.GroupJson.GroupInfo;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-
-import com.jcraft.jsch.JSchException;
 
 import org.apache.http.HttpStatus;
 import org.junit.Test;
 
-import java.io.IOException;
-
 public class CreateGroupIT extends AbstractDaemonTest {
-
-  @Inject
-  private GroupCache groupCache;
-
   @Test
-  public void testCreateGroup() throws IOException {
+  public void testCreateGroup() throws Exception {
     final String newGroupName = "newGroup";
     RestResponse r = adminSession.put("/groups/" + newGroupName);
     GroupInfo g = newGson().fromJson(r.getReader(), GroupInfo.class);
-    assertEquals(newGroupName, g.name);
+    assertThat(g.name).isEqualTo(newGroupName);
     AccountGroup group = groupCache.get(new AccountGroup.NameKey(newGroupName));
-    assertNotNull(group);
+    assertThat(group).isNotNull();
     assertGroupInfo(group, g);
   }
 
   @Test
-  public void testCreateGroupWithProperties() throws IOException {
+  public void testCreateGroupWithProperties() throws Exception {
     final String newGroupName = "newGroup";
     CreateGroup.Input in = new CreateGroup.Input();
     in.description = "Test description";
@@ -60,24 +48,23 @@
     in.ownerId = groupCache.get(new AccountGroup.NameKey("Administrators")).getGroupUUID().get();
     RestResponse r = adminSession.put("/groups/" + newGroupName, in);
     GroupInfo g = newGson().fromJson(r.getReader(), GroupInfo.class);
-    assertEquals(newGroupName, g.name);
+    assertThat(g.name).isEqualTo(newGroupName);
     AccountGroup group = groupCache.get(new AccountGroup.NameKey(newGroupName));
-    assertEquals(in.description, group.getDescription());
-    assertEquals(in.visibleToAll, group.isVisibleToAll());
-    assertEquals(in.ownerId, group.getOwnerGroupUUID().get());
+    assertThat(group.getDescription()).isEqualTo(in.description);
+    assertThat(group.isVisibleToAll()).isEqualTo(in.visibleToAll);
+    assertThat(group.getOwnerGroupUUID().get()).isEqualTo(in.ownerId);
   }
 
   @Test
-  public void testCreateGroupWithoutCapability_Forbidden() throws OrmException,
-      JSchException, IOException {
+  public void testCreateGroupWithoutCapability_Forbidden() throws Exception {
     RestResponse r = (new RestSession(server, user)).put("/groups/newGroup");
-    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
   }
 
   @Test
   public void testCreateGroupWhenGroupAlreadyExists_Conflict()
-      throws OrmException, JSchException, IOException {
+      throws Exception {
     RestResponse r = adminSession.put("/groups/Administrators");
-    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/DefaultGroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/DefaultGroupsIT.java
index 0d229ca..8365d79 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/DefaultGroupsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/DefaultGroupsIT.java
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.acceptance.rest.group;
 
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
 
 import com.google.common.collect.Sets;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -24,13 +25,9 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gson.reflect.TypeToken;
-import com.google.gwtorm.server.OrmException;
-
-import com.jcraft.jsch.JSchException;
 
 import org.junit.Test;
 
-import java.io.IOException;
 import java.util.Map;
 import java.util.Set;
 
@@ -44,33 +41,35 @@
 public class DefaultGroupsIT extends AbstractDaemonTest {
 
   @Test
-  public void defaultGroupsCreated_ssh() throws JSchException, IOException {
+  public void defaultGroupsCreated_ssh() throws Exception {
     SshSession session = new SshSession(server, admin);
     String result = session.exec("gerrit ls-groups");
-    assertTrue(result.contains("Administrators"));
-    assertTrue(result.contains("Non-Interactive Users"));
+    assert_().withFailureMessage(session.getError())
+      .that(session.hasError()).isFalse();
+    assertThat(result).contains("Administrators");
+    assertThat(result).contains("Non-Interactive Users");
     session.close();
   }
 
   @Test
-  public void defaultGroupsCreated_rest() throws IOException {
+  public void defaultGroupsCreated_rest() throws Exception {
     RestSession session = new RestSession(server, admin);
     RestResponse r = session.get("/groups/");
     Map<String, GroupInfo> result =
         newGson().fromJson(r.getReader(),
             new TypeToken<Map<String, GroupInfo>>() {}.getType());
     Set<String> names = result.keySet();
-    assertTrue(names.contains("Administrators"));
-    assertTrue(names.contains("Non-Interactive Users"));
+    assertThat((Iterable<?>)names).contains("Administrators");
+    assertThat((Iterable<?>)names).contains("Non-Interactive Users");
   }
 
   @Test
-  public void defaultGroupsCreated_internals() throws OrmException {
+  public void defaultGroupsCreated_internals() throws Exception {
     Set<String> names = Sets.newHashSet();
     for (AccountGroup g : db.accountGroups().all()) {
       names.add(g.getName());
     }
-    assertTrue(names.contains("Administrators"));
-    assertTrue(names.contains("Non-Interactive Users"));
+    assertThat((Iterable<?>)names).contains("Administrators");
+    assertThat((Iterable<?>)names).contains("Non-Interactive Users");
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java
index 91c9898..8dfb5ff 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java
@@ -19,21 +19,15 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.group.GroupJson.GroupInfo;
-import com.google.inject.Inject;
 
 import org.junit.Test;
 
 import java.io.IOException;
 
 public class GetGroupIT extends AbstractDaemonTest {
-
-  @Inject
-  private GroupCache groupCache;
-
   @Test
-  public void testGetGroup() throws IOException {
+  public void testGetGroup() throws Exception {
     AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
 
     // by UUID
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupAssert.java
index 70107fe..97503f4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupAssert.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.acceptance.rest.group;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
 
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -27,22 +27,24 @@
 
   public static void assertGroups(Iterable<String> expected, Set<String> actual) {
     for (String g : expected) {
-      assertTrue("missing group " + g, actual.remove(g));
+      assert_().withFailureMessage("missing group " + g)
+        .that(actual.remove(g)).isTrue();
     }
-    assertTrue("unexpected groups: " + actual, actual.isEmpty());
+    assert_().withFailureMessage("unexpected groups: " + actual)
+      .that((Iterable<?>)actual).isEmpty();
   }
 
   public static void assertGroupInfo(AccountGroup group, GroupInfo info) {
     if (info.name != null) {
       // 'name' is not set if returned in a map
-      assertEquals(group.getName(), info.name);
+      assertThat(info.name).isEqualTo(group.getName());
     }
-    assertEquals(group.getGroupUUID().get(), Url.decode(info.id));
-    assertEquals(Integer.valueOf(group.getId().get()), info.groupId);
-    assertEquals("#/admin/groups/uuid-" + Url.encode(group.getGroupUUID().get()), info.url);
-    assertEquals(group.isVisibleToAll(), toBoolean(info.options.visibleToAll));
-    assertEquals(group.getDescription(), info.description);
-    assertEquals(group.getOwnerGroupUUID().get(), Url.decode(info.ownerId));
+    assertThat(Url.decode(info.id)).isEqualTo(group.getGroupUUID().get());
+    assertThat(info.groupId).isEqualTo(Integer.valueOf(group.getId().get()));
+    assertThat(info.url).isEqualTo("#/admin/groups/uuid-" + Url.encode(group.getGroupUUID().get()));
+    assertThat(toBoolean(info.options.visibleToAll)).isEqualTo(group.isVisibleToAll());
+    assertThat(info.description).isEqualTo(group.getDescription());
+    assertThat(Url.decode(info.ownerId)).isEqualTo(group.getOwnerGroupUUID().get());
   }
 
   public static boolean toBoolean(Boolean b) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupPropertiesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupPropertiesIT.java
index c630919..e9dd776 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupPropertiesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupPropertiesIT.java
@@ -14,17 +14,14 @@
 
 package com.google.gerrit.acceptance.rest.group;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.group.GroupAssert.assertGroupInfo;
 import static com.google.gerrit.acceptance.rest.group.GroupAssert.toBoolean;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gerrit.server.group.GroupOptionsInfo;
 import com.google.gerrit.server.group.PutDescription;
@@ -32,46 +29,39 @@
 import com.google.gerrit.server.group.PutOptions;
 import com.google.gerrit.server.group.PutOwner;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.inject.Inject;
 
 import org.apache.http.HttpStatus;
 import org.junit.Test;
 
-import java.io.IOException;
-
 public class GroupPropertiesIT extends AbstractDaemonTest {
-
-  @Inject
-  private GroupCache groupCache;
-
   @Test
-  public void testGroupName() throws IOException {
+  public void testGroupName() throws Exception {
     AccountGroup.NameKey adminGroupName = new AccountGroup.NameKey("Administrators");
     String url = "/groups/" + groupCache.get(adminGroupName).getGroupUUID().get() + "/name";
 
     // get name
     RestResponse r = adminSession.get(url);
     String name = newGson().fromJson(r.getReader(), String.class);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    assertEquals("Administrators", name);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    assertThat(name).isEqualTo("Administrators");
     r.consume();
 
     // set name with name conflict
     String newGroupName = "newGroup";
     r = adminSession.put("/groups/" + newGroupName);
     r.consume();
-    assertEquals(HttpStatus.SC_CREATED, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
     PutName.Input in = new PutName.Input();
     in.name = newGroupName;
     r = adminSession.put(url, in);
-    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
     r.consume();
 
     // set name to same name
     in = new PutName.Input();
     in.name = "Administrators";
     r = adminSession.put(url, in);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     r.consume();
 
     // rename
@@ -79,15 +69,15 @@
     in.name = "Admins";
     r = adminSession.put(url, in);
     String newName = newGson().fromJson(r.getReader(), String.class);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    assertNotNull(groupCache.get(new AccountGroup.NameKey(in.name)));
-    assertNull(groupCache.get(adminGroupName));
-    assertEquals(in.name, newName);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    assertThat(groupCache.get(new AccountGroup.NameKey(in.name))).isNotNull();
+    assertThat(groupCache.get(adminGroupName)).isNull();
+    assertThat(newName).isEqualTo(in.name);
     r.consume();
   }
 
   @Test
-  public void testGroupDescription() throws IOException {
+  public void testGroupDescription() throws Exception {
     AccountGroup.NameKey adminGroupName = new AccountGroup.NameKey("Administrators");
     AccountGroup adminGroup = groupCache.get(adminGroupName);
     String url = "/groups/" + adminGroup.getGroupUUID().get() + "/description";
@@ -95,8 +85,8 @@
     // get description
     RestResponse r = adminSession.get(url);
     String description = newGson().fromJson(r.getReader(), String.class);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    assertEquals(adminGroup.getDescription(), description);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    assertThat(description).isEqualTo(adminGroup.getDescription());
     r.consume();
 
     // set description
@@ -104,29 +94,29 @@
     in.description = "All users that can administrate the Gerrit Server.";
     r = adminSession.put(url, in);
     String newDescription = newGson().fromJson(r.getReader(), String.class);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    assertEquals(in.description, newDescription);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    assertThat(newDescription).isEqualTo(in.description);
     adminGroup = groupCache.get(adminGroupName);
-    assertEquals(in.description, adminGroup.getDescription());
+    assertThat(adminGroup.getDescription()).isEqualTo(in.description);
     r.consume();
 
     // delete description
     r = adminSession.delete(url);
-    assertEquals(HttpStatus.SC_NO_CONTENT, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
     adminGroup = groupCache.get(adminGroupName);
-    assertNull(adminGroup.getDescription());
+    assertThat(adminGroup.getDescription()).isNull();
 
     // set description to empty string
     in = new PutDescription.Input();
     in.description = "";
     r = adminSession.put(url, in);
-    assertEquals(HttpStatus.SC_NO_CONTENT, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
     adminGroup = groupCache.get(adminGroupName);
-    assertNull(adminGroup.getDescription());
+    assertThat(adminGroup.getDescription()).isNull();
   }
 
   @Test
-  public void testGroupOptions() throws IOException {
+  public void testGroupOptions() throws Exception {
     AccountGroup.NameKey adminGroupName = new AccountGroup.NameKey("Administrators");
     AccountGroup adminGroup = groupCache.get(adminGroupName);
     String url = "/groups/" + adminGroup.getGroupUUID().get() + "/options";
@@ -134,8 +124,8 @@
     // get options
     RestResponse r = adminSession.get(url);
     GroupOptionsInfo options = newGson().fromJson(r.getReader(), GroupOptionsInfo.class);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    assertEquals(adminGroup.isVisibleToAll(), toBoolean(options.visibleToAll));
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    assertThat(toBoolean(options.visibleToAll)).isEqualTo(adminGroup.isVisibleToAll());
     r.consume();
 
     // set options
@@ -143,15 +133,15 @@
     in.visibleToAll = !adminGroup.isVisibleToAll();
     r = adminSession.put(url, in);
     GroupOptionsInfo newOptions = newGson().fromJson(r.getReader(), GroupOptionsInfo.class);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    assertEquals(in.visibleToAll, toBoolean(newOptions.visibleToAll));
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    assertThat(toBoolean(newOptions.visibleToAll)).isEqualTo(in.visibleToAll);
     adminGroup = groupCache.get(adminGroupName);
-    assertEquals(in.visibleToAll, adminGroup.isVisibleToAll());
+    assertThat(adminGroup.isVisibleToAll()).isEqualTo(in.visibleToAll);
     r.consume();
   }
 
   @Test
-  public void testGroupOwner() throws IOException {
+  public void testGroupOwner() throws Exception {
     AccountGroup.NameKey adminGroupName = new AccountGroup.NameKey("Administrators");
     AccountGroup adminGroup = groupCache.get(adminGroupName);
     String url = "/groups/" + adminGroup.getGroupUUID().get() + "/owner";
@@ -159,7 +149,7 @@
     // get owner
     RestResponse r = adminSession.get(url);
     GroupInfo options = newGson().fromJson(r.getReader(), GroupInfo.class);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     assertGroupInfo(groupCache.get(adminGroup.getOwnerGroupUUID()), options);
     r.consume();
 
@@ -168,30 +158,29 @@
     in.owner = "Registered Users";
     r = adminSession.put(url, in);
     GroupInfo newOwner = newGson().fromJson(r.getReader(), GroupInfo.class);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    assertEquals(in.owner, newOwner.name);
-    assertEquals(
-        SystemGroupBackend.getGroup(SystemGroupBackend.REGISTERED_USERS).getName(),
-        newOwner.name);
-    assertEquals(
-        SystemGroupBackend.REGISTERED_USERS.get(),
-        Url.decode(newOwner.id));
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    assertThat(newOwner.name).isEqualTo(in.owner);
+    assertThat(newOwner.name).isEqualTo(
+        SystemGroupBackend.getGroup(SystemGroupBackend.REGISTERED_USERS).getName());
+    assertThat(SystemGroupBackend.REGISTERED_USERS.get())
+      .isEqualTo(Url.decode(newOwner.id));
     r.consume();
 
     // set owner by UUID
     in = new PutOwner.Input();
     in.owner = adminGroup.getGroupUUID().get();
     r = adminSession.put(url, in);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     adminGroup = groupCache.get(adminGroupName);
-    assertEquals(in.owner, groupCache.get(adminGroup.getOwnerGroupUUID()).getGroupUUID().get());
+    assertThat(groupCache.get(adminGroup.getOwnerGroupUUID()).getGroupUUID().get())
+      .isEqualTo(in.owner);
     r.consume();
 
     // set non existing owner
     in = new PutOwner.Input();
     in.owner = "Non-Existing Group";
     r = adminSession.put(url, in);
-    assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
     r.consume();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupIncludesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupIncludesIT.java
index d639f54..5dc49c6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupIncludesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupIncludesIT.java
@@ -14,8 +14,7 @@
 
 package com.google.gerrit.acceptance.rest.group;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
@@ -37,13 +36,13 @@
 
   @Test
   public void listNonExistingGroupIncludes_NotFound() throws Exception {
-    assertEquals(HttpStatus.SC_NOT_FOUND,
-      adminSession.get("/groups/non-existing/groups/").getStatusCode());
+    assertThat(adminSession.get("/groups/non-existing/groups/").getStatusCode())
+      .isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 
   @Test
   public void listEmptyGroupIncludes() throws Exception {
-    assertTrue(GET("/groups/Administrators/groups/").isEmpty());
+    assertThat(GET("/groups/Administrators/groups/")).isEmpty();
   }
 
   @Test
@@ -63,19 +62,19 @@
     PUT("/groups/Administrators/groups/gx");
     PUT("/groups/Administrators/groups/gy");
 
-    assertEquals(GET_ONE("/groups/Administrators/groups/gx").name, "gx");
+    assertThat(GET_ONE("/groups/Administrators/groups/gx").name).isEqualTo("gx");
   }
 
   private List<GroupInfo> GET(String endpoint) throws IOException {
     RestResponse r = adminSession.get(endpoint);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     return newGson().fromJson(r.getReader(),
         new TypeToken<List<GroupInfo>>() {}.getType());
   }
 
   private GroupInfo GET_ONE(String endpoint) throws IOException {
     RestResponse r = adminSession.get(endpoint);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     return newGson().fromJson(r.getReader(), GroupInfo.class);
   }
 
@@ -98,10 +97,10 @@
             return info.name;
           }
         });
-    assertTrue(includeNames.contains(name));
+    assertThat((Iterable<?>)includeNames).contains(name);
     for (String n : names) {
-      assertTrue(includeNames.contains(n));
+      assertThat((Iterable<?>)includeNames).contains(n);
     }
-    assertEquals(includes.size(), names.length + 1);
+    assertThat(includes).hasSize(names.length + 1);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupMembersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupMembersIT.java
index 4db13e5..80fb960 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupMembersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupMembersIT.java
@@ -14,15 +14,14 @@
 
 package com.google.gerrit.acceptance.rest.group;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.server.group.CreateGroup;
 import com.google.gson.reflect.TypeToken;
 
@@ -37,14 +36,14 @@
 
   @Test
   public void listNonExistingGroupMembers_NotFound() throws Exception {
-    assertEquals(HttpStatus.SC_NOT_FOUND,
-        adminSession.get("/groups/non-existing/members/").getStatusCode());
+    assertThat(adminSession.get("/groups/non-existing/members/").getStatusCode())
+      .isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 
   @Test
   public void listEmptyGroupMembers() throws Exception {
     group("empty", "Administrators");
-    assertTrue(GET("/groups/empty/members/").isEmpty());
+    assertThat(GET("/groups/empty/members/")).isEmpty();
   }
 
   @Test
@@ -57,9 +56,9 @@
   }
 
   @Test
-  public void listOneGroupMember() throws IOException {
-    assertEquals(GET_ONE("/groups/Administrators/members/admin").name,
-        admin.fullName);
+  public void listOneGroupMember() throws Exception {
+    assertThat(GET_ONE("/groups/Administrators/members/admin").name)
+      .isEqualTo(admin.fullName);
   }
 
   @Test
@@ -78,14 +77,14 @@
 
   private List<AccountInfo> GET(String endpoint) throws IOException {
     RestResponse r = adminSession.get(endpoint);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     return newGson().fromJson(r.getReader(),
         new TypeToken<List<AccountInfo>>() {}.getType());
   }
 
   private AccountInfo GET_ONE(String endpoint) throws IOException {
     RestResponse r = adminSession.get(endpoint);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     return newGson().fromJson(r.getReader(), AccountInfo.class);
   }
 
@@ -110,10 +109,10 @@
           }
         });
 
-    assertTrue(memberNames.contains(name));
+    assertThat((Iterable<?>)memberNames).contains(name);
     for (String n : names) {
-      assertTrue(memberNames.contains(n));
+      assertThat((Iterable<?>)memberNames).contains(n);
     }
-    assertEquals(members.size(), names.length + 1);
+    assertThat(members).hasSize(names.length + 1);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
index 8b5dde6..763c36a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
@@ -14,10 +14,9 @@
 
 package com.google.gerrit.acceptance.rest.group;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.group.GroupAssert.assertGroupInfo;
 import static com.google.gerrit.acceptance.rest.group.GroupAssert.assertGroups;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 
 import com.google.common.base.Function;
 import com.google.common.collect.Iterables;
@@ -26,29 +25,19 @@
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.group.CreateGroup;
 import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gson.reflect.TypeToken;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-
-import com.jcraft.jsch.JSchException;
 
 import org.apache.http.HttpStatus;
 import org.junit.Test;
 
-import java.io.IOException;
 import java.util.Map;
 import java.util.Set;
 
 public class ListGroupsIT extends AbstractDaemonTest {
-
-  @Inject
-  private GroupCache groupCache;
-
   @Test
-  public void testListAllGroups() throws IOException, OrmException {
+  public void testListAllGroups() throws Exception {
     Iterable<String> expectedGroups = Iterables.transform(groupCache.all(),
         new Function<AccountGroup, String>() {
           @Override
@@ -65,8 +54,7 @@
   }
 
   @Test
-  public void testOnlyVisibleGroupsReturned() throws OrmException,
-      JSchException, IOException {
+  public void testOnlyVisibleGroupsReturned() throws Exception {
     String newGroupName = "newGroup";
     CreateGroup.Input in = new CreateGroup.Input();
     in.description = "a hidden group";
@@ -80,11 +68,11 @@
     Map<String, GroupInfo> result =
         newGson().fromJson(r.getReader(),
             new TypeToken<Map<String, GroupInfo>>() {}.getType());
-    assertTrue("no groups visible", result.isEmpty());
+    assertThat(result).isEmpty();
 
-    assertEquals(HttpStatus.SC_CREATED, adminSession.put(
-        String.format("/groups/%s/members/%s", newGroupName, user.username)
-      ).getStatusCode());
+    r = adminSession.put(
+        String.format("/groups/%s/members/%s", newGroupName, user.username));
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
 
     r = userSession.get("/groups/");
     result = newGson().fromJson(r.getReader(),
@@ -93,8 +81,7 @@
   }
 
   @Test
-  public void testAllGroupInfoFieldsSetCorrectly() throws IOException,
-      OrmException {
+  public void testAllGroupInfoFieldsSetCorrectly() throws Exception {
     AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
     RestResponse r = adminSession.get("/groups/?q=" + adminGroup.getName());
     Map<String, GroupInfo> result =
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
index fa8b10e..1efaa60 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
@@ -17,6 +17,7 @@
   deps = [
     '//lib:guava',
     '//lib:junit',
+    '//lib:truth',
     '//gerrit-server:server',
   ],
 )
@@ -33,5 +34,6 @@
     '//lib:gwtorm',
     '//lib:guava',
     '//lib:junit',
+    '//lib:truth',
   ],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java
index f4414a2..ceed9b6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java
@@ -14,12 +14,10 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.add;
 import static com.google.gerrit.acceptance.GitUtil.createCommit;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -29,35 +27,33 @@
 import com.google.gerrit.server.project.BanCommit.BanResultInfo;
 
 import org.apache.http.HttpStatus;
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.transport.PushResult;
 import org.junit.Test;
 
-import java.io.IOException;
-
 public class BanCommitIT extends AbstractDaemonTest {
 
   @Test
-  public void banCommit() throws IOException, GitAPIException {
+  public void banCommit() throws Exception {
     add(git, "a.txt", "some content");
     Commit c = createCommit(git, admin.getIdent(), "subject");
 
     RestResponse r =
         adminSession.put("/projects/" + project.get() + "/ban/",
             BanCommit.Input.fromCommits(c.getCommit().getName()));
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     BanResultInfo info = newGson().fromJson(r.getReader(), BanResultInfo.class);
-    assertEquals(c.getCommit().getName(), Iterables.getOnlyElement(info.newlyBanned));
-    assertNull(info.alreadyBanned);
-    assertNull(info.ignored);
+    assertThat(Iterables.getOnlyElement(info.newlyBanned))
+      .isEqualTo(c.getCommit().getName());
+    assertThat(info.alreadyBanned).isNull();
+    assertThat(info.ignored).isNull();
 
     PushResult pushResult = pushHead(git, "refs/heads/master", false);
-    assertTrue(pushResult.getRemoteUpdate("refs/heads/master").getMessage()
-        .startsWith("contains banned commit"));
+    assertThat(pushResult.getRemoteUpdate("refs/heads/master").getMessage())
+        .startsWith("contains banned commit");
   }
 
   @Test
-  public void banAlreadyBannedCommit() throws IOException, GitAPIException {
+  public void banAlreadyBannedCommit() throws Exception {
     RestResponse r =
         adminSession.put("/projects/" + project.get() + "/ban/",
             BanCommit.Input.fromCommits("a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96"));
@@ -65,18 +61,19 @@
 
     r = adminSession.put("/projects/" + project.get() + "/ban/",
         BanCommit.Input.fromCommits("a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96"));
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     BanResultInfo info = newGson().fromJson(r.getReader(), BanResultInfo.class);
-    assertEquals("a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96", Iterables.getOnlyElement(info.alreadyBanned));
-    assertNull(info.newlyBanned);
-    assertNull(info.ignored);
+    assertThat(Iterables.getOnlyElement(info.alreadyBanned))
+      .isEqualTo("a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96");
+    assertThat(info.newlyBanned).isNull();
+    assertThat(info.ignored).isNull();
   }
 
   @Test
-  public void banCommit_Forbidden() throws IOException {
+  public void banCommit_Forbidden() throws Exception {
     RestResponse r =
         userSession.put("/projects/" + project.get() + "/ban/",
             BanCommit.Input.fromCommits("a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96"));
-    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java
index 5b0dfca..c706e17 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java
@@ -14,9 +14,7 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
@@ -38,20 +36,19 @@
               return info.ref.equals(b.ref);
             }
           }, null);
-      assertNotNull("missing branch: " + b.ref, info);
+      assertThat(info).named("branch " + b.ref).isNotNull();
       assertBranchInfo(b, info);
       missingBranches.remove(info);
     }
-    assertTrue("unexpected branches: " + missingBranches,
-        missingBranches.isEmpty());
+    assertThat(missingBranches).named("" + missingBranches).isEmpty();
   }
 
   public static void assertBranchInfo(BranchInfo expected, BranchInfo actual) {
-    assertEquals(expected.ref, actual.ref);
+    assertThat(actual.ref).isEqualTo(expected.ref);
     if (expected.revision != null) {
-      assertEquals(expected.revision, actual.revision);
+      assertThat(actual.revision).isEqualTo(expected.revision);
     }
-    assertEquals(expected.canDelete, toBoolean(actual.canDelete));
+    assertThat(toBoolean(actual.canDelete)).isEqualTo(expected.canDelete);
   }
 
   private static boolean toBoolean(Boolean b) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index 23cd278..3cadf66 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -14,40 +14,22 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.Util.allow;
 import static com.google.gerrit.server.project.Util.block;
-import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.inject.Inject;
 
 import org.apache.http.HttpStatus;
-import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
-
 public class CreateBranchIT extends AbstractDaemonTest {
-  @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
-
-  @Inject
-  private ProjectCache projectCache;
-
-  @Inject
-  private AllProjectsName allProjects;
-
   private Branch.NameKey branch;
 
   @Before
@@ -56,102 +38,86 @@
   }
 
   @Test
-  public void createBranch_Forbidden() throws IOException {
+  public void createBranch_Forbidden() throws Exception {
     RestResponse r =
         userSession.put("/projects/" + project.get()
             + "/branches/" + branch.getShortName());
-    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
   }
 
   @Test
-  public void createBranchByAdmin() throws IOException {
+  public void createBranchByAdmin() throws Exception {
     RestResponse r =
         adminSession.put("/projects/" + project.get()
             + "/branches/" + branch.getShortName());
-    assertEquals(HttpStatus.SC_CREATED, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
     r.consume();
 
     r = adminSession.get("/projects/" + project.get()
         + "/branches/" + branch.getShortName());
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
   }
 
   @Test
-  public void branchAlreadyExists_Conflict() throws IOException {
+  public void branchAlreadyExists_Conflict() throws Exception {
     RestResponse r =
         adminSession.put("/projects/" + project.get()
             + "/branches/" + branch.getShortName());
-    assertEquals(HttpStatus.SC_CREATED, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
     r.consume();
 
     r = adminSession.put("/projects/" + project.get()
         + "/branches/" + branch.getShortName());
-    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
   }
 
   @Test
-  public void createBranchByProjectOwner() throws IOException,
-      ConfigInvalidException {
+  public void createBranchByProjectOwner() throws Exception {
     grantOwner();
 
     RestResponse r =
         userSession.put("/projects/" + project.get()
             + "/branches/" + branch.getShortName());
-    assertEquals(HttpStatus.SC_CREATED, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
     r.consume();
 
     r = adminSession.get("/projects/" + project.get()
         + "/branches/" + branch.getShortName());
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
   }
 
   @Test
-  public void createBranchByAdminCreateReferenceBlocked() throws IOException,
-      ConfigInvalidException {
+  public void createBranchByAdminCreateReferenceBlocked() throws Exception {
     blockCreateReference();
     RestResponse r =
         adminSession.put("/projects/" + project.get()
             + "/branches/" + branch.getShortName());
-    assertEquals(HttpStatus.SC_CREATED, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
     r.consume();
 
     r = adminSession.get("/projects/" + project.get()
         + "/branches/" + branch.getShortName());
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
   }
 
   @Test
   public void createBranchByProjectOwnerCreateReferenceBlocked_Forbidden()
-      throws IOException, ConfigInvalidException {
+      throws Exception {
     grantOwner();
     blockCreateReference();
     RestResponse r =
         userSession.put("/projects/" + project.get()
             + "/branches/" + branch.getShortName());
-    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
   }
 
-  private void blockCreateReference() throws IOException, ConfigInvalidException {
+  private void blockCreateReference() throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
     block(cfg, Permission.CREATE, ANONYMOUS_USERS, "refs/*");
     saveProjectConfig(allProjects, cfg);
-    projectCache.evict(cfg.getProject());
   }
 
-  private void grantOwner() throws IOException, ConfigInvalidException {
-    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
-    allow(cfg, Permission.OWNER, REGISTERED_USERS, "refs/*");
-    saveProjectConfig(project, cfg);
-    projectCache.evict(cfg.getProject());
-  }
-
-  private void saveProjectConfig(Project.NameKey p, ProjectConfig cfg)
-      throws IOException {
-    MetaDataUpdate md = metaDataUpdateFactory.create(p);
-    try {
-      cfg.commit(md);
-    } finally {
-      md.close();
-    }
+  private void grantOwner() throws Exception {
+    allow(Permission.OWNER, REGISTERED_USERS, "refs/*");
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index cdf671d..68c9a5e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -14,35 +14,23 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectInfo;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectOwners;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.extensions.api.GerritApi;
-import com.google.gerrit.extensions.api.projects.ProjectApi;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
-import com.google.gerrit.extensions.common.InheritableBoolean;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ProjectInfo;
-import com.google.gerrit.extensions.common.SubmitType;
-import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-
-import com.jcraft.jsch.JSchException;
 
 import org.apache.http.HttpStatus;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -58,54 +46,64 @@
 import java.util.Set;
 
 public class CreateProjectIT extends AbstractDaemonTest {
-
-  @Inject
-  private ProjectCache projectCache;
-
-  @Inject
-  private GroupCache groupCache;
-
-  @Inject
-  private GitRepositoryManager git;
-
-  @Inject
-  private GerritApi gApi;
-
   @Test
-  public void testCreateProjectApi() throws RestApiException, IOException {
+  public void testCreateProjectApi() throws Exception {
     final String newProjectName = "newProject";
-    ProjectApi projectApi = gApi.projects().name(newProjectName).create();
-    ProjectInfo p = projectApi.get();
-    assertEquals(newProjectName, p.name);
+    ProjectInfo p = gApi.projects().name(newProjectName).create().get();
+    assertThat(p.name).isEqualTo(newProjectName);
     ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
-    assertNotNull(projectState);
+    assertThat(projectState).isNotNull();
     assertProjectInfo(projectState.getProject(), p);
     assertHead(newProjectName, "refs/heads/master");
   }
 
   @Test
-  public void testCreateProject() throws IOException {
+  public void testCreateProjectApiWithGitSuffix() throws Exception {
+    final String newProjectName = "newProject";
+    ProjectInfo p = gApi.projects().name(newProjectName + ".git").create().get();
+    assertThat(p.name).isEqualTo(newProjectName);
+    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    assertThat(projectState).isNotNull();
+    assertProjectInfo(projectState.getProject(), p);
+    assertHead(newProjectName, "refs/heads/master");
+  }
+
+  @Test
+  public void testCreateProject() throws Exception {
     final String newProjectName = "newProject";
     RestResponse r = adminSession.put("/projects/" + newProjectName);
-    assertEquals(HttpStatus.SC_CREATED, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
     ProjectInfo p = newGson().fromJson(r.getReader(), ProjectInfo.class);
-    assertEquals(newProjectName, p.name);
+    assertThat(p.name).isEqualTo(newProjectName);
     ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
-    assertNotNull(projectState);
+    assertThat(projectState).isNotNull();
     assertProjectInfo(projectState.getProject(), p);
     assertHead(newProjectName, "refs/heads/master");
   }
 
   @Test
-  public void testCreateProjectWithNameMismatch_BadRequest() throws IOException {
+  public void testCreateProjectWithGitSuffix() throws Exception {
+    final String newProjectName = "newProject";
+    RestResponse r = adminSession.put("/projects/" + newProjectName + ".git");
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
+    ProjectInfo p = newGson().fromJson(r.getReader(), ProjectInfo.class);
+    assertThat(p.name).isEqualTo(newProjectName);
+    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    assertThat(projectState).isNotNull();
+    assertProjectInfo(projectState.getProject(), p);
+    assertHead(newProjectName, "refs/heads/master");
+  }
+
+  @Test
+  public void testCreateProjectWithNameMismatch_BadRequest() throws Exception {
     ProjectInput in = new ProjectInput();
     in.name = "otherName";
     RestResponse r = adminSession.put("/projects/someName", in);
-    assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST);
   }
 
   @Test
-  public void testCreateProjectWithProperties() throws IOException {
+  public void testCreateProjectWithProperties() throws Exception {
     final String newProjectName = "newProject";
     ProjectInput in = new ProjectInput();
     in.description = "Test description";
@@ -116,19 +114,19 @@
     in.requireChangeId = InheritableBoolean.TRUE;
     RestResponse r = adminSession.put("/projects/" + newProjectName, in);
     ProjectInfo p = newGson().fromJson(r.getReader(), ProjectInfo.class);
-    assertEquals(newProjectName, p.name);
+    assertThat(p.name).isEqualTo(newProjectName);
     Project project = projectCache.get(new Project.NameKey(newProjectName)).getProject();
     assertProjectInfo(project, p);
-    assertEquals(in.description, project.getDescription());
-    assertEquals(in.submitType, project.getSubmitType());
-    assertEquals(in.useContributorAgreements, project.getUseContributorAgreements());
-    assertEquals(in.useSignedOffBy, project.getUseSignedOffBy());
-    assertEquals(in.useContentMerge, project.getUseContentMerge());
-    assertEquals(in.requireChangeId, project.getRequireChangeID());
+    assertThat(project.getDescription()).isEqualTo(in.description);
+    assertThat(project.getSubmitType()).isEqualTo(in.submitType);
+    assertThat(project.getUseContributorAgreements()).isEqualTo(in.useContributorAgreements);
+    assertThat(project.getUseSignedOffBy()).isEqualTo(in.useSignedOffBy);
+    assertThat(project.getUseContentMerge()).isEqualTo(in.useContentMerge);
+    assertThat(project.getRequireChangeID()).isEqualTo(in.requireChangeId);
   }
 
   @Test
-  public void testCreateChildProject() throws IOException {
+  public void testCreateChildProject() throws Exception {
     final String parentName = "parent";
     RestResponse r = adminSession.put("/projects/" + parentName);
     r.consume();
@@ -137,20 +135,20 @@
     in.parent = parentName;
     r = adminSession.put("/projects/" + childName, in);
     Project project = projectCache.get(new Project.NameKey(childName)).getProject();
-    assertEquals(in.parent, project.getParentName());
+    assertThat(project.getParentName()).isEqualTo(in.parent);
   }
 
   @Test
   public void testCreateChildProjectUnderNonExistingParent_UnprocessableEntity()
-      throws IOException {
+      throws Exception {
     ProjectInput in = new ProjectInput();
     in.parent = "non-existing-project";
     RestResponse r = adminSession.put("/projects/child", in);
-    assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
   }
 
   @Test
-  public void testCreateProjectWithOwner() throws IOException {
+  public void testCreateProjectWithOwner() throws Exception {
     final String newProjectName = "newProject";
     ProjectInput in = new ProjectInput();
     in.owners = Lists.newArrayListWithCapacity(3);
@@ -169,15 +167,15 @@
 
   @Test
   public void testCreateProjectWithNonExistingOwner_UnprocessableEntity()
-      throws IOException {
+      throws Exception {
     ProjectInput in = new ProjectInput();
     in.owners = Collections.singletonList("non-existing-group");
     RestResponse r = adminSession.put("/projects/newProject", in);
-    assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
   }
 
   @Test
-  public void testCreatePermissionOnlyProject() throws IOException {
+  public void testCreatePermissionOnlyProject() throws Exception {
     final String newProjectName = "newProject";
     ProjectInput in = new ProjectInput();
     in.permissionsOnly = true;
@@ -186,7 +184,7 @@
   }
 
   @Test
-  public void testCreateProjectWithEmptyCommit() throws IOException {
+  public void testCreateProjectWithEmptyCommit() throws Exception {
     final String newProjectName = "newProject";
     ProjectInput in = new ProjectInput();
     in.createEmptyCommit = true;
@@ -195,7 +193,7 @@
   }
 
   @Test
-  public void testCreateProjectWithBranches() throws IOException {
+  public void testCreateProjectWithBranches() throws Exception {
     final String newProjectName = "newProject";
     ProjectInput in = new ProjectInput();
     in.createEmptyCommit = true;
@@ -210,17 +208,16 @@
   }
 
   @Test
-  public void testCreateProjectWithoutCapability_Forbidden() throws OrmException,
-      JSchException, IOException {
+  public void testCreateProjectWithoutCapability_Forbidden() throws Exception {
     RestResponse r = userSession.put("/projects/newProject");
-    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
   }
 
   @Test
   public void testCreateProjectWhenProjectAlreadyExists_Conflict()
-      throws OrmException, JSchException, IOException {
+      throws Exception {
     RestResponse r = adminSession.put("/projects/All-Projects");
-    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
   }
 
   private AccountGroup.UUID groupUuid(String groupName) {
@@ -229,10 +226,11 @@
 
   private void assertHead(String projectName, String expectedRef)
       throws RepositoryNotFoundException, IOException {
-    Repository repo = git.openRepository(new Project.NameKey(projectName));
+    Repository repo =
+        repoManager.openRepository(new Project.NameKey(projectName));
     try {
-      assertEquals(expectedRef, repo.getRef(Constants.HEAD).getTarget()
-          .getName());
+      assertThat(repo.getRef(Constants.HEAD).getTarget().getName())
+        .isEqualTo(expectedRef);
     } finally {
       repo.close();
     }
@@ -240,21 +238,17 @@
 
   private void assertEmptyCommit(String projectName, String... refs)
       throws RepositoryNotFoundException, IOException {
-    Repository repo = git.openRepository(new Project.NameKey(projectName));
-    RevWalk rw = new RevWalk(repo);
-    TreeWalk tw = new TreeWalk(repo);
-    try {
+    Project.NameKey projectKey = new Project.NameKey(projectName);
+    try (Repository repo = repoManager.openRepository(projectKey);
+        RevWalk rw = new RevWalk(repo);
+        TreeWalk tw = new TreeWalk(rw.getObjectReader())) {
       for (String ref : refs) {
         RevCommit commit = rw.lookupCommit(repo.getRef(ref).getObjectId());
         rw.parseBody(commit);
         tw.addTree(commit.getTree());
-        assertFalse("ref " + ref + " has non empty commit", tw.next());
+        assertThat(tw.next()).isFalse();
         tw.reset();
       }
-    } finally {
-      tw.release();
-      rw.release();
-      repo.close();
     }
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index 0d6ec5b..8be6c92 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -14,41 +14,23 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.Util.allow;
 import static com.google.gerrit.server.project.Util.block;
-import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.inject.Inject;
 
 import org.apache.http.HttpStatus;
-import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
-
 public class DeleteBranchIT extends AbstractDaemonTest {
 
-  @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
-
-  @Inject
-  private ProjectCache projectCache;
-
-  @Inject
-  private AllProjectsName allProjects;
-
   private Branch.NameKey branch;
 
   @Before
@@ -59,93 +41,78 @@
   }
 
   @Test
-  public void deleteBranch_Forbidden() throws IOException {
+  public void deleteBranch_Forbidden() throws Exception {
     RestResponse r =
         userSession.delete("/projects/" + project.get()
             + "/branches/" + branch.getShortName());
-    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
     r.consume();
   }
 
   @Test
-  public void deleteBranchByAdmin() throws IOException {
+  public void deleteBranchByAdmin() throws Exception {
     RestResponse r =
         adminSession.delete("/projects/" + project.get()
             + "/branches/" + branch.getShortName());
-    assertEquals(HttpStatus.SC_NO_CONTENT, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
     r.consume();
 
     r = adminSession.get("/projects/" + project.get()
         + "/branches/" + branch.getShortName());
-    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
     r.consume();
   }
 
   @Test
-  public void deleteBranchByProjectOwner() throws IOException,
-      ConfigInvalidException {
+  public void deleteBranchByProjectOwner() throws Exception {
     grantOwner();
 
     RestResponse r =
         userSession.delete("/projects/" + project.get()
             + "/branches/" + branch.getShortName());
-    assertEquals(HttpStatus.SC_NO_CONTENT, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
     r.consume();
 
     r = userSession.get("/projects/" + project.get()
         + "/branches/" + branch.getShortName());
-    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
     r.consume();
   }
 
   @Test
-  public void deleteBranchByAdminForcePushBlocked() throws IOException,
-      ConfigInvalidException {
+  public void deleteBranchByAdminForcePushBlocked() throws Exception {
     blockForcePush();
     RestResponse r =
         adminSession.delete("/projects/" + project.get()
             + "/branches/" + branch.getShortName());
-    assertEquals(HttpStatus.SC_NO_CONTENT, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
     r.consume();
 
     r = adminSession.get("/projects/" + project.get()
         + "/branches/" + branch.getShortName());
-    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
     r.consume();
   }
 
   @Test
   public void deleteBranchByProjectOwnerForcePushBlocked_Forbidden()
-      throws IOException, ConfigInvalidException {
+      throws Exception {
     grantOwner();
     blockForcePush();
     RestResponse r =
         userSession.delete("/projects/" + project.get()
             + "/branches/" + branch.getShortName());
-    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
     r.consume();
   }
 
-  private void blockForcePush() throws IOException, ConfigInvalidException {
+  private void blockForcePush() throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
     block(cfg, Permission.PUSH, ANONYMOUS_USERS, "refs/heads/*").setForce(true);
     saveProjectConfig(allProjects, cfg);
-    projectCache.evict(cfg.getProject());
   }
 
-  private void grantOwner() throws IOException, ConfigInvalidException {
-    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
-    allow(cfg, Permission.OWNER, REGISTERED_USERS, "refs/*");
-    saveProjectConfig(project, cfg);
-    projectCache.evict(cfg.getProject());
-  }
-
-  private void saveProjectConfig(Project.NameKey p, ProjectConfig cfg) throws IOException {
-    MetaDataUpdate md = metaDataUpdateFactory.create(p);
-    try {
-      cfg.commit(md);
-    } finally {
-      md.close();
-    }
+  private void grantOwner() throws Exception {
+    allow(Permission.OWNER, REGISTERED_USERS, "refs/*");
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
index 26585ba..6aa3af6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
@@ -14,20 +14,16 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.createProject;
-import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GcAssert;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
-import com.jcraft.jsch.JSchException;
-
 import org.apache.http.HttpStatus;
 import org.junit.Before;
 import org.junit.Test;
@@ -37,42 +33,36 @@
 public class GarbageCollectionIT extends AbstractDaemonTest {
 
   @Inject
-  private AllProjectsName allProjects;
-
-  @Inject
   private GcAssert gcAssert;
 
-  private Project.NameKey project1;
   private Project.NameKey project2;
 
   @Before
   public void setUp() throws Exception {
-    project1 = new Project.NameKey("p1");
-    createProject(sshSession, project1.get());
-
     project2 = new Project.NameKey("p2");
     createProject(sshSession, project2.get());
   }
 
   @Test
-  public void testGcNonExistingProject_NotFound() throws IOException {
-    assertEquals(HttpStatus.SC_NOT_FOUND, POST("/projects/non-existing/gc"));
+  public void testGcNonExistingProject_NotFound() throws Exception {
+    assertThat(POST("/projects/non-existing/gc")).isEqualTo(
+        HttpStatus.SC_NOT_FOUND);
   }
 
   @Test
-  public void testGcNotAllowed_Forbidden() throws IOException, OrmException,
-      JSchException {
-    assertEquals(HttpStatus.SC_FORBIDDEN,
+  public void testGcNotAllowed_Forbidden() throws Exception {
+    assertThat(
         userSession.post("/projects/" + allProjects.get() + "/gc")
-            .getStatusCode());
+            .getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
   }
 
   @Test
   @UseLocalDisk
-  public void testGcOneProject() throws JSchException, IOException {
-    assertEquals(HttpStatus.SC_OK, POST("/projects/" + allProjects.get() + "/gc"));
+  public void testGcOneProject() throws Exception {
+    assertThat(POST("/projects/" + allProjects.get() + "/gc")).isEqualTo(
+        HttpStatus.SC_OK);
     gcAssert.assertHasPackFile(allProjects);
-    gcAssert.assertHasNoPackFile(project1, project2);
+    gcAssert.assertHasNoPackFile(project, project2);
   }
 
   private int POST(String endPoint) throws IOException {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
index 10d59d2d..f49408e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
@@ -14,20 +14,15 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectInfo;
-import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.inject.Inject;
-
-import com.jcraft.jsch.JSchException;
 
 import org.apache.http.HttpStatus;
 import org.junit.Test;
@@ -36,59 +31,55 @@
 
 public class GetChildProjectIT extends AbstractDaemonTest {
 
-  @Inject
-  private AllProjectsName allProjects;
-
-  @Inject
-  private ProjectCache projectCache;
-
   @Test
-  public void getNonExistingChildProject_NotFound() throws IOException {
-    assertEquals(HttpStatus.SC_NOT_FOUND,
-        GET("/projects/" + allProjects.get() + "/children/non-existing").getStatusCode());
+  public void getNonExistingChildProject_NotFound() throws Exception {
+    assertThat(
+        GET("/projects/" + allProjects.get() + "/children/non-existing")
+            .getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 
   @Test
-  public void getNonChildProject_NotFound() throws IOException, JSchException {
+  public void getNonChildProject_NotFound() throws Exception {
     SshSession sshSession = new SshSession(server, admin);
     Project.NameKey p1 = new Project.NameKey("p1");
     createProject(sshSession, p1.get());
     Project.NameKey p2 = new Project.NameKey("p2");
     createProject(sshSession, p2.get());
     sshSession.close();
-    assertEquals(HttpStatus.SC_NOT_FOUND,
-        GET("/projects/" + p1.get() + "/children/" + p2.get()).getStatusCode());
+    assertThat(
+        GET("/projects/" + p1.get() + "/children/" + p2.get()).getStatusCode())
+        .isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 
   @Test
-  public void getChildProject() throws IOException, JSchException {
+  public void getChildProject() throws Exception {
     SshSession sshSession = new SshSession(server, admin);
     Project.NameKey child = new Project.NameKey("p1");
     createProject(sshSession, child.get());
     sshSession.close();
-    RestResponse r = GET("/projects/" + allProjects.get() + "/children/" + child.get());
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    RestResponse r =
+        GET("/projects/" + allProjects.get() + "/children/" + child.get());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     ProjectInfo childInfo =
         newGson().fromJson(r.getReader(), ProjectInfo.class);
     assertProjectInfo(projectCache.get(child).getProject(), childInfo);
   }
 
   @Test
-  public void getGrandChildProject_NotFound() throws IOException, JSchException {
+  public void getGrandChildProject_NotFound() throws Exception {
     SshSession sshSession = new SshSession(server, admin);
     Project.NameKey child = new Project.NameKey("p1");
     createProject(sshSession, child.get());
     Project.NameKey grandChild = new Project.NameKey("p1.1");
     createProject(sshSession, grandChild.get(), child);
     sshSession.close();
-    assertEquals(HttpStatus.SC_NOT_FOUND,
+    assertThat(
         GET("/projects/" + allProjects.get() + "/children/" + grandChild.get())
-            .getStatusCode());
+            .getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 
   @Test
-  public void getGrandChildProjectWithRecursiveFlag() throws IOException,
-      JSchException {
+  public void getGrandChildProjectWithRecursiveFlag() throws Exception {
     SshSession sshSession = new SshSession(server, admin);
     Project.NameKey child = new Project.NameKey("p1");
     createProject(sshSession, child.get());
@@ -98,7 +89,7 @@
     RestResponse r =
         GET("/projects/" + allProjects.get() + "/children/" + grandChild.get()
             + "?recursive");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     ProjectInfo grandChildInfo =
         newGson().fromJson(r.getReader(), ProjectInfo.class);
     assertProjectInfo(projectCache.get(grandChild).getProject(), grandChildInfo);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
index 2aacf20..4a20957 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
@@ -14,98 +14,133 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.common.CommitInfo;
-import com.google.gerrit.extensions.restapi.IdString;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.project.ListBranches.BranchInfo;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.inject.Inject;
 
 import org.apache.http.HttpStatus;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
-
 public class GetCommitIT extends AbstractDaemonTest {
+  private TestRepository<Repository> repo;
 
-  @Inject
-  private ProjectCache projectCache;
+  @Before
+  public void setUp() throws Exception {
+    repo = new TestRepository<>(repoManager.openRepository(project));
 
-  @Inject
-  private AllProjectsName allProjects;
-
-  @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
-
-  @Test
-  public void getCommit() throws IOException {
-    RestResponse r =
-        adminSession.get("/projects/" + project.get() + "/branches/"
-            + IdString.fromDecoded(RefNames.REFS_CONFIG).encoded());
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    BranchInfo branchInfo =
-        newGson().fromJson(r.getReader(), BranchInfo.class);
-    r.consume();
-
-    r = adminSession.get("/projects/" + project.get() + "/commits/"
-        + branchInfo.revision);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    CommitInfo commitInfo =
-        newGson().fromJson(r.getReader(), CommitInfo.class);
-    assertEquals(branchInfo.revision, commitInfo.commit);
-    assertEquals("Created project", commitInfo.subject);
-    assertEquals("Created project\n", commitInfo.message);
-    assertNotNull(commitInfo.author);
-    assertEquals("Administrator", commitInfo.author.name);
-    assertNotNull(commitInfo.committer);
-    assertEquals("Gerrit Code Review", commitInfo.committer.name);
-    assertTrue(commitInfo.parents.isEmpty());
-  }
-
-  @Test
-  public void getNonExistingCommit_NotFound() throws IOException {
-    RestResponse r = adminSession.get("/projects/" + project.get() + "/commits/"
-        + ObjectId.zeroId().name());
-    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
-  }
-
-  @Test
-  public void getNonVisibleCommit_NotFound() throws IOException {
-    RestResponse r =
-        adminSession.get("/projects/" + project.get() + "/branches/"
-            + IdString.fromDecoded(RefNames.REFS_CONFIG).encoded());
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    BranchInfo branchInfo =
-        newGson().fromJson(r.getReader(), BranchInfo.class);
-    r.consume();
-
-    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
-    cfg.getAccessSection("refs/*", false).removePermission(Permission.READ);
-    saveProjectConfig(cfg);
-    projectCache.evict(cfg.getProject());
-
-    r = adminSession.get("/projects/" + project.get() + "/commits/"
-        + branchInfo.revision);
-    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
-  }
-
-  private void saveProjectConfig(ProjectConfig cfg) throws IOException {
-    MetaDataUpdate md = metaDataUpdateFactory.create(allProjects);
-    try {
-      cfg.commit(md);
-    } finally {
-      md.close();
+    ProjectConfig pc = projectCache.checkedGet(allProjects).getConfig();
+    for (AccessSection sec : pc.getAccessSections()) {
+      sec.removePermission(Permission.READ);
     }
+    saveProjectConfig(allProjects, pc);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    if (repo != null) {
+      repo.getRepository().close();
+    }
+  }
+
+  @Test
+  public void getNonExistingCommit_NotFound() throws Exception {
+    assertNotFound(
+        ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+  }
+
+  @Test
+  public void getMergedCommit_Found() throws Exception {
+    allow(Permission.READ, REGISTERED_USERS, "refs/heads/*");
+    RevCommit commit = repo.parseBody(repo.branch("master")
+        .commit()
+        .message("Create\n\nNew commit\n")
+        .create());
+
+    CommitInfo info = getCommit(commit);
+    assertThat(info.commit).isEqualTo(commit.name());
+    assertThat(info.subject).isEqualTo("Create");
+    assertThat(info.message).isEqualTo("Create\n\nNew commit\n");
+    assertThat(info.author.name).isEqualTo("J. Author");
+    assertThat(info.author.email).isEqualTo("jauthor@example.com");
+    assertThat(info.committer.name).isEqualTo("J. Committer");
+    assertThat(info.committer.email).isEqualTo("jcommitter@example.com");
+
+    CommitInfo parent = Iterables.getOnlyElement(info.parents);
+    assertThat(parent.commit).isEqualTo(commit.getParent(0).name());
+    assertThat(parent.subject).isEqualTo("Initial empty repository");
+    assertThat(parent.message).isNull();
+    assertThat(parent.author).isNull();
+    assertThat(parent.committer).isNull();
+  }
+
+  @Test
+  public void getMergedCommit_NotFound() throws Exception {
+    RevCommit commit = repo.parseBody(repo.branch("master")
+        .commit()
+        .message("Create\n\nNew commit\n")
+        .create());
+    assertNotFound(commit);
+  }
+
+  @Test
+  public void getOpenChange_Found() throws Exception {
+    allow(Permission.READ, REGISTERED_USERS, "refs/heads/*");
+    PushOneCommit.Result r = pushFactory.create(db, admin.getIdent())
+        .to(git, "refs/for/master");
+    r.assertOkStatus();
+
+    CommitInfo info = getCommit(r.getCommitId());
+    assertThat(info.commit).isEqualTo(r.getCommitId().name());
+    assertThat(info.subject).isEqualTo("test commit");
+    assertThat(info.message).isEqualTo(
+        "test commit\n\nChange-Id: " + r.getChangeId() + "\n");
+    assertThat(info.author.name).isEqualTo("admin");
+    assertThat(info.author.email).isEqualTo("admin@example.com");
+    assertThat(info.committer.name).isEqualTo("admin");
+    assertThat(info.committer.email).isEqualTo("admin@example.com");
+
+    CommitInfo parent = Iterables.getOnlyElement(info.parents);
+    assertThat(parent.commit).isEqualTo(r.getCommit().getParent(0).name());
+    assertThat(parent.subject).isEqualTo("Initial empty repository");
+    assertThat(parent.message).isNull();
+    assertThat(parent.author).isNull();
+    assertThat(parent.committer).isNull();
+  }
+
+  @Test
+  public void getOpenChange_NotFound() throws Exception {
+    PushOneCommit.Result r = pushFactory.create(db, admin.getIdent())
+        .to(git, "refs/for/master");
+    r.assertOkStatus();
+    assertNotFound(r.getCommitId());
+  }
+
+  private void assertNotFound(ObjectId id) throws Exception {
+    RestResponse r = userSession.get(
+        "/projects/" + project.get() + "/commits/" + id.name());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+  }
+
+  private CommitInfo getCommit(ObjectId id) throws Exception {
+    RestResponse r = userSession.get(
+        "/projects/" + project.get() + "/commits/" + id.name());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    CommitInfo result = newGson().fromJson(r.getReader(), CommitInfo.class);
+    r.consume();
+    return result;
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetProjectIT.java
new file mode 100644
index 0000000..761e282
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetProjectIT.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.common.ProjectInfo;
+
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+
+public class GetProjectIT extends AbstractDaemonTest {
+
+  @Test
+  public void getProject() throws Exception {
+    String name = project.get();
+    RestResponse r = adminSession.get("/projects/" + name);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    ProjectInfo p = newGson().fromJson(r.getReader(), ProjectInfo.class);
+    assertThat(p.name).isEqualTo(name);
+  }
+
+  @Test
+  public void getProjectWithGitSuffix() throws Exception {
+    String name = project.get();
+    RestResponse r = adminSession.get("/projects/" + name + ".git");
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    ProjectInfo p = newGson().fromJson(r.getReader(), ProjectInfo.class);
+    assertThat(p.name).isEqualTo(name);
+  }
+
+  @Test
+  public void getProjectNotExisting() throws Exception {
+    RestResponse r = adminSession.get("/projects/does-not-exist");
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
index e6fde73..48f9ad89 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -14,32 +14,18 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static com.google.gerrit.acceptance.rest.project.BranchAssert.assertBranches;
-import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.server.project.Util.block;
-import static org.junit.Assert.assertEquals;
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.ListBranches.BranchInfo;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.gson.reflect.TypeToken;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-
-import com.jcraft.jsch.JSchException;
 
 import org.apache.http.HttpStatus;
-import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -47,29 +33,22 @@
 import java.util.List;
 
 public class ListBranchesIT extends AbstractDaemonTest {
-
-  @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
-
-  @Inject
-  private ProjectCache projectCache;
-
   @Test
-  public void listBranchesOfNonExistingProject_NotFound() throws IOException {
-    assertEquals(HttpStatus.SC_NOT_FOUND,
-        GET("/projects/non-existing/branches").getStatusCode());
+  public void listBranchesOfNonExistingProject_NotFound() throws Exception {
+    assertThat(GET("/projects/non-existing/branches").getStatusCode())
+        .isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 
   @Test
-  public void listBranchesOfNonVisibleProject_NotFound() throws IOException,
-      OrmException, JSchException, ConfigInvalidException {
+  public void listBranchesOfNonVisibleProject_NotFound() throws Exception {
     blockRead(project, "refs/*");
-    assertEquals(HttpStatus.SC_NOT_FOUND,
-        userSession.get("/projects/" + project.get() + "/branches").getStatusCode());
+    assertThat(
+        userSession.get("/projects/" + project.get() + "/branches")
+            .getStatusCode()).isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 
   @Test
-  public void listBranchesOfEmptyProject() throws IOException, JSchException {
+  public void listBranchesOfEmptyProject() throws Exception {
     Project.NameKey emptyProject = new Project.NameKey("empty");
     createProject(sshSession, emptyProject.get(), null, false);
     RestResponse r = adminSession.get("/projects/" + emptyProject.get() + "/branches");
@@ -82,7 +61,7 @@
   }
 
   @Test
-  public void listBranches() throws IOException, GitAPIException {
+  public void listBranches() throws Exception {
     pushTo("refs/heads/master");
     String masterCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
     pushTo("refs/heads/dev");
@@ -99,15 +78,14 @@
     assertBranches(expected, result);
 
     // verify correct sorting
-    assertEquals("HEAD", result.get(0).ref);
-    assertEquals("refs/meta/config", result.get(1).ref);
-    assertEquals("refs/heads/dev", result.get(2).ref);
-    assertEquals("refs/heads/master", result.get(3).ref);
+    assertThat(result.get(0).ref).isEqualTo("HEAD");
+    assertThat(result.get(1).ref).isEqualTo("refs/meta/config");
+    assertThat(result.get(2).ref).isEqualTo("refs/heads/dev");
+    assertThat(result.get(3).ref).isEqualTo("refs/heads/master");
   }
 
   @Test
-  public void listBranchesSomeHidden() throws IOException, GitAPIException,
-      ConfigInvalidException, OrmException, JSchException {
+  public void listBranchesSomeHidden() throws Exception {
     blockRead(project, "refs/heads/dev");
     pushTo("refs/heads/master");
     String masterCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
@@ -123,8 +101,7 @@
   }
 
   @Test
-  public void listBranchesHeadHidden() throws IOException, GitAPIException,
-      ConfigInvalidException, OrmException, JSchException {
+  public void listBranchesHeadHidden() throws Exception {
     blockRead(project, "refs/heads/master");
     pushTo("refs/heads/master");
     pushTo("refs/heads/dev");
@@ -135,16 +112,88 @@
         devCommit, false)), toBranchInfoList(r));
   }
 
-  private RestResponse GET(String endpoint) throws IOException {
-    return adminSession.get(endpoint);
+  @Test
+  public void listBranchesUsingPagination() throws Exception {
+    pushTo("refs/heads/master");
+    pushTo("refs/heads/someBranch1");
+    pushTo("refs/heads/someBranch2");
+    pushTo("refs/heads/someBranch3");
+
+    // using only limit
+    RestResponse r =
+        adminSession.get("/projects/" + project.get() + "/branches?n=4");
+    List<BranchInfo> result = toBranchInfoList(r);
+    assertThat(result).hasSize(4);
+    assertThat(result.get(0).ref).isEqualTo("HEAD");
+    assertThat(result.get(1).ref).isEqualTo("refs/meta/config");
+    assertThat(result.get(2).ref).isEqualTo("refs/heads/master");
+    assertThat(result.get(3).ref).isEqualTo("refs/heads/someBranch1");
+
+    // limit higher than total number of branches
+    r = adminSession.get("/projects/" + project.get() + "/branches?n=25");
+    result = toBranchInfoList(r);
+    assertThat(result).hasSize(6);
+    assertThat(result.get(0).ref).isEqualTo("HEAD");
+    assertThat(result.get(1).ref).isEqualTo("refs/meta/config");
+    assertThat(result.get(2).ref).isEqualTo("refs/heads/master");
+    assertThat(result.get(3).ref).isEqualTo("refs/heads/someBranch1");
+    assertThat(result.get(4).ref).isEqualTo("refs/heads/someBranch2");
+    assertThat(result.get(5).ref).isEqualTo("refs/heads/someBranch3");
+
+    // using skip only
+    r = adminSession.get("/projects/" + project.get() + "/branches?s=2");
+    result = toBranchInfoList(r);
+    assertThat(result).hasSize(4);
+    assertThat(result.get(0).ref).isEqualTo("refs/heads/master");
+    assertThat(result.get(1).ref).isEqualTo("refs/heads/someBranch1");
+    assertThat(result.get(2).ref).isEqualTo("refs/heads/someBranch2");
+    assertThat(result.get(3).ref).isEqualTo("refs/heads/someBranch3");
+
+    // skip more branches than the number of available branches
+    r = adminSession.get("/projects/" + project.get() + "/branches?s=7");
+    result = toBranchInfoList(r);
+    assertThat(result).isEmpty();
+
+    // using skip and limit
+    r = adminSession.get("/projects/" + project.get() + "/branches?s=2&n=2");
+    result = toBranchInfoList(r);
+    assertThat(result).hasSize(2);
+    assertThat(result.get(0).ref).isEqualTo("refs/heads/master");
+    assertThat(result.get(1).ref).isEqualTo("refs/heads/someBranch1");
   }
 
-  private void blockRead(Project.NameKey project, String ref)
-      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
-    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
-    block(cfg, Permission.READ, REGISTERED_USERS, ref);
-    saveProjectConfig(project, cfg);
-    projectCache.evict(cfg.getProject());
+  @Test
+  public void listBranchesUsingFilter() throws Exception {
+    pushTo("refs/heads/master");
+    pushTo("refs/heads/someBranch1");
+    pushTo("refs/heads/someBranch2");
+    pushTo("refs/heads/someBranch3");
+
+    //using substring
+    RestResponse r =
+        adminSession.get("/projects/" + project.get() + "/branches?m=some");
+    List<BranchInfo> result = toBranchInfoList(r);
+    assertThat(result).hasSize(3);
+    assertThat(result.get(0).ref).isEqualTo("refs/heads/someBranch1");
+    assertThat(result.get(1).ref).isEqualTo("refs/heads/someBranch2");
+    assertThat(result.get(2).ref).isEqualTo("refs/heads/someBranch3");
+
+    r = adminSession.get("/projects/" + project.get() + "/branches?m=Branch");
+    result = toBranchInfoList(r);
+    assertThat(result).hasSize(3);
+    assertThat(result.get(0).ref).isEqualTo("refs/heads/someBranch1");
+    assertThat(result.get(1).ref).isEqualTo("refs/heads/someBranch2");
+    assertThat(result.get(2).ref).isEqualTo("refs/heads/someBranch3");
+
+    //using regex
+    r = adminSession.get("/projects/" + project.get() + "/branches?r=.*ast.*r");
+    result = toBranchInfoList(r);
+    assertThat(result).hasSize(1);
+    assertThat(result.get(0).ref).isEqualTo("refs/heads/master");
+  }
+
+  private RestResponse GET(String endpoint) throws IOException {
+    return adminSession.get(endpoint);
   }
 
   private static List<BranchInfo> toBranchInfoList(RestResponse r)
@@ -154,19 +203,4 @@
             new TypeToken<List<BranchInfo>>() {}.getType());
     return result;
   }
-
-  private PushOneCommit.Result pushTo(String ref) throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, ref);
-  }
-
-  private void saveProjectConfig(Project.NameKey p, ProjectConfig cfg) throws IOException {
-    MetaDataUpdate md = metaDataUpdateFactory.create(p);
-    try {
-      cfg.commit(md);
-    } finally {
-      md.close();
-    }
-  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
index 466a239..0d3b467 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
@@ -14,20 +14,15 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjects;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gson.reflect.TypeToken;
-import com.google.inject.Inject;
-
-import com.jcraft.jsch.JSchException;
 
 import org.apache.http.HttpStatus;
 import org.junit.Test;
@@ -38,26 +33,23 @@
 
 public class ListChildProjectsIT extends AbstractDaemonTest {
 
-  @Inject
-  private AllProjectsName allProjects;
-
   @Test
-  public void listChildrenOfNonExistingProject_NotFound() throws IOException {
-    assertEquals(HttpStatus.SC_NOT_FOUND,
-        GET("/projects/non-existing/children/").getStatusCode());
+  public void listChildrenOfNonExistingProject_NotFound() throws Exception {
+    assertThat(GET("/projects/non-existing/children/").getStatusCode())
+        .isEqualTo(HttpStatus.SC_NOT_FOUND);
   }
 
   @Test
-  public void listNoChildren() throws IOException {
+  public void listNoChildren() throws Exception {
     RestResponse r = GET("/projects/" + allProjects.get() + "/children/");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     List<ProjectInfo> projectInfoList = toProjectInfoList(r);
     // Project 'p' was already created in the base class
-    assertTrue(projectInfoList.size() == 2);
+    assertThat(projectInfoList).hasSize(2);
   }
 
   @Test
-  public void listChildren() throws IOException, JSchException {
+  public void listChildren() throws Exception {
     Project.NameKey existingProject = new Project.NameKey("p");
     Project.NameKey child1 = new Project.NameKey("p1");
     createProject(sshSession, child1.get());
@@ -66,7 +58,7 @@
     createProject(sshSession, "p1.1", child1);
 
     RestResponse r = GET("/projects/" + allProjects.get() + "/children/");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     assertProjects(
         Arrays.asList(
             new Project.NameKey("All-Users"),
@@ -75,7 +67,7 @@
   }
 
   @Test
-  public void listChildrenRecursively() throws IOException, JSchException {
+  public void listChildrenRecursively() throws Exception {
     Project.NameKey child1 = new Project.NameKey("p1");
     createProject(sshSession, child1.get());
     createProject(sshSession, "p2");
@@ -89,7 +81,7 @@
     createProject(sshSession, child1_1_1_1.get(), child1_1_1);
 
     RestResponse r = GET("/projects/" + child1.get() + "/children/?recursive");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     assertProjects(Arrays.asList(child1_1, child1_2,
         child1_1_1, child1_1_1_1), toProjectInfoList(r));
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index 923d752..0f5bed1 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -14,25 +14,19 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjects;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.common.ProjectInfo;
-import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
 
-import com.jcraft.jsch.JSchException;
-
 import org.apache.http.HttpStatus;
 import org.junit.Test;
 
@@ -43,36 +37,33 @@
 public class ListProjectsIT extends AbstractDaemonTest {
 
   @Inject
-  private AllProjectsName allProjects;
-
-  @Inject
   private AllUsersName allUsers;
 
   @Test
-  public void listProjects() throws IOException, JSchException {
+  public void listProjects() throws Exception {
     Project.NameKey someProject = new Project.NameKey("some-project");
     createProject(sshSession, someProject.get());
 
     RestResponse r = GET("/projects/");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     Map<String, ProjectInfo> result = toProjectInfoMap(r);
     assertProjects(Arrays.asList(allUsers, someProject, project),
         result.values());
   }
 
   @Test
-  public void listProjectsWithBranch() throws IOException, JSchException {
+  public void listProjectsWithBranch() throws Exception {
     RestResponse r = GET("/projects/?b=master");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     Map<String, ProjectInfo> result = toProjectInfoMap(r);
-    assertNotNull(result.get(project.get()));
-    assertNotNull(result.get(project.get()).branches);
-    assertEquals(1, result.get(project.get()).branches.size());
-    assertNotNull(result.get(project.get()).branches.get("master"));
+    assertThat(result.get(project.get())).isNotNull();
+    assertThat(result.get(project.get()).branches).isNotNull();
+    assertThat(result.get(project.get()).branches).hasSize(1);
+    assertThat(result.get(project.get()).branches.get("master")).isNotNull();
   }
 
   @Test
-  public void listProjectWithDescription() throws RestApiException, IOException {
+  public void listProjectWithDescription() throws Exception {
     ProjectInput projectInput = new ProjectInput();
     projectInput.name = "some-project";
     projectInput.description = "Description of some-project";
@@ -80,39 +71,39 @@
 
     // description not be included in the results by default.
     RestResponse r = GET("/projects/");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     Map<String, ProjectInfo> result = toProjectInfoMap(r);
-    assertNotNull(result.get(projectInput.name));
-    assertNull(result.get(projectInput.name).description);
+    assertThat(result.get(projectInput.name)).isNotNull();
+    assertThat(result.get(projectInput.name).description).isNull();
 
     r = GET("/projects/?d");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     result = toProjectInfoMap(r);
-    assertNotNull(result.get(projectInput.name));
-    assertEquals(projectInput.description,
-        result.get(projectInput.name).description);
+    assertThat(result.get(projectInput.name)).isNotNull();
+    assertThat(result.get(projectInput.name).description).isEqualTo(
+        projectInput.description);
   }
 
   @Test
-  public void listProjectsWithLimit() throws IOException, JSchException {
+  public void listProjectsWithLimit() throws Exception {
     for (int i = 0; i < 5; i++) {
       createProject(sshSession, new Project.NameKey("someProject" + i).get());
     }
 
     RestResponse r = GET("/projects/");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     Map<String, ProjectInfo> result = toProjectInfoMap(r);
-    assertEquals(7, result.size()); // 5 plus 2 existing projects: p and
-                                    // All-Users
+    assertThat(result).hasSize(7); // 5 plus 2 existing projects: p and
+                                   // All-Users
 
     r = GET("/projects/?n=2");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     result = toProjectInfoMap(r);
-    assertEquals(2, result.size());
+    assertThat(result).hasSize(2);
   }
 
   @Test
-  public void listProjectsWithPrefix() throws IOException, JSchException {
+  public void listProjectsWithPrefix() throws Exception {
     Project.NameKey someProject = new Project.NameKey("some-project");
     createProject(sshSession, someProject.get());
     Project.NameKey someOtherProject =
@@ -121,15 +112,20 @@
     Project.NameKey projectAwesome = new Project.NameKey("project-awesome");
     createProject(sshSession, projectAwesome.get());
 
+    assertThat(GET("/projects/?p=some&r=.*").getStatusCode()).isEqualTo(
+        HttpStatus.SC_BAD_REQUEST);
+    assertThat(GET("/projects/?p=some&m=some").getStatusCode()).isEqualTo(
+        HttpStatus.SC_BAD_REQUEST);
+
     RestResponse r = GET("/projects/?p=some");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     Map<String, ProjectInfo> result = toProjectInfoMap(r);
     assertProjects(Arrays.asList(someProject, someOtherProject),
         result.values());
   }
 
   @Test
-  public void listProjectsWithRegex() throws IOException, JSchException {
+  public void listProjectsWithRegex() throws Exception {
     Project.NameKey someProject = new Project.NameKey("some-project");
     createProject(sshSession, someProject.get());
     Project.NameKey someOtherProject =
@@ -138,41 +134,50 @@
     Project.NameKey projectAwesome = new Project.NameKey("project-awesome");
     createProject(sshSession, projectAwesome.get());
 
+    assertThat(GET("/projects/?r=[.*some").getStatusCode()).isEqualTo(
+        HttpStatus.SC_BAD_REQUEST);
+    assertThat(GET("/projects/?r=.*&p=s").getStatusCode()).isEqualTo(
+        HttpStatus.SC_BAD_REQUEST);
+    assertThat(GET("/projects/?r=.*&m=s").getStatusCode()).isEqualTo(
+        HttpStatus.SC_BAD_REQUEST);
+
     RestResponse r = GET("/projects/?r=.*some");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     Map<String, ProjectInfo> result = toProjectInfoMap(r);
     assertProjects(Arrays.asList(projectAwesome), result.values());
 
-    r = GET("/projects/?r=[.*some");
-    assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode());
+    r = GET("/projects/?r=some-project$");
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    result = toProjectInfoMap(r);
+    assertProjects(Arrays.asList(someProject), result.values());
 
     r = GET("/projects/?r=.*");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     result = toProjectInfoMap(r);
     assertProjects(Arrays.asList(someProject, someOtherProject, projectAwesome,
         project, allUsers), result.values());
   }
 
   @Test
-  public void listProjectsWithSkip() throws IOException, JSchException {
+  public void listProjectsWithSkip() throws Exception {
     for (int i = 0; i < 5; i++) {
       createProject(sshSession, new Project.NameKey("someProject" + i).get());
     }
 
     RestResponse r = GET("/projects/");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     Map<String, ProjectInfo> result = toProjectInfoMap(r);
-    assertEquals(7, result.size()); // 5 plus 2 existing projects: p and
-                                    // All-Users
+    assertThat(result).hasSize(7); // 5 plus 2 existing projects: p and
+                                   // All-Users
 
     r = GET("/projects/?S=6");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     result = toProjectInfoMap(r);
-    assertEquals(1, result.size());
+    assertThat(result).hasSize(1);
   }
 
   @Test
-  public void listProjectsWithSubstring() throws IOException, JSchException {
+  public void listProjectsWithSubstring() throws Exception {
     Project.NameKey someProject = new Project.NameKey("some-project");
     createProject(sshSession, someProject.get());
     Project.NameKey someOtherProject =
@@ -181,8 +186,13 @@
     Project.NameKey projectAwesome = new Project.NameKey("project-awesome");
     createProject(sshSession, projectAwesome.get());
 
+    assertThat(GET("/projects/?m=some&r=.*").getStatusCode()).isEqualTo(
+        HttpStatus.SC_BAD_REQUEST);
+    assertThat(GET("/projects/?m=some&p=some").getStatusCode()).isEqualTo(
+        HttpStatus.SC_BAD_REQUEST);
+
     RestResponse r = GET("/projects/?m=some");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     Map<String, ProjectInfo> result = toProjectInfoMap(r);
     assertProjects(
         Arrays.asList(someProject, someOtherProject, projectAwesome),
@@ -190,7 +200,7 @@
   }
 
   @Test
-  public void listProjectsWithTree() throws IOException, JSchException {
+  public void listProjectsWithTree() throws Exception {
     Project.NameKey someParentProject =
         new Project.NameKey("some-parent-project");
     createProject(sshSession, someParentProject.get());
@@ -199,25 +209,25 @@
     createProject(sshSession, someChildProject.get(), someParentProject);
 
     RestResponse r = GET("/projects/?tree");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     Map<String, ProjectInfo> result = toProjectInfoMap(r);
-    assertNotNull(result.get(someChildProject.get()));
-    assertEquals(someParentProject.get(),
-        result.get(someChildProject.get()).parent);
+    assertThat(result.get(someChildProject.get())).isNotNull();
+    assertThat(result.get(someChildProject.get()).parent).isEqualTo(
+        someParentProject.get());
   }
 
   @Test
-  public void listProjectWithType() throws RestApiException, IOException {
+  public void listProjectWithType() throws Exception {
     RestResponse r = GET("/projects/?type=PERMISSIONS");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     Map<String, ProjectInfo> result = toProjectInfoMap(r);
-    assertEquals(1, result.size());
-    assertNotNull(result.get(allProjects.get()));
+    assertThat(result).hasSize(1);
+    assertThat(result.get(allProjects.get())).isNotNull();
 
     r = GET("/projects/?type=ALL");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     result = toProjectInfoMap(r);
-    assertEquals(3, result.size());
+    assertThat(result).hasSize(3);
     assertProjects(Arrays.asList(allProjects, allUsers, project),
         result.values());
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
index 3354cb8..95f46e8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
@@ -14,9 +14,7 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
@@ -42,29 +40,33 @@
           return new Project.NameKey(info.name != null ? info.name : Url
               .decode(info.id)).equals(p);
         }}, null);
-      assertNotNull("missing project: " + p, info);
+      assertThat(info).isNotNull();
       actual.remove(info);
     }
-    assertTrue("unexpected projects: " + actual, actual.isEmpty());
+    assertThat((Iterable<?>)actual).isEmpty();
   }
 
   public static void assertProjectInfo(Project project, ProjectInfo info) {
     if (info.name != null) {
       // 'name' is not set if returned in a map
-      assertEquals(project.getName(), info.name);
+      assertThat(info.name).isEqualTo(project.getName());
     }
-    assertEquals(project.getName(), Url.decode(info.id));
+    assertThat(Url.decode(info.id)).isEqualTo(project.getName());
     Project.NameKey parentName = project.getParent(new Project.NameKey("All-Projects"));
-    assertEquals(parentName != null ? parentName.get() : null, info.parent);
-    assertEquals(project.getDescription(), Strings.nullToEmpty(info.description));
+    if (parentName != null) {
+      assertThat(info.parent).isEqualTo(parentName.get());
+    } else {
+      assertThat(info.parent).isNull();
+    }
+    assertThat(Strings.nullToEmpty(info.description)).isEqualTo(
+        project.getDescription());
   }
 
   public static void assertProjectOwners(Set<AccountGroup.UUID> expectedOwners,
       ProjectState state) {
     for (AccountGroup.UUID g : state.getOwners()) {
-      assertTrue("unexpected owner group " + g, expectedOwners.remove(g));
+      assertThat(expectedOwners.remove(g)).isTrue();
     }
-    assertTrue("missing owner groups: " + expectedOwners,
-        expectedOwners.isEmpty());
+    assertThat((Iterable<?>)expectedOwners).isEmpty();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
index fb830e4..7e2af65 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java
@@ -14,72 +14,30 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.checkout;
 import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static com.google.gerrit.acceptance.GitUtil.fetch;
-import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.config.AllProjectsNameProvider;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
-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.eclipse.jgit.lib.Config;
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
-
 public class ProjectLevelConfigIT extends AbstractDaemonTest {
-
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
-  private ProjectCache projectCache;
-
-  @Inject
-  private AllProjectsNameProvider allProjects;
-
-  @Inject
-  private PushOneCommit.Factory pushFactory;
-
-  private ReviewDb db;
-  private SshSession sshSession;
-  private String project;
-  private Git git;
-
   @Before
   public void setUp() throws Exception {
-    sshSession = new SshSession(server, admin);
-
-    project = "p";
-    createProject(sshSession, project, null, true);
-    git = cloneProject(sshSession.getUrl() + "/" + project);
     fetch(git, RefNames.REFS_CONFIG + ":refs/heads/config");
     checkout(git, "refs/heads/config");
-
-    db = reviewDbProvider.open();
-  }
-
-  @After
-  public void cleanup() {
-    db.close();
   }
 
   @Test
-  public void accessProjectSpecificConfig() throws GitAPIException, IOException {
+  public void accessProjectSpecificConfig() throws Exception {
     String configName = "test.config";
     Config cfg = new Config();
     cfg.setString("s1", null, "k1", "v1");
@@ -89,18 +47,19 @@
             configName, cfg.toText());
     push.to(git, RefNames.REFS_CONFIG);
 
-    ProjectState state = projectCache.get(new Project.NameKey(project));
-    assertEquals(cfg.toText(), state.getConfig(configName).get().toText());
+    ProjectState state = projectCache.get(project);
+    assertThat(state.getConfig(configName).get().toText()).isEqualTo(
+        cfg.toText());
   }
 
   @Test
   public void nonExistingConfig() {
-    ProjectState state = projectCache.get(new Project.NameKey(project));
-    assertEquals("", state.getConfig("test.config").get().toText());
+    ProjectState state = projectCache.get(project);
+    assertThat(state.getConfig("test.config").get().toText()).isEqualTo("");
   }
 
   @Test
-  public void withInheritance() throws GitAPIException, IOException {
+  public void withInheritance() throws Exception {
     String configName = "test.config";
 
     Config parentCfg = new Config();
@@ -110,7 +69,7 @@
     parentCfg.setString("s2", "ss", "k4", "parentValue4");
 
     Git parentGit =
-        cloneProject(sshSession.getUrl() + "/" + allProjects.get().get(), false);
+        cloneProject(sshSession.getUrl() + "/" + allProjects.get(), false);
     fetch(parentGit, RefNames.REFS_CONFIG + ":refs/heads/config");
     checkout(parentGit, "refs/heads/config");
     PushOneCommit push =
@@ -125,7 +84,7 @@
         configName, cfg.toText());
     push.to(git, RefNames.REFS_CONFIG);
 
-    ProjectState state = projectCache.get(new Project.NameKey(project));
+    ProjectState state = projectCache.get(project);
 
     Config expectedCfg = new Config();
     expectedCfg.setString("s1", null, "k1", "childValue1");
@@ -133,9 +92,10 @@
     expectedCfg.setString("s2", "ss", "k3", "childValue2");
     expectedCfg.setString("s2", "ss", "k4", "parentValue4");
 
-    assertEquals(expectedCfg.toText(), state.getConfig(configName)
-        .getWithInheritance().toText());
+    assertThat(state.getConfig(configName).getWithInheritance().toText())
+        .isEqualTo(expectedCfg.toText());
 
-    assertEquals(cfg.toText(), state.getConfig(configName).get().toText());
+    assertThat(state.getConfig(configName).get().toText()).isEqualTo(
+        cfg.toText());
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
index 7c6f280..ac90ac0 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
@@ -14,95 +14,85 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.createProject;
-import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.project.SetParent;
-import com.google.inject.Inject;
-
-import com.jcraft.jsch.JSchException;
 
 import org.apache.http.HttpStatus;
 import org.junit.Test;
 
-import java.io.IOException;
-
 public class SetParentIT extends AbstractDaemonTest {
-
-  @Inject
-  private AllProjectsNameProvider allProjects;
-
   @Test
-  public void setParent_Forbidden() throws IOException, JSchException {
+  public void setParent_Forbidden() throws Exception {
     String parent = "parent";
     createProject(sshSession, parent, null, true);
     RestResponse r =
         userSession.put("/projects/" + project.get() + "/parent",
             newParentInput(parent));
-    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN);
     r.consume();
   }
 
   @Test
-  public void setParent() throws IOException, JSchException {
+  public void setParent() throws Exception {
     String parent = "parent";
     createProject(sshSession, parent, null, true);
     RestResponse r =
         adminSession.put("/projects/" + project.get() + "/parent",
             newParentInput(parent));
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     r.consume();
 
     r = adminSession.get("/projects/" + project.get() + "/parent");
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     String newParent =
         newGson().fromJson(r.getReader(), String.class);
-    assertEquals(parent, newParent);
+    assertThat(newParent).isEqualTo(parent);
     r.consume();
   }
 
   @Test
-  public void setParentForAllProjects_Conflict() throws IOException {
+  public void setParentForAllProjects_Conflict() throws Exception {
     RestResponse r =
         adminSession.put("/projects/" + allProjects.get() + "/parent",
             newParentInput(project.get()));
-    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
     r.consume();
   }
 
   @Test
-  public void setInvalidParent_Conflict() throws IOException, JSchException {
+  public void setInvalidParent_Conflict() throws Exception {
     RestResponse r =
         adminSession.put("/projects/" + project.get() + "/parent",
             newParentInput(project.get()));
-    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
     r.consume();
 
     String child = "child";
     createProject(sshSession, child, project, true);
     r = adminSession.put("/projects/" + project.get() + "/parent",
            newParentInput(child));
-    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
     r.consume();
 
     String grandchild = "grandchild";
     createProject(sshSession, grandchild, new Project.NameKey(child), true);
     r = adminSession.put("/projects/" + project.get() + "/parent",
            newParentInput(grandchild));
-    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
     r.consume();
   }
 
   @Test
-  public void setNonExistingParent_UnprocessibleEntity() throws IOException {
+  public void setNonExistingParent_UnprocessibleEntity() throws Exception {
     RestResponse r =
         adminSession.put("/projects/" + project.get() + "/parent",
             newParentInput("non-existing"));
-    assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_UNPROCESSABLE_ENTITY);
     r.consume();
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
new file mode 100644
index 0000000..c6cf647
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -0,0 +1,143 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.common.TagInfo;
+import com.google.gson.reflect.TypeToken;
+
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+
+import java.util.List;
+
+public class TagsIT extends AbstractDaemonTest {
+  @Test
+  public void listTagsOfNonExistingProject_NotFound() throws Exception {
+    assertThat(adminSession.get("/projects/non-existing/tags").getStatusCode())
+        .isEqualTo(HttpStatus.SC_NOT_FOUND);
+  }
+
+  @Test
+  public void listTagsOfNonVisibleProject_NotFound() throws Exception {
+    blockRead(project, "refs/*");
+    assertThat(
+        userSession.get("/projects/" + project.get() + "/tags").getStatusCode())
+        .isEqualTo(HttpStatus.SC_NOT_FOUND);
+  }
+
+  @Test
+  public void listTags() throws Exception {
+    grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
+    grant(Permission.CREATE, project, "refs/tags/*");
+    grant(Permission.PUSH, project, "refs/tags/*");
+
+    PushOneCommit.Tag tag1 = new PushOneCommit.Tag("v1.0");
+    PushOneCommit push1 = pushFactory.create(db, admin.getIdent());
+    push1.setTag(tag1);
+    PushOneCommit.Result r1 = push1.to(git, "refs/for/master%submit");
+    r1.assertOkStatus();
+
+    PushOneCommit.AnnotatedTag tag2 =
+        new PushOneCommit.AnnotatedTag("v2.0", "annotation", admin.getIdent());
+    PushOneCommit push2 = pushFactory.create(db, admin.getIdent());
+    push2.setTag(tag2);
+    PushOneCommit.Result r2 = push2.to(git, "refs/for/master%submit");
+    r2.assertOkStatus();
+
+    List<TagInfo> result =
+        toTagInfoList(adminSession.get("/projects/" + project.get() + "/tags"));
+    assertThat(result).hasSize(2);
+
+    TagInfo t = result.get(0);
+    assertThat(t.ref).isEqualTo("refs/tags/" + tag1.name);
+    assertThat(t.revision).isEqualTo(r1.getCommitId().getName());
+
+    t = result.get(1);
+    assertThat(t.ref).isEqualTo("refs/tags/" + tag2.name);
+    assertThat(t.object).isEqualTo(r2.getCommitId().getName());
+    assertThat(t.message).isEqualTo(tag2.message);
+    assertThat(t.tagger.name).isEqualTo(tag2.tagger.getName());
+    assertThat(t.tagger.email).isEqualTo(tag2.tagger.getEmailAddress());
+  }
+
+  @Test
+  public void listTagsOfNonVisibleBranch() throws Exception {
+    grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
+    grant(Permission.SUBMIT, project, "refs/for/refs/heads/hidden");
+    grant(Permission.CREATE, project, "refs/tags/*");
+    grant(Permission.PUSH, project, "refs/tags/*");
+
+    PushOneCommit.Tag tag1 = new PushOneCommit.Tag("v1.0");
+    PushOneCommit push1 = pushFactory.create(db, admin.getIdent());
+    push1.setTag(tag1);
+    PushOneCommit.Result r1 = push1.to(git, "refs/for/master%submit");
+    r1.assertOkStatus();
+
+    pushTo("refs/heads/hidden");
+    PushOneCommit.Tag tag2 = new PushOneCommit.Tag("v2.0");
+    PushOneCommit push2 = pushFactory.create(db, admin.getIdent());
+    push2.setTag(tag2);
+    PushOneCommit.Result r2 = push2.to(git, "refs/for/hidden%submit");
+    r2.assertOkStatus();
+
+    List<TagInfo> result =
+        toTagInfoList(adminSession.get("/projects/" + project.get() + "/tags"));
+    assertThat(result).hasSize(2);
+    assertThat(result.get(0).ref).isEqualTo("refs/tags/" + tag1.name);
+    assertThat(result.get(0).revision).isEqualTo(r1.getCommitId().getName());
+    assertThat(result.get(1).ref).isEqualTo("refs/tags/" + tag2.name);
+    assertThat(result.get(1).revision).isEqualTo(r2.getCommitId().getName());
+
+    blockRead(project, "refs/heads/hidden");
+    result =
+        toTagInfoList(adminSession.get("/projects/" + project.get() + "/tags"));
+    assertThat(result).hasSize(1);
+    assertThat(result.get(0).ref).isEqualTo("refs/tags/" + tag1.name);
+    assertThat(result.get(0).revision).isEqualTo(r1.getCommitId().getName());
+  }
+
+  @Test
+  public void getTag() throws Exception {
+    grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
+    grant(Permission.CREATE, project, "refs/tags/*");
+    grant(Permission.PUSH, project, "refs/tags/*");
+
+    PushOneCommit.Tag tag1 = new PushOneCommit.Tag("v1.0");
+    PushOneCommit push1 = pushFactory.create(db, admin.getIdent());
+    push1.setTag(tag1);
+    PushOneCommit.Result r1 = push1.to(git, "refs/for/master%submit");
+    r1.assertOkStatus();
+
+    RestResponse response =
+        adminSession.get("/projects/" + project.get() + "/tags/" + tag1.name);
+    TagInfo tagInfo =
+        newGson().fromJson(response.getReader(), TagInfo.class);
+    assertThat(tagInfo.ref).isEqualTo("refs/tags/" + tag1.name);
+    assertThat(tagInfo.revision).isEqualTo(r1.getCommitId().getName());
+  }
+
+  private static List<TagInfo> toTagInfoList(RestResponse r) throws Exception {
+    List<TagInfo> result =
+        newGson().fromJson(r.getReader(),
+            new TypeToken<List<TagInfo>>() {}.getType());
+    return result;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
new file mode 100644
index 0000000..541d1b8
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -0,0 +1,278 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.server.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.Comment;
+import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.ChangesCollection;
+import com.google.gerrit.server.change.PostReview;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.Revisions;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.apache.http.HttpStatus;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.sql.Timestamp;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class CommentsIT extends AbstractDaemonTest {
+  @ConfigSuite.Config
+  public static Config noteDbEnabled() {
+    return NotesMigration.allEnabledConfig();
+  }
+
+  @Inject
+  private Provider<ChangesCollection> changes;
+
+  @Inject
+  private Provider<Revisions> revisions;
+
+  @Inject
+  private Provider<PostReview> postReview;
+
+  private final Integer lines[] = {0, 1};
+
+  @Test
+  public void createDraft() throws Exception {
+    for (Integer line : lines) {
+      PushOneCommit.Result r = createChange();
+      String changeId = r.getChangeId();
+      String revId = r.getCommit().getName();
+      ReviewInput.CommentInput comment = newCommentInfo(
+          "file1", Side.REVISION, line, "comment 1");
+      addDraft(changeId, revId, comment);
+      Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
+      assertThat(result).hasSize(1);
+      CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
+      assertCommentInfo(comment, actual);
+    }
+  }
+
+  @Test
+  public void postComment() throws Exception {
+    for (Integer line : lines) {
+      String file = "file";
+      String contents = "contents " + line;
+      PushOneCommit push = pushFactory.create(db, admin.getIdent(),
+          "first subject", file, contents);
+      PushOneCommit.Result r = push.to(git, "refs/for/master");
+      String changeId = r.getChangeId();
+      String revId = r.getCommit().getName();
+      ReviewInput input = new ReviewInput();
+      ReviewInput.CommentInput comment = newCommentInfo(
+          file, Side.REVISION, line, "comment 1");
+      input.comments = new HashMap<>();
+      input.comments.put(comment.path, Lists.newArrayList(comment));
+      revision(r).review(input);
+      Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
+      assertThat(result).isNotEmpty();
+      CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
+      assertCommentInfo(comment, actual);
+    }
+  }
+
+  @Test
+  public void putDraft() throws Exception {
+    for (Integer line : lines) {
+      PushOneCommit.Result r = createChange();
+      String changeId = r.getChangeId();
+      String revId = r.getCommit().getName();
+      ReviewInput.CommentInput comment = newCommentInfo(
+          "file1", Side.REVISION, line, "comment 1");
+      addDraft(changeId, revId, comment);
+      Map<String, List<CommentInfo>> result = getDraftComments(changeId, revId);
+      CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
+      assertCommentInfo(comment, actual);
+      String uuid = actual.id;
+      comment.message = "updated comment 1";
+      updateDraft(changeId, revId, comment, uuid);
+      result = getDraftComments(changeId, revId);
+      actual = Iterables.getOnlyElement(result.get(comment.path));
+      assertCommentInfo(comment, actual);
+    }
+  }
+
+  @Test
+  public void getDraft() throws Exception {
+    for (Integer line : lines) {
+      PushOneCommit.Result r = createChange();
+      String changeId = r.getChangeId();
+      String revId = r.getCommit().getName();
+      ReviewInput.CommentInput comment = newCommentInfo(
+          "file1", Side.REVISION, line, "comment 1");
+      CommentInfo returned = addDraft(changeId, revId, comment);
+      CommentInfo actual = getDraftComment(changeId, revId, returned.id);
+      assertCommentInfo(comment, actual);
+    }
+  }
+
+  @Test
+  public void deleteDraft() throws Exception {
+    for (Integer line : lines) {
+      PushOneCommit.Result r = createChange();
+      String changeId = r.getChangeId();
+      String revId = r.getCommit().getName();
+      ReviewInput.CommentInput comment = newCommentInfo(
+          "file1", Side.REVISION, line, "comment 1");
+      CommentInfo returned = addDraft(changeId, revId, comment);
+      deleteDraft(changeId, revId, returned.id);
+      Map<String, List<CommentInfo>> drafts = getDraftComments(changeId, revId);
+      assertThat(drafts).isEmpty();
+    }
+  }
+
+  @Test
+  public void insertCommentsWithHistoricTimestamp() throws Exception {
+    Timestamp timestamp = new Timestamp(0);
+    for (Integer line : lines) {
+      String file = "file";
+      String contents = "contents " + line;
+      PushOneCommit push = pushFactory.create(db, admin.getIdent(),
+          "first subject", file, contents);
+      PushOneCommit.Result r = push.to(git, "refs/for/master");
+      String changeId = r.getChangeId();
+      String revId = r.getCommit().getName();
+      ReviewInput input = new ReviewInput();
+      ReviewInput.CommentInput comment = newCommentInfo(
+          file, Side.REVISION, line, "comment 1");
+      comment.updated = timestamp;
+      input.comments = new HashMap<>();
+      input.comments.put(comment.path, Lists.newArrayList(comment));
+      ChangeResource changeRsrc =
+          changes.get().parse(TopLevelResource.INSTANCE,
+              IdString.fromDecoded(changeId));
+      RevisionResource revRsrc =
+          revisions.get().parse(changeRsrc, IdString.fromDecoded(revId));
+      postReview.get().apply(revRsrc, input, timestamp);
+      Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
+      assertThat(result).isNotEmpty();
+      CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path));
+      assertCommentInfo(comment, actual);
+      assertThat(comment.updated).isEqualTo(timestamp);
+    }
+  }
+
+  private CommentInfo addDraft(String changeId, String revId,
+      ReviewInput.CommentInput c) throws IOException {
+    RestResponse r = userSession.put(
+        "/changes/" + changeId + "/revisions/" + revId + "/drafts", c);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CREATED);
+    return newGson().fromJson(r.getReader(), CommentInfo.class);
+  }
+
+  private void updateDraft(String changeId, String revId,
+      ReviewInput.CommentInput c, String uuid) throws IOException {
+    RestResponse r = userSession.put(
+        "/changes/" + changeId + "/revisions/" + revId + "/drafts/" + uuid, c);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+  }
+
+  private void deleteDraft(String changeId, String revId, String uuid)
+      throws IOException {
+    RestResponse r = userSession.delete(
+        "/changes/" + changeId + "/revisions/" + revId + "/drafts/" + uuid);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+  }
+
+  private Map<String, List<CommentInfo>> getPublishedComments(String changeId,
+      String revId) throws IOException {
+    RestResponse r = userSession.get(
+        "/changes/" + changeId + "/revisions/" + revId + "/comments/");
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    Type mapType = new TypeToken<Map<String, List<CommentInfo>>>() {}.getType();
+    return newGson().fromJson(r.getReader(), mapType);
+  }
+
+  private Map<String, List<CommentInfo>> getDraftComments(String changeId,
+      String revId) throws IOException {
+    RestResponse r = userSession.get(
+        "/changes/" + changeId + "/revisions/" + revId + "/drafts/");
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    Type mapType = new TypeToken<Map<String, List<CommentInfo>>>() {}.getType();
+    return newGson().fromJson(r.getReader(), mapType);
+  }
+
+  private CommentInfo getDraftComment(String changeId, String revId,
+      String uuid) throws IOException {
+    RestResponse r = userSession.get(
+        "/changes/" + changeId + "/revisions/" + revId + "/drafts/" + uuid);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    return newGson().fromJson(r.getReader(), CommentInfo.class);
+  }
+
+  private static void assertCommentInfo(ReviewInput.CommentInput expected,
+      CommentInfo actual) {
+    assertThat(actual.line).isEqualTo(expected.line);
+    assertThat(actual.message).isEqualTo(expected.message);
+    assertThat(actual.inReplyTo).isEqualTo(expected.inReplyTo);
+    assertCommentRange(expected.range, actual.range);
+    if (actual.side == null) {
+      assertThat(Side.REVISION).isEqualTo(expected.side);
+    }
+  }
+
+  private static void assertCommentRange(Comment.Range expected,
+      Comment.Range actual) {
+    if (expected == null) {
+      assertThat(actual).isNull();
+    } else {
+      assertThat(actual).isNotNull();
+      assertThat(actual.startLine).isEqualTo(expected.startLine);
+      assertThat(actual.startCharacter).isEqualTo(expected.startCharacter);
+      assertThat(actual.endLine).isEqualTo(expected.endLine);
+      assertThat(actual.endCharacter).isEqualTo(expected.endCharacter);
+    }
+  }
+
+  private ReviewInput.CommentInput newCommentInfo(String path,
+      Side side, int line, String message) {
+    ReviewInput.CommentInput input = new ReviewInput.CommentInput();
+    input.path = path;
+    input.side = side;
+    input.line = line != 0 ? line : null;
+    input.message = message;
+    if (line != 0) {
+      Comment.Range range = new Comment.Range();
+      range.startLine = 1;
+      range.startCharacter = 1;
+      range.endLine = 1;
+      range.endCharacter = 5;
+      input.range = range;
+    }
+    return input;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index 1456086..6cd39ab 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -14,43 +14,50 @@
 
 package com.google.gerrit.acceptance.server.change;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.add;
 import static com.google.gerrit.acceptance.GitUtil.createCommit;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
-import static org.junit.Assert.assertEquals;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil.Commit;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestSession;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.change.GetRelated.ChangeAndCommit;
 import com.google.gerrit.server.change.GetRelated.RelatedInfo;
+import com.google.gerrit.server.edit.ChangeEditModifier;
+import com.google.gerrit.server.edit.ChangeEditUtil;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
 
 import org.eclipse.jgit.api.ResetCommand.ResetType;
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.junit.Test;
 
 import java.io.IOException;
 import java.util.List;
 
 public class GetRelatedIT extends AbstractDaemonTest {
+  @Inject
+  private ChangeEditUtil editUtil;
+
+  @Inject
+  private ChangeEditModifier editModifier;
 
   @Test
-  public void getRelatedNoResult() throws GitAPIException,
-      IOException, Exception {
+  public void getRelatedNoResult() throws Exception {
     PushOneCommit push = pushFactory.create(db, admin.getIdent());
     PatchSet.Id ps = push.to(git, "refs/for/master").getPatchSetId();
     List<ChangeAndCommit> related = getRelated(ps);
-    assertEquals(0, related.size());
+    assertThat(related).isEmpty();
   }
 
   @Test
-  public void getRelatedLinear() throws GitAPIException,
-      IOException, Exception {
+  public void getRelatedLinear() throws Exception {
     add(git, "a.txt", "1");
     Commit c1 = createCommit(git, admin.getIdent(), "subject: 1");
     add(git, "b.txt", "2");
@@ -59,15 +66,16 @@
 
     for (Commit c : ImmutableList.of(c2, c1)) {
       List<ChangeAndCommit> related = getRelated(getPatchSetId(c));
-      assertEquals(2, related.size());
-      assertEquals("related to " + c.getChangeId(), c2.getChangeId(), related.get(0).changeId);
-      assertEquals("related to " + c.getChangeId(), c1.getChangeId(), related.get(1).changeId);
+      assertThat(related).hasSize(2);
+      assertThat(related.get(0).changeId)
+          .named("related to " + c.getChangeId()).isEqualTo(c2.getChangeId());
+      assertThat(related.get(1).changeId)
+          .named("related to " + c.getChangeId()).isEqualTo(c1.getChangeId());
     }
   }
 
   @Test
-  public void getRelatedReorder() throws GitAPIException,
-      IOException, Exception {
+  public void getRelatedReorder() throws Exception {
     // Create two commits and push.
     add(git, "a.txt", "1");
     Commit c1 = createCommit(git, admin.getIdent(), "subject: 1");
@@ -86,22 +94,25 @@
 
     for (PatchSet.Id ps : ImmutableList.of(c2ps2, c1ps2)) {
       List<ChangeAndCommit> related = getRelated(ps);
-      assertEquals(2, related.size());
-      assertEquals("related to " + ps, c1.getChangeId(), related.get(0).changeId);
-      assertEquals("related to " + ps, c2.getChangeId(), related.get(1).changeId);
+      assertThat(related).hasSize(2);
+      assertThat(related.get(0).changeId).named("related to " + ps).isEqualTo(
+          c1.getChangeId());
+      assertThat(related.get(1).changeId).named("related to " + ps).isEqualTo(
+          c2.getChangeId());
     }
 
     for (PatchSet.Id ps : ImmutableList.of(c2ps1, c1ps1)) {
       List<ChangeAndCommit> related = getRelated(ps);
-      assertEquals(2, related.size());
-      assertEquals("related to " + ps, c2.getChangeId(), related.get(0).changeId);
-      assertEquals("related to " + ps, c1.getChangeId(), related.get(1).changeId);
+      assertThat(related).hasSize(2);
+      assertThat(related.get(0).changeId).named("related to " + ps).isEqualTo(
+          c2.getChangeId());
+      assertThat(related.get(1).changeId).named("related to " + ps).isEqualTo(
+          c1.getChangeId());
     }
   }
 
   @Test
-  public void getRelatedReorderAndExtend() throws GitAPIException,
-      IOException, Exception {
+  public void getRelatedReorderAndExtend() throws Exception {
     // Create two commits and push.
     add(git, "a.txt", "1");
     Commit c1 = createCommit(git, admin.getIdent(), "subject: 1");
@@ -124,30 +135,79 @@
 
     for (PatchSet.Id ps : ImmutableList.of(c3ps1, c2ps2, c1ps2)) {
       List<ChangeAndCommit> related = getRelated(ps);
-      assertEquals(3, related.size());
-      assertEquals("related to " + ps, c3.getChangeId(), related.get(0).changeId);
-      assertEquals("related to " + ps, c1.getChangeId(), related.get(1).changeId);
-      assertEquals("related to " + ps, c2.getChangeId(), related.get(2).changeId);
+      assertThat(related).hasSize(3);
+      assertThat(related.get(0).changeId).named("related to " + ps).isEqualTo(
+          c3.getChangeId());
+      assertThat(related.get(1).changeId).named("related to " + ps).isEqualTo(
+          c1.getChangeId());
+      assertThat(related.get(2).changeId).named("related to " + ps).isEqualTo(
+          c2.getChangeId());
     }
 
     for (PatchSet.Id ps : ImmutableList.of(c2ps1, c1ps1)) {
       List<ChangeAndCommit> related = getRelated(ps);
-      assertEquals(3, related.size());
-      assertEquals("related to " + ps, c3.getChangeId(), related.get(0).changeId);
-      assertEquals("related to " + ps, c2.getChangeId(), related.get(1).changeId);
-      assertEquals("related to " + ps, c1.getChangeId(), related.get(2).changeId);
+      assertThat(related).hasSize(3);
+      assertThat(related.get(0).changeId).named("related to " + ps).isEqualTo(
+          c3.getChangeId());
+      assertThat(related.get(1).changeId).named("related to " + ps).isEqualTo(
+          c2.getChangeId());
+      assertThat(related.get(2).changeId).named("related to " + ps).isEqualTo(
+          c1.getChangeId());
     }
   }
 
+  @Test
+  public void getRelatedEdit() throws Exception {
+    add(git, "a.txt", "1");
+    Commit c1 = createCommit(git, admin.getIdent(), "subject: 1");
+    add(git, "b.txt", "2");
+    Commit c2 = createCommit(git, admin.getIdent(), "subject: 2");
+    add(git, "b.txt", "3");
+    Commit c3 = createCommit(git, admin.getIdent(), "subject: 3");
+    pushHead(git, "refs/for/master", false);
+
+    Change ch2 = getChange(c2).change();
+    editModifier.createEdit(ch2, getPatchSet(ch2));
+    editModifier.modifyFile(editUtil.byChange(ch2).get(), "a.txt",
+        RestSession.newRawInput(new byte[] {'a'}));
+    String editRev = editUtil.byChange(ch2).get().getRevision().get();
+
+    List<ChangeAndCommit> related = getRelated(ch2.getId(), 0);
+    assertThat(related).hasSize(3);
+    assertThat(related.get(0).changeId).named("related to " + c2.getChangeId())
+        .isEqualTo(c3.getChangeId());
+    assertThat(related.get(1).changeId).named("related to " + c2.getChangeId())
+        .isEqualTo(c2.getChangeId());
+    assertThat(related.get(1)._revisionNumber.intValue()).named(
+        "has edit revision number").isEqualTo(0);
+    assertThat(related.get(1).commit.commit).named(
+        "has edit revision " + editRev).isEqualTo(editRev);
+    assertThat(related.get(2).changeId).named("related to " + c2.getChangeId())
+        .isEqualTo(c1.getChangeId());
+  }
+
   private List<ChangeAndCommit> getRelated(PatchSet.Id ps) throws IOException {
+    return getRelated(ps.getParentKey(), ps.get());
+  }
+
+  private List<ChangeAndCommit> getRelated(Change.Id changeId, int ps)
+      throws IOException {
     String url = String.format("/changes/%d/revisions/%d/related",
-        ps.getParentKey().get(), ps.get());
+        changeId.get(), ps);
     return newGson().fromJson(adminSession.get(url).getReader(),
         RelatedInfo.class).changes;
   }
 
   private PatchSet.Id getPatchSetId(Commit c) throws OrmException {
+    return getChange(c).change().currentPatchSetId();
+  }
+
+  private PatchSet getPatchSet(Change c) throws OrmException {
+    return db.patchSets().get(c.currentPatchSetId());
+  }
+
+  private ChangeData getChange(Commit c) throws OrmException {
     return Iterables.getOnlyElement(
-        db.changes().byKey(new Change.Key(c.getChangeId()))).currentPatchSetId();
+        queryProvider.get().byKeyPrefix(c.getChangeId()));
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
index 70a0a60..d598b06 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
@@ -14,16 +14,16 @@
 
 package com.google.gerrit.acceptance.server.change;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.add;
 import static com.google.gerrit.acceptance.GitUtil.amendCommit;
 import static com.google.gerrit.acceptance.GitUtil.createCommit;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static com.google.gerrit.acceptance.GitUtil.rm;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil.Commit;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Patch;
@@ -32,17 +32,15 @@
 import com.google.gerrit.server.patch.PatchListEntry;
 import com.google.gerrit.server.patch.PatchListKey;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.api.ResetCommand.ResetType;
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
 
-import java.io.IOException;
 import java.util.List;
 
+@NoHttpd
 public class PatchListCacheIT extends AbstractDaemonTest {
   private static String SUBJECT_1 = "subject 1";
   private static String SUBJECT_2 = "subject 2";
@@ -56,8 +54,7 @@
   private PatchListCache patchListCache;
 
   @Test
-  public void listPatchesAgainstBase() throws GitAPIException, IOException,
-      PatchListNotAvailableException, OrmException, RestApiException {
+  public void listPatchesAgainstBase() throws Exception {
     add(git, FILE_D, "4");
     createCommit(git, admin.getIdent(), SUBJECT_1);
     pushHead(git, "refs/heads/master", false);
@@ -70,7 +67,7 @@
 
     // Compare Change 1,1 with Base (+FILE_A, -FILE_D)
     List<PatchListEntry> entries = getCurrentPatches(c.getChangeId());
-    assertEquals(3, entries.size());
+    assertThat(entries).hasSize(3);
     assertAdded(Patch.COMMIT_MSG, entries.get(0));
     assertAdded(FILE_A, entries.get(1));
     assertDeleted(FILE_D, entries.get(2));
@@ -82,7 +79,7 @@
     entries = getCurrentPatches(c.getChangeId());
 
     // Compare Change 1,2 with Base (+FILE_A, +FILE_B, -FILE_D)
-    assertEquals(4, entries.size());
+    assertThat(entries).hasSize(4);
     assertAdded(Patch.COMMIT_MSG, entries.get(0));
     assertAdded(FILE_A, entries.get(1));
     assertAdded(FILE_B, entries.get(2));
@@ -90,9 +87,7 @@
   }
 
   @Test
-  public void listPatchesAgainstBaseWithRebase() throws GitAPIException,
-      IOException, PatchListNotAvailableException, OrmException,
-      RestApiException {
+  public void listPatchesAgainstBaseWithRebase() throws Exception {
     add(git, FILE_D, "4");
     createCommit(git, admin.getIdent(), SUBJECT_1);
     pushHead(git, "refs/heads/master", false);
@@ -103,7 +98,7 @@
     Commit c = createCommit(git, admin.getIdent(), SUBJECT_2);
     pushHead(git, "refs/for/master", false);
     List<PatchListEntry> entries = getCurrentPatches(c.getChangeId());
-    assertEquals(3, entries.size());
+    assertThat(entries).hasSize(3);
     assertAdded(Patch.COMMIT_MSG, entries.get(0));
     assertAdded(FILE_A, entries.get(1));
     assertDeleted(FILE_D, entries.get(2));
@@ -120,16 +115,14 @@
 
     // Compare Change 1,2 with Base (+FILE_A, -FILE_D))
     entries = getCurrentPatches(c.getChangeId());
-    assertEquals(3, entries.size());
+    assertThat(entries).hasSize(3);
     assertAdded(Patch.COMMIT_MSG, entries.get(0));
     assertAdded(FILE_A, entries.get(1));
     assertDeleted(FILE_D, entries.get(2));
   }
 
   @Test
-  public void listPatchesAgainstOtherPatchSet() throws GitAPIException,
-      IOException, PatchListNotAvailableException, OrmException,
-      RestApiException {
+  public void listPatchesAgainstOtherPatchSet() throws Exception {
     add(git, FILE_D, "4");
     createCommit(git, admin.getIdent(), SUBJECT_1);
     pushHead(git, "refs/heads/master", false);
@@ -151,15 +144,13 @@
 
     // Compare Change 1,1 with Change 1,2 (+FILE_B)
     List<PatchListEntry>  entries = getPatches(a, b);
-    assertEquals(2, entries.size());
+    assertThat(entries).hasSize(2);
     assertModified(Patch.COMMIT_MSG, entries.get(0));
     assertAdded(FILE_B, entries.get(1));
   }
 
   @Test
-  public void listPatchesAgainstOtherPatchSetWithRebase()
-      throws GitAPIException, IOException, PatchListNotAvailableException,
-      OrmException, RestApiException {
+  public void listPatchesAgainstOtherPatchSetWithRebase() throws Exception {
     add(git, FILE_D, "4");
     createCommit(git, admin.getIdent(), SUBJECT_1);
     pushHead(git, "refs/heads/master", false);
@@ -186,42 +177,42 @@
 
     // Compare Change 1,1 with Change 1,2 (+FILE_C)
     List<PatchListEntry>  entries = getPatches(a, b);
-    assertEquals(2, entries.size());
+    assertThat(entries).hasSize(2);
     assertModified(Patch.COMMIT_MSG, entries.get(0));
     assertAdded(FILE_C, entries.get(1));
   }
 
   private static void assertAdded(String expectedNewName, PatchListEntry e) {
     assertName(expectedNewName, e);
-    assertEquals(ChangeType.ADDED, e.getChangeType());
+    assertThat(e.getChangeType()).isEqualTo(ChangeType.ADDED);
   }
 
   private static void assertModified(String expectedNewName, PatchListEntry e) {
     assertName(expectedNewName, e);
-    assertEquals(ChangeType.MODIFIED, e.getChangeType());
+    assertThat(e.getChangeType()).isEqualTo(ChangeType.MODIFIED);
   }
 
   private static void assertDeleted(String expectedNewName, PatchListEntry e) {
     assertName(expectedNewName, e);
-    assertEquals(ChangeType.DELETED, e.getChangeType());
+    assertThat(e.getChangeType()).isEqualTo(ChangeType.DELETED);
   }
 
   private static void assertName(String expectedNewName, PatchListEntry e) {
-    assertEquals(expectedNewName, e.getNewName());
-    assertNull(e.getOldName());
+    assertThat(e.getNewName()).isEqualTo(expectedNewName);
+    assertThat(e.getOldName()).isNull();
   }
 
   private List<PatchListEntry> getCurrentPatches(String changeId)
-      throws PatchListNotAvailableException, OrmException, RestApiException {
+      throws PatchListNotAvailableException, RestApiException {
     return patchListCache.get(getKey(null, getCurrentRevisionId(changeId))).getPatches();
   }
 
   private List<PatchListEntry> getPatches(ObjectId revisionIdA, ObjectId revisionIdB)
-      throws PatchListNotAvailableException, OrmException {
+      throws PatchListNotAvailableException {
     return patchListCache.get(getKey(revisionIdA, revisionIdB)).getPatches();
   }
 
-  private PatchListKey getKey(ObjectId revisionIdA, ObjectId revisionIdB) throws OrmException {
+  private PatchListKey getKey(ObjectId revisionIdA, ObjectId revisionIdB) {
     return new PatchListKey(project, revisionIdA, revisionIdB, Whitespace.IGNORE_NONE);
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
index 2a20a2c..cd76c12 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
@@ -14,14 +14,10 @@
 
 package com.google.gerrit.acceptance.server.project;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
-import static com.google.gerrit.server.project.Util.allow;
 import static com.google.gerrit.server.project.Util.category;
 import static com.google.gerrit.server.project.Util.value;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -32,12 +28,10 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.inject.Inject;
+import com.google.gerrit.server.project.Util;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -45,15 +39,6 @@
 @NoHttpd
 public class CustomLabelIT extends AbstractDaemonTest {
 
-  @Inject
-  private ProjectCache projectCache;
-
-  @Inject
-  private AllProjectsName allProjects;
-
-  @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
-
   private final LabelType Q = category("CustomLabel",
       value(1, "Positive"),
       value(0, "No score"),
@@ -64,7 +49,7 @@
     ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
     AccountGroup.UUID anonymousUsers =
         SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
-    allow(cfg, Permission.forLabel(Q.getName()), -1, 1, anonymousUsers,
+    Util.allow(cfg, Permission.forLabel(Q.getName()), -1, 1, anonymousUsers,
         "refs/heads/*");
     saveProjectConfig(cfg);
   }
@@ -77,9 +62,9 @@
     revision(r).review(new ReviewInput().label(Q.getName(), -1));
     ChangeInfo c = get(r.getChangeId());
     LabelInfo q = c.labels.get(Q.getName());
-    assertEquals(1, q.all.size());
-    assertNotNull(q.rejected);
-    assertNull(q.blocking);
+    assertThat(q.all).hasSize(1);
+    assertThat(q.rejected).isNotNull();
+    assertThat(q.blocking).isNull();
   }
 
   @Test
@@ -90,9 +75,9 @@
     revision(r).review(new ReviewInput().label(Q.getName(), -1));
     ChangeInfo c = get(r.getChangeId());
     LabelInfo q = c.labels.get(Q.getName());
-    assertEquals(1, q.all.size());
-    assertNotNull(q.rejected);
-    assertNull(q.blocking);
+    assertThat(q.all).hasSize(1);
+    assertThat(q.rejected).isNotNull();
+    assertThat(q.blocking).isNull();
   }
 
   @Test
@@ -103,9 +88,9 @@
     revision(r).review(new ReviewInput().label(Q.getName(), -1));
     ChangeInfo c = get(r.getChangeId());
     LabelInfo q = c.labels.get(Q.getName());
-    assertEquals(1, q.all.size());
-    assertNotNull(q.rejected);
-    assertNull(q.blocking);
+    assertThat(q.all).hasSize(1);
+    assertThat(q.rejected).isNotNull();
+    assertThat(q.blocking).isNull();
   }
 
   @Test
@@ -116,10 +101,10 @@
     revision(r).review(new ReviewInput().label(Q.getName(), -1));
     ChangeInfo c = get(r.getChangeId());
     LabelInfo q = c.labels.get(Q.getName());
-    assertEquals(1, q.all.size());
-    assertNull(q.disliked);
-    assertNotNull(q.rejected);
-    assertTrue(q.blocking);
+    assertThat(q.all).hasSize(1);
+    assertThat(q.disliked).isNull();
+    assertThat(q.rejected).isNotNull();
+    assertThat(q.blocking).isTrue();
   }
 
   @Test
@@ -129,10 +114,10 @@
     revision(r).review(new ReviewInput().label(Q.getName(), -1));
     ChangeInfo c = get(r.getChangeId());
     LabelInfo q = c.labels.get(Q.getName());
-    assertEquals(1, q.all.size());
-    assertNull(q.disliked);
-    assertNotNull(q.rejected);
-    assertTrue(q.blocking);
+    assertThat(q.all).hasSize(1);
+    assertThat(q.disliked).isNull();
+    assertThat(q.rejected).isNotNull();
+    assertThat(q.blocking).isTrue();
   }
 
   private void saveLabelConfig() throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
index bc311fd..efb4615 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.acceptance.server.project;
 
 import static com.google.common.base.Preconditions.checkNotNull;
-import static org.junit.Assert.assertEquals;
+import static com.google.common.truth.Truth.assertThat;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -25,13 +25,10 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.testutil.ConfigSuite;
-import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
@@ -42,40 +39,25 @@
 public class LabelTypeIT extends AbstractDaemonTest {
   @ConfigSuite.Config
   public static Config noteDbEnabled() {
-    Config cfg = new Config();
-    cfg.setBoolean("notedb", null, "write", true);
-    cfg.setBoolean("notedb", "patchSetApprovals", "read", true);
-    return cfg;
+    return NotesMigration.allEnabledConfig();
   }
 
-  @Inject
-  private GitRepositoryManager repoManager;
-
-  @Inject
-  private ProjectCache projectCache;
-
-  @Inject
-  private AllProjectsName allProjects;
-
-  @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
-
   private LabelType codeReview;
 
   @Before
   public void setUp() throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
     codeReview = checkNotNull(cfg.getLabelSections().get("Code-Review"));
-    codeReview.setCopyMinScore(false);
-    codeReview.setCopyMaxScore(false);
-    codeReview.setCopyAllScoresOnTrivialRebase(false);
-    codeReview.setCopyAllScoresIfNoCodeChange(false);
     codeReview.setDefaultValue((short)-1);
     saveProjectConfig(cfg);
   }
 
   @Test
   public void noCopyMinScoreOnRework() throws Exception {
+    //allProjects only has it true by default
+    codeReview.setCopyMinScore(false);
+    saveLabelConfig();
+
     PushOneCommit.Result r = createChange();
     revision(r).review(ReviewInput.reject());
     assertApproval(r, -2);
@@ -89,7 +71,7 @@
     saveLabelConfig();
     PushOneCommit.Result r = createChange();
     revision(r).review(ReviewInput.reject());
-    //assertApproval(r, -2);
+    assertApproval(r, -2);
     r = amendChange(r.getChangeId());
     assertApproval(r, -2);
   }
@@ -141,6 +123,22 @@
   }
 
   @Test
+  public void noCopyAllScoresIfNoChange() throws Exception {
+    codeReview.setCopyAllScoresIfNoChange(false);
+    saveLabelConfig();
+    PushOneCommit.Result patchSet = readyPatchSetForNoChangeRebase();
+    rebase(patchSet);
+    assertApproval(patchSet, 0);
+  }
+
+  @Test
+  public void copyAllScoresIfNoChange() throws Exception {
+    PushOneCommit.Result patchSet = readyPatchSetForNoChangeRebase();
+    rebase(patchSet);
+    assertApproval(patchSet, 1);
+  }
+
+  @Test
   public void noCopyAllScoresIfNoCodeChange() throws Exception {
     String file = "a.txt";
     String contents = "contents";
@@ -285,6 +283,34 @@
             .get());
   }
 
+  private PushOneCommit.Result readyPatchSetForNoChangeRebase()
+      throws Exception {
+    String file = "a.txt";
+    String contents = "contents";
+
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(),
+        PushOneCommit.SUBJECT, file, contents);
+    PushOneCommit.Result base = push.to(git, "refs/for/master");
+    merge(base);
+
+    push = pushFactory.create(db, admin.getIdent(),
+        PushOneCommit.SUBJECT, file, contents + "M");
+    PushOneCommit.Result basePlusM = push.to(git, "refs/for/master");
+    merge(basePlusM);
+
+    push = pushFactory.create(db, admin.getIdent(),
+        PushOneCommit.SUBJECT, file, contents);
+    PushOneCommit.Result basePlusMMinusM = push.to(git, "refs/for/master");
+    merge(basePlusMMinusM);
+
+    git.checkout().setName(base.getCommit().name()).call();
+    push = pushFactory.create(db, admin.getIdent(),
+        PushOneCommit.SUBJECT, file, contents + "MM");
+    PushOneCommit.Result patchSet = push.to(git, "refs/for/master");
+    revision(patchSet).review(ReviewInput.recommend());
+    return patchSet;
+  }
+
   private void saveLabelConfig() throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
     cfg.getLabelSections().clear();
@@ -306,8 +332,8 @@
     revision(r).submit();
     Repository repo = repoManager.openRepository(project);
     try {
-      assertEquals(r.getCommitId(),
-          repo.getRef("refs/heads/master").getObjectId());
+      assertThat(repo.getRef("refs/heads/master").getObjectId()).isEqualTo(
+          r.getCommitId());
     } finally {
       repo.close();
     }
@@ -327,9 +353,9 @@
 
   private void doAssertApproval(int expected, ChangeInfo c) {
     LabelInfo cr = c.labels.get("Code-Review");
-    assertEquals(-1, (int) cr.defaultValue);
-    assertEquals(1, cr.all.size());
-    assertEquals("Administrator", cr.all.get(0).name);
-    assertEquals(expected, cr.all.get(0).value.intValue());
+    assertThat((int) cr.defaultValue).isEqualTo(-1);
+    assertThat(cr.all).hasSize(1);
+    assertThat(cr.all.get(0).name).isEqualTo("Administrator");
+    assertThat(cr.all.get(0).value.intValue()).isEqualTo(expected);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK
index 74b26ba6..2ea5dec 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK
@@ -2,6 +2,5 @@
 
 acceptance_tests(
   srcs = glob(['*IT.java']),
-  deps = ['//gerrit-acceptance-tests:lib'],
   labels = ['ssh'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BanCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BanCommitIT.java
index bfce523..e483716 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BanCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BanCommitIT.java
@@ -14,39 +14,38 @@
 
 package com.google.gerrit.acceptance.ssh;
 
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.acceptance.GitUtil.add;
 import static com.google.gerrit.acceptance.GitUtil.createCommit;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil.Commit;
+import com.google.gerrit.acceptance.NoHttpd;
 
-import com.jcraft.jsch.JSchException;
-
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.transport.PushResult;
 import org.junit.Test;
 
-import java.io.IOException;
 import java.util.Locale;
 
+@NoHttpd
 public class BanCommitIT extends AbstractDaemonTest {
 
   @Test
-  public void banCommit() throws IOException, GitAPIException, JSchException {
+  public void banCommit() throws Exception {
     add(git, "a.txt", "some content");
     Commit c = createCommit(git, admin.getIdent(), "subject");
 
     String response =
         sshSession.exec("gerrit ban-commit " + project.get() + " "
             + c.getCommit().getName());
-    assertFalse(sshSession.hasError());
-    assertFalse(response, response.toLowerCase(Locale.US).contains("error"));
+    assert_().withFailureMessage(sshSession.getError())
+        .that(sshSession.hasError()).isFalse();
+    assertThat(response.toLowerCase(Locale.US)).doesNotContain("error");
 
     PushResult pushResult = pushHead(git, "refs/heads/master", false);
-    assertTrue(pushResult.getRemoteUpdate("refs/heads/master").getMessage()
-        .startsWith("contains banned commit"));
+    assertThat(pushResult.getRemoteUpdate("refs/heads/master").getMessage())
+        .startsWith("contains banned commit");
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
index 9f859dc7..2bdd894 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
@@ -14,38 +14,31 @@
 
 package com.google.gerrit.acceptance.ssh;
 
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.acceptance.GitUtil.createProject;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GcAssert;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.common.data.GarbageCollectionResult;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GarbageCollection;
 import com.google.gerrit.server.git.GarbageCollectionQueue;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
-import com.jcraft.jsch.JSchException;
-
 import org.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
 import java.util.Arrays;
 import java.util.Locale;
 
+@NoHttpd
 public class GarbageCollectionIT extends AbstractDaemonTest {
 
   @Inject
-  private AllProjectsName allProjects;
-
-  @Inject
   private GarbageCollection.Factory garbageCollectionFactory;
 
   @Inject
@@ -54,15 +47,11 @@
   @Inject
   private GcAssert gcAssert;
 
-  private Project.NameKey project1;
   private Project.NameKey project2;
   private Project.NameKey project3;
 
   @Before
   public void setUp() throws Exception {
-    project1 = new Project.NameKey("p1");
-    createProject(sshSession, project1.get());
-
     project2 = new Project.NameKey("p2");
     createProject(sshSession, project2.get());
 
@@ -72,28 +61,29 @@
 
   @Test
   @UseLocalDisk
-  public void testGc() throws JSchException, IOException {
+  public void testGc() throws Exception {
     String response =
-        sshSession.exec("gerrit gc \"" + project1.get() + "\" \""
+        sshSession.exec("gerrit gc \"" + project.get() + "\" \""
             + project2.get() + "\"");
-    assertFalse(sshSession.hasError());
+    assert_().withFailureMessage(sshSession.getError())
+        .that(sshSession.hasError()).isFalse();
     assertNoError(response);
-    gcAssert.assertHasPackFile(project1, project2);
+    gcAssert.assertHasPackFile(project, project2);
     gcAssert.assertHasNoPackFile(allProjects, project3);
   }
 
   @Test
   @UseLocalDisk
-  public void testGcAll() throws JSchException, IOException {
+  public void testGcAll() throws Exception {
     String response = sshSession.exec("gerrit gc --all");
-    assertFalse(sshSession.hasError());
+    assert_().withFailureMessage(sshSession.getError())
+        .that(sshSession.hasError()).isFalse();
     assertNoError(response);
-    gcAssert.assertHasPackFile(allProjects, project1, project2, project3);
+    gcAssert.assertHasPackFile(allProjects, project, project2, project3);
   }
 
   @Test
-  public void testGcWithoutCapability_Error() throws IOException, OrmException,
-      JSchException {
+  public void testGcWithoutCapability_Error() throws Exception {
     SshSession s = new SshSession(server, user);
     s.exec("gerrit gc --all");
     assertError("Capability runGC is required to access this resource", s.getError());
@@ -102,22 +92,23 @@
 
   @Test
   @UseLocalDisk
-  public void testGcAlreadyScheduled() {
-    gcQueue.addAll(Arrays.asList(project1));
+  public void testGcAlreadyScheduled() throws Exception {
+    gcQueue.addAll(Arrays.asList(project));
     GarbageCollectionResult result = garbageCollectionFactory.create().run(
-        Arrays.asList(allProjects, project1, project2, project3));
-    assertTrue(result.hasErrors());
-    assertEquals(1, result.getErrors().size());
+        Arrays.asList(allProjects, project, project2, project3));
+    assertThat(result.hasErrors()).isTrue();
+    assertThat(result.getErrors().size()).isEqualTo(1);
     GarbageCollectionResult.Error error = result.getErrors().get(0);
-    assertEquals(GarbageCollectionResult.Error.Type.GC_ALREADY_SCHEDULED, error.getType());
-    assertEquals(project1, error.getProjectName());
+    assertThat(error.getType()).isEqualTo(
+        GarbageCollectionResult.Error.Type.GC_ALREADY_SCHEDULED);
+    assertThat(error.getProjectName()).isEqualTo(project);
   }
 
   private void assertError(String expectedError, String response) {
-    assertTrue(response, response.contains(expectedError));
+    assertThat(response).contains(expectedError);
   }
 
   private void assertNoError(String response) {
-    assertFalse(response, response.toLowerCase(Locale.US).contains("error"));
+    assertThat(response.toLowerCase(Locale.US)).doesNotContain("error");
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/JschVerifyFalseBugIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/JschVerifyFalseBugIT.java
index 9bbc125..c9e0a89 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/JschVerifyFalseBugIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/JschVerifyFalseBugIT.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.acceptance.ssh;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.cloneProject;
 import static com.google.gerrit.acceptance.GitUtil.createProject;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 
-import org.junit.Assert;
 import org.junit.Ignore;
 import org.junit.Test;
 
@@ -62,6 +62,6 @@
     for (Future<Void> future : futures) {
       future.get();
     }
-    Assert.assertEquals(threads, futures.size());
+    assertThat(futures.size()).isEqualTo(threads);
   }
 }
diff --git a/gerrit-antlr/src/main/antlr3/com/google/gerrit/server/query/Query.g b/gerrit-antlr/src/main/antlr3/com/google/gerrit/server/query/Query.g
index 423acb4..98f1af9 100644
--- a/gerrit-antlr/src/main/antlr3/com/google/gerrit/server/query/Query.g
+++ b/gerrit-antlr/src/main/antlr3/com/google/gerrit/server/query/Query.g
@@ -168,7 +168,7 @@
   :  ( '\u0000'..' '
      | '!'
      | '"'
-     | '#'
+     // '#' permit
      | '$'
      | '%'
      | '&'
@@ -188,6 +188,6 @@
      | '?'
      | '[' | ']'
      | '{' | '}'
-     | '~'
+     // | '~' permit
      )
   ;
diff --git a/gerrit-cache-h2/BUCK b/gerrit-cache-h2/BUCK
index d3e8994..c7a2221 100644
--- a/gerrit-cache-h2/BUCK
+++ b/gerrit-cache-h2/BUCK
@@ -2,6 +2,7 @@
   name = 'cache-h2',
   srcs = glob(['src/main/java/**/*.java']),
   deps = [
+    '//gerrit-common:server',
     '//gerrit-extension-api:api',
     '//gerrit-server:server',
     '//lib:guava',
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index 65bb034..5870f91 100644
--- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -152,7 +152,7 @@
     }
   }
 
-  @SuppressWarnings({"unchecked", "cast"})
+  @SuppressWarnings({"unchecked"})
   @Override
   public <K, V> Cache<K, V> build(CacheBinding<K, V> def) {
     long limit = config.getLong("cache", def.name(), "diskLimit", 128 << 20);
@@ -163,7 +163,7 @@
 
     SqlStore<K, V> store = newSqlStore(def.name(), def.keyType(), limit,
         def.expireAfterWrite(TimeUnit.SECONDS));
-    H2CacheImpl<K, V> cache = new H2CacheImpl<K, V>(
+    H2CacheImpl<K, V> cache = new H2CacheImpl<>(
         executor, store, def.keyType(),
         (Cache<K, ValueHolder<V>>) defaultFactory.create(def, true).build());
     synchronized (caches) {
@@ -187,9 +187,9 @@
         def.expireAfterWrite(TimeUnit.SECONDS));
     Cache<K, ValueHolder<V>> mem = (Cache<K, ValueHolder<V>>)
         defaultFactory.create(def, true)
-        .build((CacheLoader<K, V>) new H2CacheImpl.Loader<K, V>(
+        .build((CacheLoader<K, V>) new H2CacheImpl.Loader<>(
               executor, store, loader));
-    H2CacheImpl<K, V> cache = new H2CacheImpl<K, V>(
+    H2CacheImpl<K, V> cache = new H2CacheImpl<>(
         executor, store, def.keyType(), mem);
     caches.add(cache);
     return cache;
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 5563988..1aaebf5 100644
--- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -11,8 +11,8 @@
 import com.google.common.hash.Funnel;
 import com.google.common.hash.Funnels;
 import com.google.common.hash.PrimitiveSink;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.server.cache.PersistentCache;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.inject.TypeLiteral;
 
 import org.h2.jdbc.JdbcSQLException;
@@ -363,21 +363,15 @@
       SqlHandle c = null;
       try {
         c = acquire();
-        Statement s = c.conn.createStatement();
-        try {
-          ResultSet r;
+        try (Statement s = c.conn.createStatement()) {
           if (estimatedSize <= 0) {
-            r = s.executeQuery("SELECT COUNT(*) FROM data");
-            try {
+            try (ResultSet r = s.executeQuery("SELECT COUNT(*) FROM data")) {
               estimatedSize = r.next() ? r.getInt(1) : 0;
-            } finally {
-              r.close();
             }
           }
 
           BloomFilter<K> b = newBloomFilter();
-          r = s.executeQuery("SELECT k FROM data");
-          try {
+          try (ResultSet r = s.executeQuery("SELECT k FROM data")) {
             while (r.next()) {
               b.put(keyType.get(r, 1));
             }
@@ -390,12 +384,8 @@
             } else {
               throw e;
             }
-          } finally {
-            r.close();
           }
           return b;
-        } finally {
-          s.close();
         }
       } catch (SQLException e) {
         log.warn("Cannot build BloomFilter for " + url, e);
@@ -414,8 +404,7 @@
           c.get = c.conn.prepareStatement("SELECT v, created FROM data WHERE k=?");
         }
         keyType.set(c.get, 1, key);
-        ResultSet r = c.get.executeQuery();
-        try {
+        try (ResultSet r = c.get.executeQuery()) {
           if (!r.next()) {
             missCount.incrementAndGet();
             return null;
@@ -436,7 +425,6 @@
           touch(c, key);
           return h;
         } finally {
-          r.close();
           c.get.clearParameters();
         }
       } catch (SQLException e) {
@@ -533,11 +521,8 @@
       SqlHandle c = null;
       try {
         c = acquire();
-        Statement s = c.conn.createStatement();
-        try {
+        try (Statement s = c.conn.createStatement()) {
           s.executeUpdate("DELETE FROM data");
-        } finally {
-          s.close();
         }
         bloomFilter = newBloomFilter();
       } catch (SQLException e) {
@@ -552,28 +537,23 @@
       SqlHandle c = null;
       try {
         c = acquire();
-        Statement s = c.conn.createStatement();
-        try {
+        try (Statement s = c.conn.createStatement()) {
           long used = 0;
-          ResultSet r = s.executeQuery("SELECT"
+          try (ResultSet r = s.executeQuery("SELECT"
               + " SUM(OCTET_LENGTH(k) + OCTET_LENGTH(v))"
-              + " FROM data");
-          try {
+              + " FROM data")) {
             used = r.next() ? r.getLong(1) : 0;
-          } finally {
-            r.close();
           }
           if (used <= maxSize) {
             return;
           }
 
-          r = s.executeQuery("SELECT"
+          try (ResultSet r = s.executeQuery("SELECT"
               + " k"
               + ",OCTET_LENGTH(k) + OCTET_LENGTH(v)"
               + ",created"
               + " FROM data"
-              + " ORDER BY accessed");
-          try {
+              + " ORDER BY accessed")) {
             while (maxSize < used && r.next()) {
               K key = keyType.get(r, 1);
               Timestamp created = r.getTimestamp(3);
@@ -584,11 +564,7 @@
                 used -= r.getLong(2);
               }
             }
-          } finally {
-            r.close();
           }
-        } finally {
-          s.close();
         }
       } catch (SQLException e) {
         log.warn("Cannot prune cache " + url, e);
@@ -604,22 +580,15 @@
       SqlHandle c = null;
       try {
         c = acquire();
-        Statement s = c.conn.createStatement();
-        try {
-          ResultSet r = s.executeQuery("SELECT"
-              + " COUNT(*)"
-              + ",SUM(OCTET_LENGTH(k) + OCTET_LENGTH(v))"
-              + " FROM data");
-          try {
-            if (r.next()) {
-              size = r.getLong(1);
-              space = r.getLong(2);
-            }
-          } finally {
-            r.close();
+        try (Statement s = c.conn.createStatement();
+            ResultSet r = s.executeQuery("SELECT"
+                + " COUNT(*)"
+                + ",SUM(OCTET_LENGTH(k) + OCTET_LENGTH(v))"
+                + " FROM data")) {
+          if (r.next()) {
+            size = r.getLong(1);
+            space = r.getLong(2);
           }
-        } finally {
-          s.close();
         }
       } catch (SQLException e) {
         log.warn("Cannot get DiskStats for " + url, e);
@@ -665,16 +634,13 @@
     SqlHandle(String url, KeyType<?> type) throws SQLException {
       this.url = url;
       this.conn = org.h2.Driver.load().connect(url, null);
-      Statement stmt = conn.createStatement();
-      try {
+      try (Statement stmt = conn.createStatement()) {
         stmt.execute("CREATE TABLE IF NOT EXISTS data"
           + "(k " + type.columnType() + " NOT NULL PRIMARY KEY HASH"
           + ",v OTHER NOT NULL"
           + ",created TIMESTAMP NOT NULL"
           + ",accessed TIMESTAMP NOT NULL"
           + ")");
-      } finally {
-        stmt.close();
       }
     }
 
diff --git a/gerrit-common/BUCK b/gerrit-common/BUCK
index 3ba22c5..88e503e 100644
--- a/gerrit-common/BUCK
+++ b/gerrit-common/BUCK
@@ -7,8 +7,11 @@
 ]
 
 EXCLUDES = [
+  SRC + 'common/SiteLibraryLoaderUtil.java',
   SRC + 'common/PluginData.java',
   SRC + 'common/FileUtil.java',
+  SRC + 'common/IoUtil.java',
+  SRC + 'common/TimeUtil.java',
 ]
 
 java_library(
@@ -47,13 +50,17 @@
     '//lib:gwtorm',
     '//lib:guava',
     '//lib/jgit:jgit',
+    '//lib/joda:joda-time',
   ],
   visibility = ['PUBLIC'],
 )
 
+TEST = 'src/test/java/com/google/gerrit/common/'
+AUTO_VALUE_TEST_SRCS = [TEST + 'AutoValueTest.java']
+
 java_test(
   name = 'client_tests',
-  srcs = glob(['src/test/java/**/*.java']),
+  srcs = glob(['src/test/java/**/*.java'], excludes = AUTO_VALUE_TEST_SRCS),
   deps = [
     ':client',
     '//lib:guava',
@@ -61,3 +68,14 @@
   ],
   source_under_test = [':client'],
 )
+
+java_test(
+  name = 'auto_value_tests',
+  srcs = AUTO_VALUE_TEST_SRCS,
+  deps = [
+    '//lib:guava',
+    '//lib:junit',
+    '//lib:truth',
+    '//lib/auto:auto-value',
+  ],
+)
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
similarity index 97%
rename from gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java
rename to gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
index d9a64fc..c45d9f9 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.util;
+package com.google.gerrit.common;
 
 import com.google.common.collect.Sets;
 
@@ -46,6 +46,7 @@
           try {
             src.close();
           } catch (IOException e2) {
+            // Ignore
           }
         }
       }
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 c0382da..b804607 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
@@ -50,6 +50,10 @@
     return toChange(c.getId());
   }
 
+  public static String toChangeInEditMode(Change.Id c) {
+    return "/c/" + c + ",edit/";
+  }
+
   public static String toChange(final Change.Id c) {
     return "/c/" + c + "/";
   }
@@ -68,7 +72,7 @@
   }
 
   public static String toChange(final PatchSet.Id ps) {
-    return "/c/" + ps.getParentKey() + "/" + ps.get();
+    return "/c/" + ps.getParentKey() + "/" + ps.getId();
   }
 
   public static String toProject(final Project.NameKey p) {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java b/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java
index fc5bb56..ffdae9d 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PluginData.java
@@ -14,9 +14,8 @@
 
 package com.google.gerrit.common;
 
-import com.google.common.base.Objects;
-
 import java.io.File;
+import java.util.Objects;
 
 public class PluginData {
   public final String name;
@@ -33,14 +32,14 @@
   public boolean equals(Object obj) {
     if (obj instanceof PluginData) {
       PluginData o = (PluginData) obj;
-      return Objects.equal(name, o.name) && Objects.equal(version, o.version)
-          && Objects.equal(pluginFile, o.pluginFile);
+      return Objects.equals(name, o.name) && Objects.equals(version, o.version)
+          && Objects.equals(pluginFile, o.pluginFile);
     }
     return super.equals(obj);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hashCode(name, version, pluginFile);
+    return Objects.hash(name, version, pluginFile);
   }
 }
\ No newline at end of file
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
new file mode 100644
index 0000000..a98e0a5
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.util.Arrays;
+import java.util.Comparator;
+
+public final class SiteLibraryLoaderUtil {
+
+  public static void loadSiteLib(File libdir) {
+    File[] jars = listJars(libdir);
+    if (jars != null && 0 < jars.length) {
+      Arrays.sort(jars, new Comparator<File>() {
+        @Override
+        public int compare(File a, File b) {
+          // Sort by reverse last-modified time so newer JARs are first.
+          int cmp = Long.compare(b.lastModified(), a.lastModified());
+          if (cmp != 0) {
+            return cmp;
+          }
+          return a.getName().compareTo(b.getName());
+        }
+      });
+      IoUtil.loadJARs(jars);
+    }
+  }
+
+  public static File[] listJars(File libdir) {
+    File[] jars = libdir.listFiles(new FileFilter() {
+      @Override
+      public boolean accept(File path) {
+        String name = path.getName();
+        return (name.endsWith(".jar") || name.endsWith(".zip"))
+            && path.isFile();
+      }
+    });
+    return jars;
+  }
+
+  private SiteLibraryLoaderUtil() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/TimeUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/TimeUtil.java
similarity index 82%
rename from gerrit-server/src/main/java/com/google/gerrit/server/util/TimeUtil.java
rename to gerrit-common/src/main/java/com/google/gerrit/common/TimeUtil.java
index 6bc261f..4274b5a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/TimeUtil.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/TimeUtil.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.util;
+package com.google.gerrit.common;
 
 import org.joda.time.DateTimeUtils;
 
@@ -28,9 +28,8 @@
     return new Timestamp(nowMs());
   }
 
-  public static Timestamp roundTimestampToSecond(Timestamp t) {
-    long milliseconds = (t.getTime()/1000) * 1000;
-    return new Timestamp(milliseconds);
+  public static Timestamp roundToSecond(Timestamp t) {
+    return new Timestamp((t.getTime() / 1000) * 1000);
   }
 
   private TimeUtil() {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/changes/Side.java b/gerrit-common/src/main/java/com/google/gerrit/common/changes/Side.java
deleted file mode 100644
index 4a9ddf8..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/changes/Side.java
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.changes;
-
-/** The side on which a comment was added. */
-public enum Side {
-  PARENT, REVISION
-}
\ No newline at end of file
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
index 9a222ed..f21f894 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
@@ -124,7 +124,7 @@
     if (!super.equals(obj) || !(obj instanceof AccessSection)) {
       return false;
     }
-    return new HashSet<Permission>(getPermissions()).equals(new HashSet<>(
+    return new HashSet<>(getPermissions()).equals(new HashSet<>(
         ((AccessSection) obj).getPermissions()));
   }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
index 5a7559d..54a573d 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
@@ -23,8 +23,8 @@
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
 import com.google.gwtjsonrpc.common.RpcImpl;
-import com.google.gwtjsonrpc.common.VoidResult;
 import com.google.gwtjsonrpc.common.RpcImpl.Version;
+import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.List;
 import java.util.Set;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalDetail.java
deleted file mode 100644
index f09241d..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalDetail.java
+++ /dev/null
@@ -1,126 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-public class ApprovalDetail {
-  public static List<ApprovalDetail> sort(Collection<ApprovalDetail> ads,
-      final int owner) {
-    List<ApprovalDetail> sorted = new ArrayList<>(ads);
-    Collections.sort(sorted, new Comparator<ApprovalDetail>() {
-      public int compare(ApprovalDetail o1, ApprovalDetail o2) {
-        int byOwner = (o2.account.get() == owner ? 1 : 0)
-            - (o1.account.get() == owner ? 1 : 0);
-        return byOwner != 0 ? byOwner : (o1.hasNonZero - o2.hasNonZero);
-      }
-    });
-    return sorted;
-  }
-
-  protected Account.Id account;
-  protected List<PatchSetApproval> approvals;
-  protected boolean canRemove;
-  private Set<String> votable;
-
-  private transient Set<String> approved;
-  private transient Set<String> rejected;
-  private transient Map<String, Integer> values;
-  private transient int hasNonZero;
-
-  protected ApprovalDetail() {
-  }
-
-  public ApprovalDetail(final Account.Id id) {
-    account = id;
-    approvals = new ArrayList<>();
-  }
-
-  public Account.Id getAccount() {
-    return account;
-  }
-
-  public boolean canRemove() {
-    return canRemove;
-  }
-
-  public void setCanRemove(boolean removeable) {
-    canRemove = removeable;
-  }
-
-  public void approved(String label) {
-    if (approved == null) {
-      approved = new HashSet<>();
-    }
-    approved.add(label);
-    hasNonZero = 1;
-  }
-
-  public void rejected(String label) {
-    if (rejected == null) {
-      rejected = new HashSet<>();
-    }
-    rejected.add(label);
-    hasNonZero = 1;
-  }
-
-  public void votable(String label) {
-    if (votable == null) {
-      votable = new HashSet<>();
-    }
-    votable.add(label);
-  }
-
-  public void value(String label, int value) {
-    if (values == null) {
-      values = new HashMap<>();
-    }
-    values.put(label, value);
-    if (value != 0) {
-      hasNonZero = 1;
-    }
-  }
-
-  public boolean isApproved(String label) {
-    return approved != null && approved.contains(label);
-  }
-
-  public boolean isRejected(String label) {
-    return rejected != null && rejected.contains(label);
-  }
-
-  public boolean canVote(String label) {
-    return votable != null && votable.contains(label);
-  }
-
-  public int getValue(String label) {
-    if (values == null) {
-      return 0;
-    }
-    Integer v = values.get(label);
-    return v != null ? v : 0;
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
deleted file mode 100644
index e067f06..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
+++ /dev/null
@@ -1,268 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import com.google.gerrit.extensions.common.SubmitType;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-
-import java.util.List;
-import java.util.Set;
-
-/** Detail necessary to display a change. */
-public class ChangeDetail {
-  protected AccountInfoCache accounts;
-  protected boolean allowsAnonymous;
-  protected boolean canAbandon;
-  protected boolean canEditCommitMessage;
-  protected boolean canCherryPick;
-  protected boolean canPublish;
-  protected boolean canRebase;
-  protected boolean canRestore;
-  protected boolean canRevert;
-  protected boolean canDeleteDraft;
-  protected Change change;
-  protected boolean starred;
-  protected List<ChangeInfo> dependsOn;
-  protected List<ChangeInfo> neededBy;
-  protected List<PatchSet> patchSets;
-  protected Set<PatchSet.Id> patchSetsWithDraftComments;
-  protected List<SubmitRecord> submitRecords;
-  protected SubmitType submitType;
-  protected SubmitTypeRecord submitTypeRecord;
-  protected boolean canSubmit;
-  protected List<ChangeMessage> messages;
-  protected PatchSet.Id currentPatchSetId;
-  protected PatchSetDetail currentDetail;
-  protected boolean canEdit;
-  protected boolean canEditTopicName;
-
-  public ChangeDetail() {
-  }
-
-  public AccountInfoCache getAccounts() {
-    return accounts;
-  }
-
-  public void setAccounts(AccountInfoCache aic) {
-    accounts = aic;
-  }
-
-  public boolean isAllowsAnonymous() {
-    return allowsAnonymous;
-  }
-
-  public void setAllowsAnonymous(final boolean anon) {
-    allowsAnonymous = anon;
-  }
-
-  public boolean canAbandon() {
-    return canAbandon;
-  }
-
-  public void setCanAbandon(final boolean a) {
-    canAbandon = a;
-  }
-
-  public boolean canEditCommitMessage() {
-    return canEditCommitMessage;
-  }
-
-  public void setCanEditCommitMessage(final boolean a) {
-    canEditCommitMessage = a;
-  }
-
-  public boolean canCherryPick() {
-    return canCherryPick;
-  }
-
-  public void setCanCherryPick(final boolean a) {
-    canCherryPick = a;
-  }
-
-  public boolean canPublish() {
-    return canPublish;
-  }
-
-  public void setCanPublish(final boolean a) {
-    canPublish = a;
-  }
-
-  public boolean canRebase() {
-    return canRebase;
-  }
-
-  public void setCanRebase(final boolean a) {
-    canRebase = a;
-  }
-
-  public boolean canRestore() {
-    return canRestore;
-  }
-
-  public void setCanRestore(final boolean a) {
-    canRestore = a;
-  }
-
-  public boolean canRevert() {
-    return canRevert;
-  }
-
-  public void setCanRevert(boolean a) {
-      canRevert = a;
-  }
-
-  public boolean canSubmit() {
-    return canSubmit;
-  }
-
-  public void setCanSubmit(boolean a) {
-    canSubmit = a;
-  }
-
-  public boolean canDeleteDraft() {
-    return canDeleteDraft;
-  }
-
-  public void setCanDeleteDraft(boolean a) {
-    canDeleteDraft = a;
-  }
-
-  public boolean canEditTopicName() {
-    return canEditTopicName;
-  }
-
-  public void setCanEditTopicName(boolean a) {
-    canEditTopicName = a;
-  }
-
-  public Change getChange() {
-    return change;
-  }
-
-  public void setChange(final Change change) {
-    this.change = change;
-    this.currentPatchSetId = change.currentPatchSetId();
-  }
-
-  public boolean isStarred() {
-    return starred;
-  }
-
-  public void setStarred(final boolean s) {
-    starred = s;
-  }
-
-  public List<ChangeInfo> getDependsOn() {
-    return dependsOn;
-  }
-
-  public void setDependsOn(List<ChangeInfo> d) {
-    dependsOn = d;
-  }
-
-  public List<ChangeInfo> getNeededBy() {
-    return neededBy;
-  }
-
-  public void setNeededBy(List<ChangeInfo> d) {
-    neededBy = d;
-  }
-
-  public List<ChangeMessage> getMessages() {
-    return messages;
-  }
-
-  public void setMessages(List<ChangeMessage> m) {
-    messages = m;
-  }
-
-  public List<PatchSet> getPatchSets() {
-    return patchSets;
-  }
-
-  public void setPatchSets(List<PatchSet> s) {
-    patchSets = s;
-  }
-
-  public void setPatchSetsWithDraftComments(Set<PatchSet.Id> pwdc) {
-    this.patchSetsWithDraftComments = pwdc;
-  }
-
-  public boolean hasDraftComments(PatchSet.Id id) {
-    return patchSetsWithDraftComments.contains(id);
-  }
-
-  public void setSubmitRecords(List<SubmitRecord> all) {
-    submitRecords = all;
-  }
-
-  public List<SubmitRecord> getSubmitRecords() {
-    return submitRecords;
-  }
-
-  public void setSubmitTypeRecord(SubmitTypeRecord submitTypeRecord) {
-    this.submitTypeRecord = submitTypeRecord;
-  }
-
-  public SubmitTypeRecord getSubmitTypeRecord() {
-    return submitTypeRecord;
-  }
-
-  public boolean isCurrentPatchSet(final PatchSetDetail detail) {
-    return currentPatchSetId != null
-        && detail.getPatchSet().getId().equals(currentPatchSetId);
-  }
-
-  public PatchSet getCurrentPatchSet() {
-    if (currentPatchSetId != null) {
-      // We search through the list backwards because its *very* likely
-      // that the current patch set is also the last patch set.
-      //
-      for (int i = patchSets.size() - 1; i >= 0; i--) {
-        final PatchSet ps = patchSets.get(i);
-        if (ps.getId().equals(currentPatchSetId)) {
-          return ps;
-        }
-      }
-    }
-    return null;
-  }
-
-  public PatchSetDetail getCurrentPatchSetDetail() {
-    return currentDetail;
-  }
-
-  public void setCurrentPatchSetDetail(PatchSetDetail d) {
-    currentDetail = d;
-  }
-
-  public void setCurrentPatchSetId(final PatchSet.Id id) {
-    currentPatchSetId = id;
-  }
-
-  public String getDescription() {
-    return currentDetail != null ? currentDetail.getInfo().getMessage() : "";
-  }
-
-  public void setCanEdit(boolean a) {
-    canEdit = a;
-  }
-
-  public boolean canEdit() {
-    return canEdit;
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java
index 31fd827..a744122 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java
@@ -31,7 +31,6 @@
   protected String topic;
   protected boolean starred;
   protected Timestamp lastUpdatedOn;
-  protected String sortKey;
   protected PatchSet.Id patchSetId;
   protected boolean latest;
 
@@ -52,7 +51,6 @@
     branch = c.getDest().getShortName();
     topic = c.getTopic();
     lastUpdatedOn = c.getLastUpdatedOn();
-    sortKey = c.getSortKey();
     patchSetId = patchId;
     latest = patchSetId == null || patchSetId.equals(c.currentPatchSetId());
   }
@@ -112,8 +110,4 @@
   public java.sql.Timestamp getLastUpdatedOn() {
     return lastUpdatedOn;
   }
-
-  public String getSortKey() {
-    return sortKey;
-  }
 }
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 66309ea..d911390 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,7 +16,6 @@
 
 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;
@@ -53,10 +52,11 @@
   protected String anonymousCowardName;
   protected int suggestFrom;
   protected int changeUpdateDelay;
-  protected AccountGeneralPreferences.ChangeScreen changeScreen;
   protected List<String> archiveFormats;
   protected int largeChangeSize;
-  protected boolean newFeatures;
+  protected String replyLabel;
+  protected String replyTitle;
+  protected boolean allowDraftChanges;
 
   public String getLoginUrl() {
     return loginUrl;
@@ -277,14 +277,6 @@
     changeUpdateDelay = seconds;
   }
 
-  public AccountGeneralPreferences.ChangeScreen getChangeScreen() {
-    return changeScreen;
-  }
-
-  public void setChangeScreen(AccountGeneralPreferences.ChangeScreen ui) {
-    this.changeScreen = ui;
-  }
-
   public int getLargeChangeSize() {
     return largeChangeSize;
   }
@@ -301,11 +293,27 @@
     archiveFormats = formats;
   }
 
-  public boolean getNewFeatures() {
-    return newFeatures;
+  public String getReplyTitle() {
+    return replyTitle;
   }
 
-  public void setNewFeatures(boolean n) {
-    newFeatures = n;
+  public void setReplyTitle(String r) {
+    replyTitle = r;
+  }
+
+  public String getReplyLabel() {
+    return replyLabel;
+  }
+
+  public void setReplyLabel(String r) {
+    replyLabel = r;
+  }
+
+  public boolean isAllowDraftChanges() {
+    return allowDraftChanges;
+  }
+
+  public void setAllowDraftChanges(boolean b) {
+    allowDraftChanges = b;
   }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
index f055136..d50e754 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -35,6 +35,16 @@
    */
   public static final String ADMINISTRATE_SERVER = "administrateServer";
 
+  /** Maximum number of changes that may be pushed in a batch. */
+  public static final String BATCH_CHANGES_LIMIT = "batchChangesLimit";
+
+  /**
+   * Default maximum number of changes that may be pushed in a batch, 0 means no
+   * limit. This is just used as a suggestion for prepopulating the field in the
+   * access UI.
+   */
+  public static final int DEFAULT_MAX_BATCH_CHANGES_LIMIT = 0;
+
   /** Can create any account on the server. */
   public static final String CREATE_ACCOUNT = "createAccount";
 
@@ -58,12 +68,12 @@
   /** Can flush any cache except the active web_sessions cache. */
   public static final String FLUSH_CACHES = "flushCaches";
 
-  /** Can generate HTTP passwords for user other than self. */
-  public static final String GENERATE_HTTP_PASSWORD = "generateHttpPassword";
-
   /** Can terminate any task using the kill command. */
   public static final String KILL_TASK = "killTask";
 
+  /** Can modify any account on the server. */
+  public static final String MODIFY_ACCOUNT = "modifyAccount";
+
   /** Queue a user can access to submit their tasks to. */
   public static final String PRIORITY = "priority";
 
@@ -104,12 +114,14 @@
     NAMES_ALL = new ArrayList<>();
     NAMES_ALL.add(ACCESS_DATABASE);
     NAMES_ALL.add(ADMINISTRATE_SERVER);
+    NAMES_ALL.add(BATCH_CHANGES_LIMIT);
     NAMES_ALL.add(CREATE_ACCOUNT);
     NAMES_ALL.add(CREATE_GROUP);
     NAMES_ALL.add(CREATE_PROJECT);
     NAMES_ALL.add(EMAIL_REVIEWERS);
     NAMES_ALL.add(FLUSH_CACHES);
     NAMES_ALL.add(KILL_TASK);
+    NAMES_ALL.add(MODIFY_ACCOUNT);
     NAMES_ALL.add(PRIORITY);
     NAMES_ALL.add(QUERY_LIMIT);
     NAMES_ALL.add(RUN_AS);
@@ -139,7 +151,8 @@
 
   /** @return true if the capability should have a range attached. */
   public static boolean hasRange(String varName) {
-    return QUERY_LIMIT.equalsIgnoreCase(varName);
+    return QUERY_LIMIT.equalsIgnoreCase(varName)
+        || BATCH_CHANGES_LIMIT.equalsIgnoreCase(varName);
   }
 
   /** @return the valid range for the capability if it has one, otherwise null. */
@@ -150,6 +163,12 @@
           0, Integer.MAX_VALUE,
           0, DEFAULT_MAX_QUERY_LIMIT);
     }
+    if (BATCH_CHANGES_LIMIT.equalsIgnoreCase(varName)) {
+      return new PermissionRange.WithDefaults(
+          varName,
+          0, Integer.MAX_VALUE,
+          0, DEFAULT_MAX_BATCH_CHANGES_LIMIT);
+    }
     return null;
   }
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
index 323af23..430c23c 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
@@ -30,6 +30,7 @@
   public Theme theme;
   public List<String> plugins;
   public List<Message> messages;
+  public boolean isNoteDbEnabled;
 
   public static class Theme {
     public String backgroundColor;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
index 5018df9..cf6f756 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -25,6 +25,13 @@
 import java.util.Map;
 
 public class LabelType {
+  public static final boolean DEF_CAN_OVERRIDE = true;
+  public static final boolean DEF_COPY_ALL_SCORES_IF_NO_CHANGE = true;
+  public static final boolean DEF_COPY_ALL_SCORES_IF_NO_CODE_CHANGE = false;
+  public static final boolean DEF_COPY_ALL_SCORES_ON_TRIVIAL_REBASE = false;
+  public static final boolean DEF_COPY_MAX_SCORE = false;
+  public static final boolean DEF_COPY_MIN_SCORE = false;
+
   public static LabelType withDefaultValues(String name) {
     checkName(name);
     List<LabelValue> values = new ArrayList<>(2);
@@ -66,6 +73,7 @@
       return Collections.unmodifiableList(values);
     }
     Collections.sort(values, new Comparator<LabelValue>() {
+      @Override
       public int compare(LabelValue o1, LabelValue o2) {
         return o1.getValue() - o2.getValue();
       }
@@ -93,6 +101,7 @@
   protected boolean copyMaxScore;
   protected boolean copyAllScoresOnTrivialRebase;
   protected boolean copyAllScoresIfNoCodeChange;
+  protected boolean copyAllScoresIfNoChange;
   protected short defaultValue;
 
   protected List<LabelValue> values;
@@ -125,6 +134,12 @@
         maxPositive = values.get(values.size() - 1).getValue();
       }
     }
+    setCanOverride(DEF_CAN_OVERRIDE);
+    setCopyAllScoresIfNoChange(DEF_COPY_ALL_SCORES_IF_NO_CHANGE);
+    setCopyAllScoresIfNoCodeChange(DEF_COPY_ALL_SCORES_IF_NO_CODE_CHANGE);
+    setCopyAllScoresOnTrivialRebase(DEF_COPY_ALL_SCORES_ON_TRIVIAL_REBASE);
+    setCopyMaxScore(DEF_COPY_MAX_SCORE);
+    setCopyMinScore(DEF_COPY_MIN_SCORE);
   }
 
   public String getName() {
@@ -218,6 +233,14 @@
     this.copyAllScoresIfNoCodeChange = copyAllScoresIfNoCodeChange;
   }
 
+  public boolean isCopyAllScoresIfNoChange() {
+    return copyAllScoresIfNoChange;
+  }
+
+  public void setCopyAllScoresIfNoChange(boolean copyAllScoresIfNoChange) {
+    this.copyAllScoresIfNoChange = copyAllScoresIfNoChange;
+  }
+
   public boolean isMaxNegative(PatchSetApproval ca) {
     return maxNegative == ca.getValue();
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelTypes.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelTypes.java
index 18928f2..66f6a8e 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelTypes.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelTypes.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
+import com.google.gerrit.reviewdb.client.LabelId;
 
 import java.util.ArrayList;
 import java.util.Collections;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
index 454324b..4ed296f 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
@@ -145,7 +145,7 @@
     }
   }
 
-  private static abstract class Format {
+  private abstract static class Format {
     abstract void format(StringBuilder b, Map<String, String> p);
   }
 
@@ -200,7 +200,7 @@
     }
   }
 
-  private static abstract class Function {
+  private abstract static class Function {
     abstract String apply(String a);
   }
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
index 914e69f..046df1d 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
@@ -56,6 +56,7 @@
   protected boolean intralineDifference;
   protected boolean intralineFailure;
   protected boolean intralineTimeout;
+  protected boolean binary;
 
   public PatchScript(final Change.Key ck, final ChangeType ct, final String on,
       final String nn, final FileMode om, final FileMode nm,
@@ -64,7 +65,7 @@
       final List<Edit> e, final DisplayMethod ma, final DisplayMethod mb,
       final String mta, final String mtb, final CommentDetail cd,
       final List<Patch> hist, final boolean hf, final boolean id,
-      final boolean idf, final boolean idt) {
+      final boolean idf, final boolean idt, boolean bin) {
     changeId = ck;
     changeType = ct;
     oldName = on;
@@ -86,6 +87,7 @@
     intralineDifference = id;
     intralineFailure = idf;
     intralineTimeout = idt;
+    binary = bin;
   }
 
   protected PatchScript() {
@@ -194,4 +196,8 @@
     }
     return new EditList(edits, ctx, a.size(), b.size()).getHunks();
   }
+
+  public boolean isBinary() {
+    return binary;
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java
deleted file mode 100644
index a9b6335..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
-
-import java.util.List;
-
-public class PatchSetPublishDetail {
-  protected AccountInfoCache accounts;
-  protected PatchSetInfo patchSetInfo;
-  protected Change change;
-  protected List<PatchLineComment> drafts;
-  protected List<SubmitRecord> submitRecords;
-  protected SubmitTypeRecord submitTypeRecord;
-  protected boolean canSubmit;
-
-  public void setSubmitTypeRecord(SubmitTypeRecord submitTypeRecord) {
-    this.submitTypeRecord = submitTypeRecord;
-  }
-
-  public SubmitTypeRecord getSubmitTypeRecord() {
-    return submitTypeRecord;
-  }
-
-  public void setAccounts(AccountInfoCache accounts) {
-    this.accounts = accounts;
-  }
-
-  public void setPatchSetInfo(PatchSetInfo patchSetInfo) {
-    this.patchSetInfo = patchSetInfo;
-  }
-
-  public void setChange(Change change) {
-    this.change = change;
-  }
-
-  public void setDrafts(List<PatchLineComment> drafts) {
-    this.drafts = drafts;
-  }
-
-  public void setCanSubmit(boolean allowed) {
-    canSubmit = allowed;
-  }
-
-  public AccountInfoCache getAccounts() {
-    return accounts;
-  }
-
-  public Change getChange() {
-    return change;
-  }
-
-  public PatchSetInfo getPatchSetInfo() {
-    return patchSetInfo;
-  }
-
-  public List<PatchLineComment> getDrafts() {
-    return drafts;
-  }
-
-  public boolean canSubmit() {
-    return canSubmit;
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
index 2379b4a..a041dc6 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
@@ -24,6 +24,7 @@
   public static final String ABANDON = "abandon";
   public static final String CREATE = "create";
   public static final String DELETE_DRAFTS = "deleteDrafts";
+  public static final String EDIT_HASHTAGS = "editHashtags";
   public static final String EDIT_TOPIC_NAME = "editTopicName";
   public static final String FORGE_AUTHOR = "forgeAuthor";
   public static final String FORGE_COMMITTER = "forgeCommitter";
@@ -68,6 +69,7 @@
     NAMES_LC.add(SUBMIT_AS.toLowerCase());
     NAMES_LC.add(VIEW_DRAFTS.toLowerCase());
     NAMES_LC.add(EDIT_TOPIC_NAME.toLowerCase());
+    NAMES_LC.add(EDIT_HASHTAGS.toLowerCase());
     NAMES_LC.add(DELETE_DRAFTS.toLowerCase());
     NAMES_LC.add(PUBLISH_DRAFTS.toLowerCase());
 
@@ -263,8 +265,7 @@
     if (!name.equals(other.name) || exclusiveGroup != other.exclusiveGroup) {
       return false;
     }
-    return new HashSet<PermissionRule>(getRules())
-        .equals(new HashSet<PermissionRule>(other.getRules()));
+    return new HashSet<>(getRules()).equals(new HashSet<>(other.getRules()));
   }
 
   @Override
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
index bd05baf..3ba7adf 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
@@ -182,10 +182,14 @@
     }
 
     if (canUseRange && (getMin() != 0 || getMax() != 0)) {
-      if (0 <= getMin()) r.append('+');
+      if (0 <= getMin()) {
+        r.append('+');
+      }
       r.append(getMin());
       r.append("..");
-      if (0 <= getMax()) r.append('+');
+      if (0 <= getMax()) {
+        r.append('+');
+      }
       r.append(getMax());
       r.append(' ');
     }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
index f82f434..76785d8 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
@@ -85,7 +85,10 @@
       DEST_BRANCH_NOT_FOUND,
 
       /** Not permitted to edit the topic name */
-      EDIT_TOPIC_NAME_NOT_PERMITTED
+      EDIT_TOPIC_NAME_NOT_PERMITTED,
+
+      /** Not permitted to edit the hashtags */
+      EDIT_HASHTAGS_NOT_PERMITTED
     }
 
     protected Type type;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerInfo.java
deleted file mode 100644
index 28a8340..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerInfo.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-/**
- * Suggested reviewer for a change. Can be a user ({@link AccountInfo}) or a
- * group ({@link GroupReference}).
- */
-public class ReviewerInfo implements Comparable<ReviewerInfo> {
-  private AccountInfo accountInfo;
-  private GroupReference groupReference;
-
-  protected ReviewerInfo() {
-  }
-
-  public ReviewerInfo(final AccountInfo accountInfo) {
-    this.accountInfo = accountInfo;
-  }
-
-  public ReviewerInfo(final GroupReference groupReference) {
-    this.groupReference = groupReference;
-  }
-
-  public AccountInfo getAccountInfo() {
-    return accountInfo;
-  }
-
-  public GroupReference getGroup() {
-    return groupReference;
-  }
-
-  @Override
-  public int compareTo(final ReviewerInfo o) {
-    return getSortValue().compareTo(o.getSortValue());
-  }
-
-  private String getSortValue() {
-    if (accountInfo != null) {
-      if (accountInfo.getPreferredEmail() != null) {
-        return accountInfo.getPreferredEmail();
-      }
-      return accountInfo.getFullName();
-    }
-    return groupReference.getName();
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerResult.java
deleted file mode 100644
index d696137..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerResult.java
+++ /dev/null
@@ -1,117 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Result from adding or removing a reviewer from a change.
- */
-public class ReviewerResult {
-  protected List<Error> errors;
-  protected ChangeDetail change;
-  protected int memberCount;
-  protected boolean askForConfirmation;
-
-  public ReviewerResult() {
-    errors = new ArrayList<>();
-  }
-
-  public void addError(final Error e) {
-    errors.add(e);
-  }
-
-  public List<Error> getErrors() {
-    return errors;
-  }
-
-  public ChangeDetail getChange() {
-    return change;
-  }
-
-  public void setChange(final ChangeDetail d) {
-    change = d;
-  }
-
-  public int getMemberCount() {
-    return memberCount;
-  }
-
-  public void setMemberCount(final int memberCount) {
-    this.memberCount = memberCount;
-  }
-
-  public boolean askForConfirmation() {
-    return askForConfirmation;
-  }
-
-  public void setAskForConfirmation(final boolean askForConfirmation) {
-    this.askForConfirmation = askForConfirmation;
-  }
-
-  public static class Error {
-    public static enum Type {
-      /** Name supplied does not match to a registered account or account group. */
-      REVIEWER_NOT_FOUND,
-
-      /** The account is inactive. */
-      ACCOUNT_INACTIVE,
-
-      /** The account is not permitted to see the change. */
-      CHANGE_NOT_VISIBLE,
-
-      /** The groups has no members. */
-      GROUP_EMPTY,
-
-      /** The groups has too many members. */
-      GROUP_HAS_TOO_MANY_MEMBERS,
-
-      /** The group is not allowed to be added as reviewer. */
-      GROUP_NOT_ALLOWED,
-
-      /** Could not remove this reviewer from the change due to ORMException. */
-      COULD_NOT_REMOVE,
-
-      /** Not permitted to remove this reviewer from the change. */
-      REMOVE_NOT_PERMITTED
-    }
-
-    protected Type type;
-    protected String name;
-
-    protected Error() {
-    }
-
-    public Error(final Type type, final String who) {
-      this.type = type;
-      this.name = who;
-    }
-
-    public Type getType() {
-      return type;
-    }
-
-    public String getName() {
-      return name;
-    }
-
-    @Override
-    public String toString() {
-      return type + " " + name;
-    }
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java
index 7b19f4c..5a79d08 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.SubmitType;
 
 /**
  * Describes the submit type for a change.
@@ -42,6 +42,7 @@
   public SubmitType type;
   public String errorMessage;
 
+  @Override
   public String toString() {
     StringBuilder sb = new StringBuilder();
     sb.append(status);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
index d05dfc2..7b25a23 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
@@ -25,34 +24,6 @@
 
 @RpcImpl(version = Version.V2_0)
 public interface SuggestService extends RemoteJsonService {
-  void suggestAccount(String query, Boolean enabled, int limit,
-      AsyncCallback<List<AccountInfo>> callback);
-
-  /**
-   * @see #suggestAccountGroupForProject(com.google.gerrit.reviewdb.client.Project.NameKey, String, int, AsyncCallback)
-   */
-  @Deprecated
-  void suggestAccountGroup(String query, int limit,
-      AsyncCallback<List<GroupReference>> callback);
-
   void suggestAccountGroupForProject(Project.NameKey project, String query,
       int limit, AsyncCallback<List<GroupReference>> callback);
-
-  /**
-   * @see #suggestChangeReviewer(com.google.gerrit.reviewdb.client.Change.Id, String, int, AsyncCallback)
-   */
-  @Deprecated
-  void suggestReviewer(Project.NameKey project, String query, int limit,
-      AsyncCallback<List<ReviewerInfo>> callback);
-
-  /**
-   * Suggests reviewers. A reviewer can be a user or a group. Inactive users,
-   * the system groups {@code SystemGroupBackend#ANONYMOUS_USERS} and
-   * {@code SystemGroupBackend#REGISTERED_USERS} and groups that have more than
-   * the configured {@code addReviewer.maxAllowed} members are not suggested as
-   * reviewers.
-   * @param changeId the change for which reviewers should be suggested
-   */
-  void suggestChangeReviewer(Change.Id changeId, String query, int limit,
-      AsyncCallback<List<ReviewerInfo>> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
index 4a45350..5e80ac5 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.AllowCrossSiteRequest;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
 import com.google.gwtjsonrpc.common.RpcImpl;
-import com.google.gwtjsonrpc.common.VoidResult;
 import com.google.gwtjsonrpc.common.RpcImpl.Version;
+import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.List;
 
diff --git a/gerrit-common/src/test/java/com/google/gerrit/common/AutoValueTest.java b/gerrit-common/src/test/java/com/google/gerrit/common/AutoValueTest.java
new file mode 100644
index 0000000..5febd80
--- /dev/null
+++ b/gerrit-common/src/test/java/com/google/gerrit/common/AutoValueTest.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.auto.value.AutoValue;
+
+import org.junit.Test;
+
+public class AutoValueTest {
+  @AutoValue
+  abstract static class Auto {
+    static Auto create(String val) {
+      return new AutoValue_AutoValueTest_Auto(val);
+    }
+
+    abstract String val();
+  }
+
+  @Test
+  public void autoValue() {
+    Auto a = Auto.create("foo");
+    assertThat(a.val()).isEqualTo("foo");
+    assertThat(a.toString()).isEqualTo("Auto{val=foo}");
+  }
+}
diff --git a/gerrit-common/src/test/java/com/google/gerrit/common/data/EncodePathSeparatorTest.java b/gerrit-common/src/test/java/com/google/gerrit/common/data/EncodePathSeparatorTest.java
index 7c662ae..816f715 100644
--- a/gerrit-common/src/test/java/com/google/gerrit/common/data/EncodePathSeparatorTest.java
+++ b/gerrit-common/src/test/java/com/google/gerrit/common/data/EncodePathSeparatorTest.java
@@ -14,9 +14,10 @@
 
 package com.google.gerrit.common.data;
 
-import org.junit.Test;
 import static org.junit.Assert.assertEquals;
 
+import org.junit.Test;
+
 public class EncodePathSeparatorTest {
 
   @Test
diff --git a/gerrit-extension-api/BUCK b/gerrit-extension-api/BUCK
index aad79d7..77a6b13 100644
--- a/gerrit-extension-api/BUCK
+++ b/gerrit-extension-api/BUCK
@@ -1,19 +1,22 @@
 SRC = 'src/main/java/com/google/gerrit/extensions/'
 SRCS = glob([SRC + '**/*.java'])
 
+EXT_API_SRCS = glob([SRC + 'client/*.java'])
+
 gwt_module(
   name = 'client',
-  srcs = glob([
-    SRC + 'api/projects/ProjectState.java',
-    SRC + 'common/InheritableBoolean.java',
-    SRC + 'common/ListChangesOption.java',
-    SRC + 'common/SubmitType.java',
-    SRC + 'webui/GerritTopMenu.java',
-  ]),
+  srcs = EXT_API_SRCS,
   gwt_xml = SRC + 'Extensions.gwt.xml',
   visibility = ['PUBLIC'],
 )
 
+java_library(
+  name = 'client-lib',
+  srcs = EXT_API_SRCS,
+  resources = EXT_API_SRCS + glob([SRC + 'Extensions.gwt.xml']),
+  visibility = ['PUBLIC'],
+)
+
 java_binary(
   name = 'extension-api',
   deps = [':lib'],
@@ -47,7 +50,7 @@
 java_doc(
   name = 'extension-api-javadoc',
   title = 'Gerrit Review Extension API Documentation',
-  pkg = 'com.google.gerrit.extensions',
+  pkgs = ['com.google.gerrit.extensions'],
   paths = ['src/main/java'],
   srcs = SRCS,
   deps = [
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
index 557d602..3832334 100644
--- a/gerrit-extension-api/pom.xml
+++ b/gerrit-extension-api/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-extension-api</artifactId>
-  <version>2.10.3</version>
+  <version>2.11</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/Extensions.gwt.xml b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/Extensions.gwt.xml
index 757e046..c857b60 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/Extensions.gwt.xml
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/Extensions.gwt.xml
@@ -14,7 +14,5 @@
  limitations under the License.
 -->
 <module>
-  <source path='api' />
-  <source path='common' />
-  <source path='webui' />
+  <source path='client' />
 </module>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
index 749b12a..71a93d3 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
@@ -18,7 +18,27 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 
 public interface Accounts {
+  /**
+   * Look up an account by ID.
+   * <p>
+   * <strong>Note:</strong> This method eagerly reads the account. Methods that
+   * mutate the account do not necessarily re-read the account. Therefore, calling
+   * a getter method on an instance after calling a mutation method on that same
+   * instance is not guaranteed to reflect the mutation. It is not recommended
+   * to store references to {@code AccountApi} instances.
+   *
+   * @param id any identifier supported by the REST API, including numeric ID,
+   *     email, or username.
+   * @return API for accessing the account.
+   * @throws RestApiException if an error occurred.
+   */
   AccountApi id(String id) throws RestApiException;
+
+  /**
+   * Look up the account of the current in-scope user.
+   *
+   * @see #id(String)
+   */
   AccountApi self() throws RestApiException;
 
   /**
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 3382b76..06f0a75 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -14,18 +14,46 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.common.EditInfo;
+import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
 import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
 
 public interface ChangeApi {
   String id();
 
+  /**
+   * Look up the current revision for the change.
+   * <p>
+   * <strong>Note:</strong> This method eagerly reads the revision. Methods that
+   * mutate the revision do not necessarily re-read the revision. Therefore,
+   * calling a getter method on an instance after calling a mutation method on
+   * that same instance is not guaranteed to reflect the mutation. It is not
+   * recommended to store references to {@code RevisionApi} instances.
+   *
+   * @return API for accessing the revision.
+   * @throws RestApiException if an error occurred.
+   */
   RevisionApi current() throws RestApiException;
+
+  /**
+   * Look up a revision of a change by number.
+   *
+   * @see #current()
+   */
   RevisionApi revision(int id) throws RestApiException;
+
+  /**
+   * Look up a revision of a change by commit SHA-1.
+   *
+   * @see #current()
+   */
   RevisionApi revision(String id) throws RestApiException;
 
   void abandon() throws RestApiException;
@@ -34,18 +62,77 @@
   void restore() throws RestApiException;
   void restore(RestoreInput in) throws RestApiException;
 
+  /**
+   * Create a new change that reverts this change.
+   *
+   * @see Changes#id(int)
+   */
   ChangeApi revert() throws RestApiException;
+
+  /**
+   * Create a new change that reverts this change.
+   *
+   * @see Changes#id(int)
+   */
   ChangeApi revert(RevertInput in) throws RestApiException;
 
+  String topic() throws RestApiException;
+  void topic(String topic) throws RestApiException;
+
   void addReviewer(AddReviewerInput in) throws RestApiException;
   void addReviewer(String in) throws RestApiException;
 
+  SuggestedReviewersRequest suggestReviewers() throws RestApiException;
+  SuggestedReviewersRequest suggestReviewers(String query) throws RestApiException;
+
   ChangeInfo get(EnumSet<ListChangesOption> options) throws RestApiException;
 
-  /** {@code get} with {@link ListChangesOption} set to ALL. */
+  /** {@code get} with {@link ListChangesOption} set to all except CHECK. */
   ChangeInfo get() throws RestApiException;
-  /** {@code get} with {@link ListChangesOption} set to NONE. */
+  /** {@code get} with {@link ListChangesOption} set to none. */
   ChangeInfo info() throws RestApiException;
+  /** Retrieve change edit when exists. */
+  EditInfo getEdit() throws RestApiException;
+
+  /**
+   * Set hashtags on a change
+   **/
+  void setHashtags(HashtagsInput input) throws RestApiException;
+
+  /**
+   * Get hashtags on a change.
+   * @return hashtags
+   * @throws RestApiException
+   */
+  Set<String> getHashtags() throws RestApiException;
+
+  ChangeInfo check() throws RestApiException;
+  ChangeInfo check(FixInput fix) throws RestApiException;
+
+  public abstract class SuggestedReviewersRequest {
+    private String query;
+    private int limit;
+
+    public abstract List<SuggestedReviewerInfo> get() throws RestApiException;
+
+    public SuggestedReviewersRequest withQuery(String query) {
+      this.query = query;
+      return this;
+    }
+
+    public SuggestedReviewersRequest withLimit(int limit) {
+      this.limit = limit;
+      return this;
+    }
+
+    public String getQuery() {
+      return query;
+    }
+
+    public int getLimit() {
+      return limit;
+    }
+  }
 
   /**
    * A default implementation which allows source compatibility
@@ -103,6 +190,16 @@
     }
 
     @Override
+    public String topic() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void topic(String topic) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public void addReviewer(AddReviewerInput in) throws RestApiException {
       throw new NotImplementedException();
     }
@@ -113,6 +210,16 @@
     }
 
     @Override
+    public SuggestedReviewersRequest suggestReviewers() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public SuggestedReviewersRequest suggestReviewers(String query) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public ChangeInfo get(EnumSet<ListChangesOption> options) throws RestApiException {
       throw new NotImplementedException();
     }
@@ -126,5 +233,30 @@
     public ChangeInfo info() throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public EditInfo getEdit() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void setHashtags(HashtagsInput input) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public Set<String> getHashtags() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ChangeInfo check() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public ChangeInfo check(FixInput fix) throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
index 201a0bd..8ab3080 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.ListChangesOption;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
@@ -24,10 +24,40 @@
 import java.util.List;
 
 public interface Changes {
+  /**
+   * Look up a change by numeric ID.
+   * <p>
+   * <strong>Note:</strong> This method eagerly reads the change. Methods that
+   * mutate the change do not necessarily re-read the change. Therefore, calling
+   * a getter method on an instance after calling a mutation method on that same
+   * instance is not guaranteed to reflect the mutation. It is not recommended
+   * to store references to {@code ChangeApi} instances.
+   *
+   * @param id change number.
+   * @return API for accessing the change.
+   * @throws RestApiException if an error occurred.
+   */
   ChangeApi id(int id) throws RestApiException;
-  ChangeApi id(String triplet) throws RestApiException;
+
+  /**
+   * Look up a change by string ID.
+   *
+   * @see #id(int)
+   * @param id any identifier supported by the REST API, including change
+   *     number, Change-Id, or project~branch~Change-Id triplet.
+   * @return API for accessing the change.
+   * @throws RestApiException if an error occurred.
+   */
+  ChangeApi id(String id) throws RestApiException;
+
+  /**
+   * Look up a change by project, branch, and change ID.
+   *
+   * @see #id(int)
+   */
   ChangeApi id(String project, String branch, String id)
       throws RestApiException;
+
   ChangeApi create(ChangeInfo in) throws RestApiException;
 
   QueryRequest query();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java
new file mode 100644
index 0000000..d0c5633
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.changes;
+
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public interface CommentApi {
+  CommentInfo get() throws RestApiException;
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  public class NotImplemented implements CommentApi {
+    @Override
+    public CommentInfo get() throws RestApiException {
+      throw new NotImplementedException();
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftApi.java
new file mode 100644
index 0000000..80a71f8
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftApi.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.changes;
+
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public interface DraftApi extends CommentApi {
+  CommentInfo update(DraftInput in) throws RestApiException;
+  void delete() throws RestApiException;
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  public class NotImplemented extends CommentApi.NotImplemented
+      implements DraftApi {
+    @Override
+    public CommentInfo update(DraftInput in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void delete() throws RestApiException {
+      throw new NotImplementedException();
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftInput.java
similarity index 80%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftInput.java
index 407b7c7..dd8f488 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DraftInput.java
@@ -12,10 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.extensions.api.changes;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+import com.google.gerrit.extensions.client.Comment;
+
+public class DraftInput extends Comment {
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java
new file mode 100644
index 0000000..f5f087c
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FileApi.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.changes;
+
+import com.google.gerrit.extensions.common.DiffInfo;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public interface FileApi {
+  BinaryResult content() throws RestApiException;
+
+  /**
+   * Diff against the revision's parent version of the file.
+   */
+  DiffInfo diff() throws RestApiException;
+
+  /**
+   * @param base revision id of the revision to be used as the
+   * diff base
+   */
+  DiffInfo diff(String base) throws RestApiException;
+
+  /**
+   * A default implementation which allows source compatibility
+   * when adding new methods to the interface.
+   **/
+  public class NotImplemented implements FileApi {
+    @Override
+    public BinaryResult content() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public DiffInfo diff() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public DiffInfo diff(String base) throws RestApiException {
+      throw new NotImplementedException();
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FixInput.java
similarity index 82%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FixInput.java
index 407b7c7..c8856e7 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FixInput.java
@@ -12,10 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.extensions.api.changes;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+public class FixInput {
+  public boolean deletePatchSetIfCommitMissing;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/HashtagsInput.java
similarity index 72%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/HashtagsInput.java
index 407b7c7..bf84ccb0 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/HashtagsInput.java
@@ -12,10 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.extensions.api.changes;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+import com.google.gerrit.extensions.restapi.DefaultInput;
+
+import java.util.Set;
+
+public class HashtagsInput {
+  @DefaultInput
+  public Set<String> add;
+  public Set<String> remove;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RebaseInput.java
similarity index 77%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RebaseInput.java
index 676c4d3..5f4a014 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RebaseInput.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2015 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,10 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.common;
+package com.google.gerrit.extensions.api.changes;
 
-public enum InheritableBoolean {
-  TRUE,
-  FALSE,
-  INHERIT
-}
\ No newline at end of file
+public class RebaseInput {
+  public String base;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index cf2d930..dd2ce92 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.extensions.api.changes;
 
-import com.google.gerrit.extensions.common.Comment;
+import com.google.gerrit.extensions.client.Comment;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 
 import java.util.LinkedHashMap;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 20bcea9..b940cc9 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -14,9 +14,14 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.common.FileInfo;
+import com.google.gerrit.extensions.common.MergeableInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 public interface RevisionApi {
@@ -29,11 +34,26 @@
   void publish() throws RestApiException;
   ChangeApi cherryPick(CherryPickInput in) throws RestApiException;
   ChangeApi rebase() throws RestApiException;
+  ChangeApi rebase(RebaseInput in) throws RestApiException;
   boolean canRebase();
 
   void setReviewed(String path, boolean reviewed) throws RestApiException;
   Set<String> reviewed() throws RestApiException;
 
+  Map<String, FileInfo> files() throws RestApiException;
+  Map<String, FileInfo> files(String base) throws RestApiException;
+  FileApi file(String path);
+  MergeableInfo mergeable() throws RestApiException;
+  MergeableInfo mergeableOtherBranches() throws RestApiException;
+
+  Map<String, List<CommentInfo>> comments() throws RestApiException;
+  Map<String, List<CommentInfo>> drafts() throws RestApiException;
+
+  DraftApi createDraft(DraftInput in) throws RestApiException;
+  DraftApi draft(String id) throws RestApiException;
+
+  CommentApi comment(String id) throws RestApiException;
+
   /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
@@ -75,6 +95,11 @@
     }
 
     @Override
+    public ChangeApi rebase(RebaseInput in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public boolean canRebase() {
       throw new NotImplementedException();
     }
@@ -88,5 +113,55 @@
     public Set<String> reviewed() throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public MergeableInfo mergeable() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public MergeableInfo mergeableOtherBranches() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public Map<String, FileInfo> files(String base) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public Map<String, FileInfo> files() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public FileApi file(String path) {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public Map<String, List<CommentInfo>> comments() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public Map<String, List<CommentInfo>> drafts() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public DraftApi createDraft(DraftInput in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public DraftApi draft(String id) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public CommentApi comment(String id) throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index d013c5d..07a48a1 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -22,6 +22,19 @@
   ProjectApi create() throws RestApiException;
   ProjectApi create(ProjectInput in) throws RestApiException;
   ProjectInfo get();
+
+  /**
+   * Look up a branch by refname.
+   * <p>
+   * <strong>Note:</strong> This method eagerly reads the branch. Methods that
+   * mutate the branch do not necessarily re-read the branch. Therefore, calling
+   * a getter method on an instance after calling a mutation method on that same
+   * instance is not guaranteed to reflect the mutation. It is not recommended
+   * to store references to {@code BranchApi} instances.
+   *
+   * @param ref branch name, with or without "refs/heads/" prefix.
+   * @return API for accessing the branch.
+   */
   BranchApi branch(String ref);
 
   /**
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
index 74b2be87..27bdf16 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectInput.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.extensions.api.projects;
 
-import com.google.gerrit.extensions.common.InheritableBoolean;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.SubmitType;
 
 import java.util.List;
 import java.util.Map;
@@ -33,6 +33,7 @@
   public InheritableBoolean useSignedOffBy;
   public InheritableBoolean useContentMerge;
   public InheritableBoolean requireChangeId;
+  public InheritableBoolean createNewChangeForAllNotInTarget;
   public String maxObjectSizeLimit;
   public Map<String, Map<String, ConfigValue>> pluginConfigValues;
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
index 9c0cfd8..736d375 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
@@ -21,6 +21,19 @@
 import java.util.List;
 
 public interface Projects {
+  /**
+   * Look up a project by name.
+   * <p>
+   * <strong>Note:</strong> This method eagerly reads the project. Methods that
+   * mutate the project do not necessarily re-read the project. Therefore,
+   * calling a getter method on an instance after calling a mutation method on
+   * that same instance is not guaranteed to reflect the mutation. It is not
+   * recommended to store references to {@code ProjectApi} instances.
+   *
+   * @param name project name.
+   * @return API for accessing the project.
+   * @throws RestApiException if an error occurred.
+   */
   ProjectApi name(String name) throws RestApiException;
 
   ListRequest list();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeStatus.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeStatus.java
similarity index 98%
rename from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeStatus.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeStatus.java
index 9af66f2..f3fc887 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeStatus.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeStatus.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.common;
+package com.google.gerrit.extensions.client;
 
 /* Current state within the basic workflow of the change **/
 public enum ChangeStatus {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/Comment.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
similarity index 89%
rename from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/Comment.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
index b7ad9a7..e79df1c 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/Comment.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.common;
+package com.google.gerrit.extensions.client;
 
 import java.sql.Timestamp;
 
@@ -20,16 +20,12 @@
   public String id;
   public String path;
   public Side side;
-  public int line;
+  public Integer line;
   public Range range;
   public String inReplyTo;
   public Timestamp updated;
   public String message;
 
-  public static enum Side {
-    PARENT, REVISION
-  }
-
   public static class Range {
     public int startLine;
     public int startCharacter;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/GerritTopMenu.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GerritTopMenu.java
similarity index 94%
rename from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/GerritTopMenu.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GerritTopMenu.java
index e3821fc..81d6149 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/GerritTopMenu.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GerritTopMenu.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.webui;
+package com.google.gerrit.extensions.client;
 
 public enum GerritTopMenu {
   ALL, MY, DIFFERENCES, PROJECTS, PEOPLE, PLUGINS, DOCUMENTATION;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/InheritableBoolean.java
similarity index 93%
rename from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/InheritableBoolean.java
index 676c4d3..57d4849 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/InheritableBoolean.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.common;
+package com.google.gerrit.extensions.client;
 
 public enum InheritableBoolean {
   TRUE,
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ListChangesOption.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
similarity index 91%
rename from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ListChangesOption.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
index f9f8b62..54617a7 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ListChangesOption.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.common;
+package com.google.gerrit.extensions.client;
 
 import java.util.EnumSet;
 
@@ -52,7 +52,13 @@
   DOWNLOAD_COMMANDS(13),
 
   /** Include patch set weblinks. */
-  WEB_LINKS(14);
+  WEB_LINKS(14),
+
+  /** Include consistency check results. */
+  CHECK(15),
+
+  /** Include allowed change actions client could perform. */
+  CHANGE_ACTIONS(16);
 
   private final int value;
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectState.java
similarity index 92%
rename from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectState.java
index 407b7c7..6f4190d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ProjectState.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.extensions.client;
 
 public enum ProjectState {
   ACTIVE,
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Side.java
similarity index 84%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Side.java
index 407b7c7..5d5af75 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Side.java
@@ -12,10 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.extensions.client;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
+public enum Side {
+  PARENT, REVISION
 }
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SubmitType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/SubmitType.java
similarity index 93%
rename from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SubmitType.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/SubmitType.java
index 95a9693..2b916f1 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SubmitType.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/SubmitType.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.common;
+package com.google.gerrit.extensions.client;
 
 public enum SubmitType {
   FAST_FORWARD_ONLY,
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Theme.java
similarity index 63%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Theme.java
index 407b7c7..39730b7 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Theme.java
@@ -12,10 +12,28 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.extensions.client;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
+public enum Theme {
+  // Light themes
+  DEFAULT,
+  ECLIPSE,
+  ELEGANT,
+  NEAT,
+
+  // Dark themes
+  MIDNIGHT,
+  NIGHT,
+  TWILIGHT;
+
+  public boolean isDark() {
+    switch (this) {
+      case MIDNIGHT:
+      case NIGHT:
+      case TWILIGHT:
+        return true;
+      default:
+        return false;
+    }
+  }
 }
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java
index 5130f9f..39d98de 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java
@@ -14,9 +14,16 @@
 
 package com.google.gerrit.extensions.common;
 
+import java.util.List;
+
 public class AccountInfo {
   public Integer _accountId;
   public String name;
   public String email;
   public String username;
+  public List<AvatarInfo> avatars;
+
+  public AccountInfo(Integer id) {
+    this._accountId = id;
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java
index 2176e8f..ce120cd 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ApprovalInfo.java
@@ -19,4 +19,8 @@
 public class ApprovalInfo extends AccountInfo {
   public Integer value;
   public Timestamp date;
+
+  public ApprovalInfo(Integer id) {
+    super(id);
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AvatarInfo.java
similarity index 61%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AvatarInfo.java
index 676c4d3..793aa24 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AvatarInfo.java
@@ -14,8 +14,18 @@
 
 package com.google.gerrit.extensions.common;
 
-public enum InheritableBoolean {
-  TRUE,
-  FALSE,
-  INHERIT
-}
\ No newline at end of file
+public class AvatarInfo {
+  /**
+   * Size in pixels the UI prefers an avatar image to be.
+   *
+   * The web UI prefers avatar images to be square, both
+   * the height and width of the image should be this size.
+   * The height is the more important dimension to match
+   * than the width.
+   */
+  public static final int DEFAULT_SIZE = 26;
+
+  public String url;
+  public Integer height;
+  public Integer width;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 653ec37..13baf6b 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -14,8 +14,11 @@
 
 package com.google.gerrit.extensions.common;
 
+import com.google.gerrit.extensions.client.ChangeStatus;
+
 import java.sql.Timestamp;
 import java.util.Collection;
+import java.util.List;
 import java.util.Map;
 
 public class ChangeInfo {
@@ -23,6 +26,7 @@
   public String project;
   public String branch;
   public String topic;
+  public Collection<String> hashtags;
   public String changeId;
   public String subject;
   public ChangeStatus status;
@@ -33,11 +37,21 @@
   public Boolean mergeable;
   public Integer insertions;
   public Integer deletions;
+
+  public String baseChange;
+  public int _number;
+
   public AccountInfo owner;
-  public String currentRevision;
+
   public Map<String, ActionInfo> actions;
   public Map<String, LabelInfo> labels;
+  public Map<String, Collection<String>> permittedLabels;
+  public Collection<AccountInfo> removableReviewers;
   public Collection<ChangeMessageInfo> messages;
+
+  public String currentRevision;
   public Map<String, RevisionInfo> revisions;
-  public int _number;
+  public Boolean _moreChanges;
+
+  public List<ProblemInfo> problems;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeType.java
new file mode 100644
index 0000000..d55580c
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeType.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+/** Type of modification made to the file path. */
+public enum ChangeType {
+  /** Path is being created/introduced by this patch. */
+  ADDED,
+
+  /** Path already exists, and has updated content. */
+  MODIFIED,
+
+  /** Path existed, but is being removed by this patch. */
+  DELETED,
+
+  /** Path existed but was moved. */
+  RENAMED,
+
+  /** Path was copied from source. */
+  COPIED,
+
+  /** Sufficient amount of content changed to claim the file was rewritten. */
+  REWRITE;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommentInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommentInfo.java
index cef1718..b1f8183 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommentInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommentInfo.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.extensions.common;
 
+import com.google.gerrit.extensions.client.Comment;
+
 public class CommentInfo extends Comment {
   public AccountInfo author;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommitInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommitInfo.java
index 7313ab3..a4e4071 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommitInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/CommitInfo.java
@@ -23,4 +23,5 @@
   public GitPerson committer;
   public String subject;
   public String message;
+  public List<WebLinkInfo> webLinks;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java
new file mode 100644
index 0000000..62b1dc7
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import java.util.List;
+
+/* This entity contains information about the diff of a file in a revision. */
+public class DiffInfo {
+  // Meta information about the file on side A
+  public FileMeta metaA;
+  // Meta information about the file on side B
+  public FileMeta metaB;
+  // Intraline status
+  public IntraLineStatus intralineStatus;
+  // The type of change
+  public ChangeType changeType;
+  // A list of strings representing the patch set diff header
+  public List<String> diffHeader;
+  // The content differences in the file as a list of entities
+  public List<ContentEntry> content;
+  // Links to the file diff in external sites
+  public List<DiffWebLinkInfo> webLinks;
+  // Binary file
+  public Boolean binary;
+
+  public static enum IntraLineStatus {
+    OK,
+    TIMEOUT,
+    FAILURE
+  }
+
+  public static class FileMeta {
+    // The name of the file
+    public String name;
+    // The content type of the file
+    public String contentType;
+    // The total number of lines in the file
+    public Integer lines;
+    // Links to the file in external sites
+    public List<WebLinkInfo> webLinks;
+  }
+
+  public static final class ContentEntry {
+    // Common lines to both sides.
+    public List<String> ab;
+    // Lines of a.
+    public List<String> a;
+    // Lines of b.
+    public List<String> b;
+
+    // A list of changed sections of the corresponding line list.
+    // Each entry is a character <offset, length> pair. The offset is from the
+    // beginning of the first line in the list. Also, the offset includes an
+    // implied trailing newline character for each line.
+    public List<List<Integer>> editA;
+    public List<List<Integer>> editB;
+
+    // a and b are actually common with this whitespace ignore setting.
+    public Boolean common;
+
+    // Number of lines to skip on both sides.
+    public Integer skip;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffWebLinkInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffWebLinkInfo.java
new file mode 100644
index 0000000..71acca3
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffWebLinkInfo.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+public class DiffWebLinkInfo extends WebLinkInfo {
+  public Boolean showOnSideBySideDiffView;
+  public Boolean showOnUnifiedDiffView;
+
+  public static DiffWebLinkInfo forSideBySideDiffView(String name,
+      String imageUrl, String url, String target) {
+    return new DiffWebLinkInfo(name, imageUrl, url, target, true, false);
+  }
+
+  public static DiffWebLinkInfo forUnifiedDiffView(String name,
+      String imageUrl, String url, String target) {
+    return new DiffWebLinkInfo(name, imageUrl, url, target, false, true);
+  }
+
+  public static DiffWebLinkInfo forSideBySideAndUnifiedDiffView(String name,
+      String imageUrl, String url, String target) {
+    return new DiffWebLinkInfo(name, imageUrl, url, target, true, true);
+  }
+
+  private DiffWebLinkInfo(String name, String imageUrl, String url,
+      String target, boolean showOnSideBySideDiffView,
+      boolean showOnUnifiedDiffView) {
+    super(name, imageUrl, url, target);
+    this.showOnSideBySideDiffView = showOnSideBySideDiffView ? true : null;
+    this.showOnUnifiedDiffView = showOnUnifiedDiffView ? true : null;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SubmitType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/EditInfo.java
similarity index 78%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SubmitType.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/EditInfo.java
index 95a9693..9dc92a8 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SubmitType.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/EditInfo.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.extensions.common;
 
-public enum SubmitType {
-  FAST_FORWARD_ONLY,
-  MERGE_IF_NECESSARY,
-  REBASE_IF_NECESSARY,
-  MERGE_ALWAYS,
-  CHERRY_PICK
-}
\ No newline at end of file
+import java.util.Map;
+
+public class EditInfo {
+  public CommitInfo commit;
+  public String baseRevision;
+  public Map<String, FetchInfo> fetch;
+  public Map<String, FileInfo> files;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupBaseInfo.java
similarity index 89%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupBaseInfo.java
index 676c4d3..288adb6 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupBaseInfo.java
@@ -14,8 +14,7 @@
 
 package com.google.gerrit.extensions.common;
 
-public enum InheritableBoolean {
-  TRUE,
-  FALSE,
-  INHERIT
-}
\ No newline at end of file
+public class GroupBaseInfo {
+  public String id;
+  public String name;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/LabelInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/LabelInfo.java
index 1e4edcd..76dd93d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/LabelInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/LabelInfo.java
@@ -23,7 +23,9 @@
   public AccountInfo recommended;
   public AccountInfo disliked;
   public List<ApprovalInfo> all;
+
   public Map<String, String> values;
+
   public Short value;
   public Short defaultValue;
   public Boolean optional;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeableInfo.java
similarity index 75%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeableInfo.java
index 676c4d3..9c38055 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeableInfo.java
@@ -14,8 +14,12 @@
 
 package com.google.gerrit.extensions.common;
 
-public enum InheritableBoolean {
-  TRUE,
-  FALSE,
-  INHERIT
-}
\ No newline at end of file
+import com.google.gerrit.extensions.client.SubmitType;
+
+import java.util.List;
+
+public class MergeableInfo {
+  public SubmitType submitType;
+  public boolean mergeable;
+  public List<String> mergeableInto;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/Comment.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
similarity index 60%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/Comment.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
index b7ad9a7..a117d07 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/Comment.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
@@ -14,26 +14,23 @@
 
 package com.google.gerrit.extensions.common;
 
-import java.sql.Timestamp;
-
-public abstract class Comment {
-  public String id;
-  public String path;
-  public Side side;
-  public int line;
-  public Range range;
-  public String inReplyTo;
-  public Timestamp updated;
-  public String message;
-
-  public static enum Side {
-    PARENT, REVISION
+public class ProblemInfo {
+  public static enum Status {
+    FIXED, FIX_FAILED;
   }
 
-  public static class Range {
-    public int startLine;
-    public int startCharacter;
-    public int endLine;
-    public int endCharacter;
+  public String message;
+  public Status status;
+  public String outcome;
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder(getClass().getSimpleName())
+        .append('[').append(message);
+    if (status != null || outcome != null) {
+      sb.append(" (").append(status).append(": ").append(outcome)
+          .append(')');
+    }
+    return sb.append(']').toString();
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProjectInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProjectInfo.java
index bb07e44..4036740 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProjectInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProjectInfo.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.extensions.common;
 
-import com.google.gerrit.extensions.api.projects.ProjectState;
+import com.google.gerrit.extensions.client.ProjectState;
 
 import java.util.List;
 import java.util.Map;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
index 8f61aa2..4b8eec1 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.extensions.common;
 
-import java.util.List;
+import java.sql.Timestamp;
 import java.util.Map;
 
 public class RevisionInfo {
@@ -22,9 +22,11 @@
   public Boolean draft;
   public Boolean hasDraftComments;
   public int _number;
+  public Timestamp created;
+  public AccountInfo uploader;
+  public String ref;
   public Map<String, FetchInfo> fetch;
   public CommitInfo commit;
   public Map<String, FileInfo> files;
   public Map<String, ActionInfo> actions;
-  public List<WebLinkInfo> webLinks;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestedReviewerInfo.java
similarity index 86%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestedReviewerInfo.java
index 676c4d3..d371f35 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/InheritableBoolean.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestedReviewerInfo.java
@@ -14,8 +14,7 @@
 
 package com.google.gerrit.extensions.common;
 
-public enum InheritableBoolean {
-  TRUE,
-  FALSE,
-  INHERIT
-}
\ No newline at end of file
+public class SuggestedReviewerInfo {
+  public AccountInfo account;
+  public GroupBaseInfo group;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/Comment.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/TagInfo.java
similarity index 61%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/Comment.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/TagInfo.java
index b7ad9a7..3e3d8db 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/Comment.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/TagInfo.java
@@ -14,26 +14,23 @@
 
 package com.google.gerrit.extensions.common;
 
-import java.sql.Timestamp;
-
-public abstract class Comment {
-  public String id;
-  public String path;
-  public Side side;
-  public int line;
-  public Range range;
-  public String inReplyTo;
-  public Timestamp updated;
+public class TagInfo {
+  public String ref;
+  public String revision;
+  public String object;
   public String message;
+  public GitPerson tagger;
 
-  public static enum Side {
-    PARENT, REVISION
+  public TagInfo(String ref, String revision) {
+    this.ref = ref;
+    this.revision = revision;
   }
 
-  public static class Range {
-    public int startLine;
-    public int startCharacter;
-    public int endLine;
-    public int endCharacter;
+  public TagInfo(String ref, String revision, String object,
+      String message, GitPerson tagger) {
+    this(ref, revision);
+    this.object = object;
+    this.message = message;
+    this.tagger = tagger;
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/WebLinkInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/WebLinkInfo.java
index 7695c8c..d9a34bf 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/WebLinkInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/WebLinkInfo.java
@@ -16,10 +16,14 @@
 
 public class WebLinkInfo {
   public String name;
+  public String imageUrl;
   public String url;
+  public String target;
 
-  public WebLinkInfo(String name, String url) {
+  public WebLinkInfo(String name, String imageUrl, String url, String target) {
     this.name = name;
+    this.imageUrl = imageUrl;
     this.url = url;
+    this.target = target;
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java
index 7edfaed..1388637 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java
@@ -200,19 +200,20 @@
   }
 
   private class ReloadableHandle implements ReloadableRegistrationHandle<T> {
-    private final Key<T> key;
+    private final Key<T> handleKey;
     private final NamedProvider<T> item;
     private final NamedProvider<T> defaultItem;
 
-    ReloadableHandle(Key<T> key, NamedProvider<T> item, NamedProvider<T> defaultItem) {
-      this.key = key;
+    ReloadableHandle(Key<T> handleKey, NamedProvider<T> item,
+        NamedProvider<T> defaultItem) {
+      this.handleKey = handleKey;
       this.item = item;
       this.defaultItem = defaultItem;
     }
 
     @Override
     public Key<T> getKey() {
-      return key;
+      return handleKey;
     }
 
     @Override
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java
index 9b09d15..01551e6 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java
@@ -36,6 +36,7 @@
     this.key = key;
   }
 
+  @Override
   public DynamicItem<T> get() {
     return new DynamicItem<>(key, find(injector, type), "gerrit");
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
index 4251891..abf944a 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
@@ -139,6 +139,7 @@
   }
 
   /** Iterate through all entries in an undefined order. */
+  @Override
   public Iterator<Entry<T>> iterator() {
     final Iterator<Map.Entry<NamePair, Provider<T>>> i =
         items.entrySet().iterator();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java
index 2554673..8fcbdd9 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java
@@ -32,6 +32,7 @@
     this.type = type;
   }
 
+  @Override
   public DynamicMap<T> get() {
     PrivateInternals_DynamicMapImpl<T> m =
         new PrivateInternals_DynamicMapImpl<>();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
index 628745a..82613c7 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -129,7 +129,7 @@
   }
 
   public static <T> DynamicSet<T> emptySet() {
-    return new DynamicSet<T>(
+    return new DynamicSet<>(
         Collections.<AtomicReference<Provider<T>>> emptySet());
   }
 
@@ -139,6 +139,10 @@
     items = new CopyOnWriteArrayList<>(base);
   }
 
+  public DynamicSet() {
+    this(Collections.<AtomicReference<Provider<T>>>emptySet());
+  }
+
   @Override
   public Iterator<T> iterator() {
     final Iterator<AtomicReference<Provider<T>>> itr = items.iterator();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
index 9ea96d4..d8b027b 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
@@ -35,6 +35,7 @@
     this.type = type;
   }
 
+  @Override
   public DynamicSet<T> get() {
     return new DynamicSet<>(find(injector, type));
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AcceptsDelete.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AcceptsDelete.java
new file mode 100644
index 0000000..2f615c1
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AcceptsDelete.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+
+/**
+ * Optional interface for {@link RestCollection}.
+ * <p>
+ * Collections that implement this interface can accept a {@code DELETE} directly
+ * on the collection itself.
+ */
+public interface AcceptsDelete<P extends RestResource> {
+  /**
+   * Handle deletion of a child resource by DELETE on the collection.
+   *
+   * @param parent parent collection handle.
+   * @param id id of the resource being created (optional).
+   * @return a view to perform the deletion.
+   * @throws RestApiException the view cannot be constructed.
+   */
+  <I> RestModifyView<P, I> delete(P parent, IdString id)
+      throws RestApiException;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
index 852c56e..18f356b 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
@@ -14,11 +14,17 @@
 
 package com.google.gerrit.extensions.restapi;
 
+import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
+import java.nio.charset.UnsupportedCharsetException;
 
 /**
  * Wrapper around a non-JSON result from a {@link RestView}.
@@ -34,13 +40,7 @@
 
   /** Produce a UTF-8 encoded result from a string. */
   public static BinaryResult create(String data) {
-    try {
-      return create(data.getBytes("UTF-8"))
-        .setContentType("text/plain")
-        .setCharacterEncoding("UTF-8");
-    } catch (UnsupportedEncodingException e) {
-      throw new RuntimeException("JVM does not support UTF-8", e);
-    }
+    return new StringResult(data);
   }
 
   /** Produce an {@code application/octet-stream} result from a byte array. */
@@ -144,7 +144,30 @@
    */
   public abstract void writeTo(OutputStream os) throws IOException;
 
+  /**
+   * Return a copy of the result as a String.
+   * <p>
+   * The default version of this method copies the result into a temporary byte
+   * array and then tries to decode it using the configured encoding.
+   *
+   * @return string version of the result.
+   * @throws IOException if the data cannot be produced or could not be
+   *         decoded to a String.
+   */
+  public String asString() throws IOException {
+    long len = getContentLength();
+    ByteArrayOutputStream buf;
+    if (0 < len) {
+      buf = new ByteArrayOutputStream((int) len);
+    } else {
+      buf = new ByteArrayOutputStream();
+    }
+    writeTo(buf);
+    return decode(buf.toByteArray(), getCharacterEncoding());
+  }
+
   /** Close the result and release any resources it holds. */
+  @Override
   public void close() throws IOException {
   }
 
@@ -160,6 +183,25 @@
         getContentType());
   }
 
+  private static String decode(byte[] data, String enc) {
+    try {
+      Charset cs = enc != null
+          ? Charset.forName(enc)
+          : StandardCharsets.UTF_8;
+      return cs.newDecoder()
+        .onMalformedInput(CodingErrorAction.REPORT)
+        .onUnmappableCharacter(CodingErrorAction.REPORT)
+        .decode(ByteBuffer.wrap(data))
+        .toString();
+    } catch (UnsupportedCharsetException | CharacterCodingException e) {
+      // Fallback to ISO-8850-1 style encoding.
+      StringBuilder r = new StringBuilder(data.length);
+      for (byte b : data)
+          r.append((char) (b & 0xff));
+      return r.toString();
+    }
+  }
+
   private static class Array extends BinaryResult {
     private final byte[] data;
 
@@ -172,6 +214,27 @@
     public void writeTo(OutputStream os) throws IOException {
       os.write(data);
     }
+
+    @Override
+    public String asString() {
+      return decode(data, getCharacterEncoding());
+    }
+  }
+
+  private static class StringResult extends Array {
+    private final String str;
+
+    StringResult(String str) {
+      super(str.getBytes(StandardCharsets.UTF_8));
+      setContentType("text/plain");
+      setCharacterEncoding("UTF-8");
+      this.str = str;
+    }
+
+    @Override
+    public String asString() {
+      return str;
+    }
   }
 
   private static class Stream extends BinaryResult {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/CacheControl.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/CacheControl.java
index 3aa1781..d71732b 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/CacheControl.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/CacheControl.java
@@ -19,6 +19,7 @@
 public class CacheControl {
 
   public enum Type {
+    @SuppressWarnings("hiding")
     NONE, PUBLIC, PRIVATE
   }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ETagView.java
similarity index 65%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ETagView.java
index 407b7c7..f95161d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ETagView.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2015 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,10 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.extensions.restapi;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+/**
+ * A view which may change, although the underlying resource did not change
+ */
+public interface ETagView<R extends RestResource> extends RestReadView<R> {
+  public String getETag(R rsrc);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
index 76942e6..611812a 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
@@ -22,19 +22,17 @@
   public ResourceNotFoundException() {
   }
 
-  /** @param id portion of the resource URI that does not exist. */
-  public ResourceNotFoundException(String id) {
-    super(id);
+  public ResourceNotFoundException(String msg) {
+    super(msg);
   }
 
-  /** @param id portion of the resource URI that does not exist. */
-  public ResourceNotFoundException(String id, Throwable cause) {
-    super(id, cause);
+  public ResourceNotFoundException(String msg, Throwable cause) {
+    super(msg, cause);
   }
 
   /** @param id portion of the resource URI that does not exist. */
   public ResourceNotFoundException(IdString id) {
-    super(id.get());
+    super("Not found: " + id.get());
   }
 
   @SuppressWarnings("unchecked")
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
index 314c898..4345076 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.extensions.restapi;
 
+import java.util.concurrent.TimeUnit;
+
 /** Special return value to mean specific HTTP status codes in a REST API. */
 public abstract class Response<T> {
   @SuppressWarnings({"rawtypes"})
@@ -24,6 +26,11 @@
     return new Impl<>(200, value);
   }
 
+  public static <T> Response<T> withMustRevalidate(T value) {
+    return ok(value).caching(
+        CacheControl.PRIVATE(0, TimeUnit.SECONDS).setMustRevalidate());
+  }
+
   /** HTTP 201 Created: typically used when a new resource is made. */
   public static <T> Response<T> created(T value) {
     return new Impl<>(201, value);
@@ -48,10 +55,12 @@
     return obj;
   }
 
+  public abstract boolean isNone();
   public abstract int statusCode();
   public abstract T value();
   public abstract CacheControl caching();
   public abstract Response<T> caching(CacheControl c);
+  @Override
   public abstract String toString();
 
   private static final class Impl<T> extends Response<T> {
@@ -65,6 +74,11 @@
     }
 
     @Override
+    public boolean isNone() {
+      return false;
+    }
+
+    @Override
     public int statusCode() {
       return statusCode;
     }
@@ -96,10 +110,16 @@
     }
 
     @Override
+    public boolean isNone() {
+      return true;
+    }
+
+    @Override
     public int statusCode() {
       return 204;
     }
 
+    @Override
     public Object value() {
       throw new UnsupportedOperationException();
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/BranchWebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/BranchWebLink.java
new file mode 100644
index 0000000..2a11012
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/BranchWebLink.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.webui;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.WebLinkInfo;
+
+@ExtensionPoint
+public interface BranchWebLink extends WebLink {
+
+  /**
+   * {@link com.google.gerrit.extensions.common.WebLinkInfo}
+   * describing a link from a branch to an external service.
+   *
+   * <p>In order for the web link to be visible
+   * {@link com.google.gerrit.extensions.common.WebLinkInfo#url}
+   * and {@link com.google.gerrit.extensions.common.WebLinkInfo#name}
+   * must be set.<p>
+   *
+   * @param projectName Name of the project
+   * @param branchName Name of the branch
+   * @return WebLinkInfo that links to branch in external service,
+   * null if there should be no link.
+   */
+  WebLinkInfo getBranchWebLink(String projectName, String branchName);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/DiffWebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/DiffWebLink.java
new file mode 100644
index 0000000..ad53519
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/DiffWebLink.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.webui;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.DiffWebLinkInfo;
+
+@ExtensionPoint
+public interface DiffWebLink extends WebLink {
+
+  /**
+   * {@link com.google.gerrit.extensions.common.DiffWebLinkInfo}
+   * describing a link from a file diff to an external service.
+   *
+   * <p>In order for the web link to be visible
+   * {@link com.google.gerrit.extensions.common.WebLinkInfo#url}
+   * and {@link com.google.gerrit.extensions.common.WebLinkInfo#name}
+   * must be set.<p>
+   *
+   * @param projectName Name of the project
+   * @param changeId ID of the change
+   * @param patchSetIdA Patch set ID of side A, <code>null</code> if no base
+   *        patch set was selected
+   * @param revisionA Name of the revision of side A (e.g. branch or commit ID)
+   * @param fileNameA Name of the file of side A
+   * @param patchSetIdB Patch set ID of side B
+   * @param revisionB Name of the revision of side B (e.g. branch or commit ID)
+   * @param fileNameB Name of the file of side B
+   * @return WebLinkInfo that links to file diff in external service,
+   * null if there should be no link.
+   */
+  DiffWebLinkInfo getDiffLink(String projectName, int changeId,
+        Integer patchSetIdA, String revisionA, String fileNameA,
+        int patchSetIdB, String revisionB, String fileNameB);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/FileWebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/FileWebLink.java
new file mode 100644
index 0000000..9136b36
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/FileWebLink.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.webui;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.WebLinkInfo;
+
+@ExtensionPoint
+public interface FileWebLink extends WebLink {
+
+  /**
+   * {@link com.google.gerrit.extensions.common.WebLinkInfo}
+   * describing a link from a file to an external service.
+   *
+   * <p>In order for the web link to be visible
+   * {@link com.google.gerrit.extensions.common.WebLinkInfo#url}
+   * and {@link com.google.gerrit.extensions.common.WebLinkInfo#name}
+   * must be set.<p>
+   *
+   * @param projectName Name of the project
+   * @param revision Name of the revision (e.g. branch or commit ID)
+   * @param fileName Name of the file
+   * @return WebLinkInfo that links to project in external service,
+   * null if there should be no link.
+   */
+  WebLinkInfo getFileWebLink(String projectName, String revision, String fileName);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/JavaScriptPlugin.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/JavaScriptPlugin.java
index 89a4f33..4619a06 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/JavaScriptPlugin.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/JavaScriptPlugin.java
@@ -16,6 +16,9 @@
 
 /** Configures a web UI plugin written using JavaScript. */
 public class JavaScriptPlugin extends WebUiPlugin {
+  public static final String INIT_JS = "init.js";
+  public static final String STATIC_INIT_JS = "static/" + INIT_JS;
+
   private final String fileName;
 
   /**
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java
index b6086f2..ad74849 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PatchSetWebLink.java
@@ -11,19 +11,28 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
+
 package com.google.gerrit.extensions.webui;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.WebLinkInfo;
 
 @ExtensionPoint
-public interface PatchSetWebLink extends WebLink {
+public interface PatchSetWebLink extends WebLink{
 
   /**
-   * URL to patch set in external service.
+   * {@link com.google.gerrit.extensions.common.WebLinkInfo}
+   * describing a link from a patch set to an external service.
+   *
+   * <p>In order for the web link to be visible
+   * {@link com.google.gerrit.extensions.common.WebLinkInfo#url}
+   * and {@link com.google.gerrit.extensions.common.WebLinkInfo#name}
+   * must be set.<p>
    *
    * @param projectName Name of the project
    * @param commit Commit of the patch set
-   * @return url to patch set in external service.
+   * @return WebLinkInfo that links to patch set in external service,
+   * null if there should be no link.
    */
-  String getPatchSetUrl(final String projectName, final String commit);
+  WebLinkInfo getPatchSetWebLink(final String projectName, final String commit);
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/ProjectWebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/ProjectWebLink.java
index 61e9982..2f8e802 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/ProjectWebLink.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/ProjectWebLink.java
@@ -15,15 +15,23 @@
 package com.google.gerrit.extensions.webui;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.WebLinkInfo;
 
 @ExtensionPoint
 public interface ProjectWebLink extends WebLink {
 
   /**
-   * URL to project in external service.
+   * {@link com.google.gerrit.extensions.common.WebLinkInfo}
+   * describing a link from a project to an external service.
+   *
+   * <p>In order for the web link to be visible
+   * {@link com.google.gerrit.extensions.common.WebLinkInfo#url}
+   * and {@link com.google.gerrit.extensions.common.WebLinkInfo#name}
+   * must be set.<p>
    *
    * @param projectName Name of the project
-   * @return url to project in external service.
+   * @return WebLinkInfo that links to project in external service,
+   * null if there should be no link.
    */
-  String getProjectUrl(String projectName);
+  WebLinkInfo getProjectWeblink(String projectName);
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/TopMenu.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/TopMenu.java
index e5a1f7e..ead7c31 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/TopMenu.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/TopMenu.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.extensions.webui;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.client.GerritTopMenu;
 
 import java.util.List;
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java
index 19d9ab7..e497f7d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java
@@ -11,14 +11,35 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
+
 package com.google.gerrit.extensions.webui;
 
-public interface WebLink {
 
+/**
+ * Marks that the implementor has a method that provides
+ * a weblinkInfo
+ *
+ */
+public interface WebLink {
   /**
-   * The link-name displayed in UI.
-   *
-   * @return name of link.
+   * Class that holds target defaults for WebLink anchors.
    */
-  String getLinkName();
+  public static class Target {
+    /**
+     * Opens the link in a new window or tab
+     */
+    public static final String BLANK = "_blank";
+    /**
+     * Opens the link in the frame it was clicked.
+     */
+    public static final String SELF = "_self";
+    /**
+     * Opens link in parent frame.
+     */
+    public static final String PARENT = "_parent";
+    /**
+     * Opens link in the full body of the window.
+     */
+    public static final String TOP = "_top";
+  }
 }
diff --git a/gerrit-gwtdebug/BUCK b/gerrit-gwtdebug/BUCK
index a926773..bf05af0 100644
--- a/gerrit-gwtdebug/BUCK
+++ b/gerrit-gwtdebug/BUCK
@@ -1,11 +1,16 @@
 java_library(
   name = 'gwtdebug',
-  srcs = ['src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java'],
+  srcs = glob(['src/main/java/**/*.java']),
   deps = [
+    '//gerrit-pgm:pgm',
+    '//gerrit-pgm:util',
+    '//gerrit-util-cli:cli',
     '//lib/gwt:dev',
     '//lib/jetty:server',
     '//lib/jetty:servlet',
-    '//lib/jetty:webapp',
+    '//lib/jetty:servlets',
+    '//lib/log:api',
+    '//lib/log:log4j',
   ],
   visibility = ['//tools/eclipse:classpath'],
 )
diff --git a/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java b/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java
deleted file mode 100644
index 09a7fb6..0000000
--- a/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java
+++ /dev/null
@@ -1,524 +0,0 @@
-/*
- * Copyright 2008 Google Inc.
- *
- * 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.gwtdebug;
-
-import com.google.gwt.core.ext.ServletContainer;
-import com.google.gwt.core.ext.ServletContainerLauncher;
-import com.google.gwt.core.ext.TreeLogger;
-import com.google.gwt.core.ext.UnableToCompleteException;
-
-import org.eclipse.jetty.http.HttpField;
-import org.eclipse.jetty.server.HttpConfiguration;
-import org.eclipse.jetty.server.HttpConnectionFactory;
-import org.eclipse.jetty.server.Request;
-import org.eclipse.jetty.server.RequestLog;
-import org.eclipse.jetty.server.Response;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.ServerConnector;
-import org.eclipse.jetty.server.handler.RequestLogHandler;
-import org.eclipse.jetty.util.component.AbstractLifeCycle;
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
-import org.eclipse.jetty.webapp.WebAppClassLoader;
-import org.eclipse.jetty.webapp.WebAppContext;
-
-import java.io.File;
-import java.io.IOException;
-import java.net.URL;
-import java.net.URLClassLoader;
-
-public class GerritDebugLauncher extends ServletContainerLauncher {
-
-  private final static boolean __escape = true;
-
-  /**
-   * Log jetty requests/responses to TreeLogger.
-   */
-  public static class JettyRequestLogger extends AbstractLifeCycle implements
-      RequestLog {
-
-    private final TreeLogger logger;
-
-    public JettyRequestLogger(TreeLogger logger) {
-      this.logger = logger;
-    }
-
-    /**
-     * Log an HTTP request/response to TreeLogger.
-     */
-    public void log(Request request, Response response) {
-      int status = response.getStatus();
-      if (status < 0) {
-        // Copied from NCSARequestLog
-        status = 404;
-      }
-      TreeLogger.Type logStatus, logHeaders;
-      if (status >= 500) {
-        logStatus = TreeLogger.ERROR;
-        logHeaders = TreeLogger.INFO;
-      } else if (status >= 400) {
-        logStatus = TreeLogger.WARN;
-        logHeaders = TreeLogger.INFO;
-      } else {
-        logStatus = TreeLogger.INFO;
-        logHeaders = TreeLogger.DEBUG;
-      }
-      String userString = request.getRemoteUser();
-      if (userString == null) {
-        userString = "";
-      } else {
-        userString += "@";
-      }
-      String bytesString = "";
-      if (response.getContentCount() > 0) {
-        bytesString = " " + response.getContentCount() + " bytes";
-      }
-      if (logger.isLoggable(logStatus)) {
-        TreeLogger branch =
-            logger.branch(logStatus, String.valueOf(status) + " - "
-                + request.getMethod() + ' ' + request.getUri() + " ("
-                + userString + request.getRemoteHost() + ')' + bytesString);
-        if (branch.isLoggable(logHeaders)) {
-          // Request headers
-          TreeLogger headers = branch.branch(logHeaders, "Request headers");
-          for (HttpField f : request.getHttpFields()) {
-            headers.log(logHeaders, f.getName() + ": " + f.getValue());
-          }
-          // Response headers
-          headers = branch.branch(logHeaders, "Response headers");
-          for (HttpField f : response.getHttpFields()) {
-            headers.log(logHeaders, f.getName() + ": " + f.getValue());
-          }
-        }
-      }
-    }
-  }
-
-  /**
-   * An adapter for the Jetty logging system to GWT's TreeLogger. This
-   * implementation class is only public to allow {@link Log} to instantiate it.
-   *
-   * The weird static data / default construction setup is a game we play with
-   * {@link Log}'s static initializer to prevent the initial log message from
-   * going to stderr.
-   */
-  public static class JettyTreeLogger implements Logger {
-    private final TreeLogger logger;
-
-    public JettyTreeLogger(TreeLogger logger) {
-      if (logger == null) {
-        throw new NullPointerException();
-      }
-      this.logger = logger;
-    }
-
-    public void debug(String msg, Object arg0, Object arg1) {
-      logger.log(TreeLogger.SPAM, format(msg, arg0, arg1));
-    }
-
-    public void debug(String msg, Throwable th) {
-      logger.log(TreeLogger.SPAM, msg, th);
-    }
-
-    public Logger getLogger(String name) {
-      return this;
-    }
-
-    public void info(String msg, Object arg0, Object arg1) {
-      logger.log(TreeLogger.INFO, format(msg, arg0, arg1));
-    }
-
-    public boolean isDebugEnabled() {
-      return logger.isLoggable(TreeLogger.SPAM);
-    }
-
-    public void setDebugEnabled(boolean enabled) {
-      // ignored
-    }
-
-    public void warn(String msg, Object arg0, Object arg1) {
-      logger.log(TreeLogger.WARN, format(msg, arg0, arg1));
-    }
-
-    public void warn(String msg, Throwable th) {
-      logger.log(TreeLogger.WARN, msg, th);
-    }
-
-    public void debug(String msg, long value) {
-      // ignored
-    }
-
-    @Override
-    public void debug(String msg, Object... args) {
-      // ignored
-    }
-
-    @Override
-    public void debug(Throwable thrown) {
-      // ignored
-    }
-
-    @Override
-    public void warn(String msg, Object... args) {
-      logger.log(TreeLogger.WARN, format(msg, args));
-    }
-
-    @Override
-    public void warn(Throwable thrown) {
-      logger.log(TreeLogger.WARN, thrown.getMessage(), thrown);
-    }
-
-    @Override
-    public void info(String msg, Object... args) {
-      logger.log(TreeLogger.INFO, format(msg, args));
-    }
-
-    @Override
-    public void info(Throwable thrown) {
-      logger.log(TreeLogger.INFO, thrown.getMessage(), thrown);
-    }
-
-    @Override
-    public void info(String msg, Throwable thrown) {
-      logger.log(TreeLogger.INFO, msg, thrown);
-    }
-
-    @Override
-    public void ignore(Throwable ignored) {
-    }
-
-    @Override
-    public String getName() {
-      return this.getClass().getName();
-    }
-
-    /**
-     * Copied from org.mortbay.log.StdErrLog.
-     */
-    private String format(String msg, Object arg0, Object arg1) {
-      int i0 = msg.indexOf("{}");
-      int i1 = i0 < 0 ? -1 : msg.indexOf("{}", i0 + 2);
-
-      if (arg1 != null && i1 >= 0) {
-        msg = msg.substring(0, i1) + arg1 + msg.substring(i1 + 2);
-      }
-      if (arg0 != null && i0 >= 0) {
-        msg = msg.substring(0, i0) + arg0 + msg.substring(i0 + 2);
-      }
-      return msg;
-    }
-
-    private String format(String msg, Object... args) {
-      StringBuilder builder = new StringBuilder();
-      if (msg == null) {
-          msg = "";
-          for (int i = 0; i < args.length; i++) {
-              msg += "{} ";
-          }
-      }
-      String braces = "{}";
-      int start = 0;
-      for (Object arg : args) {
-          int bracesIndex = msg.indexOf(braces,start);
-          if (bracesIndex < 0) {
-              escape(builder, msg.substring(start));
-              builder.append(" ");
-              builder.append(arg);
-              start = msg.length();
-          } else {
-              escape(builder, msg.substring(start, bracesIndex));
-              builder.append(String.valueOf(arg));
-              start = bracesIndex + braces.length();
-          }
-      }
-      escape(builder, msg.substring(start));
-      return builder.toString();
-    }
-
-    private void escape(StringBuilder builder, String string) {
-      if (__escape) {
-        for (int i = 0; i < string.length(); ++i) {
-          char c = string.charAt(i);
-          if (Character.isISOControl(c)) {
-            if (c == '\n') {
-              builder.append('|');
-            } else if (c == '\r') {
-              builder.append('<');
-            } else {
-              builder.append('?');
-            }
-          } else {
-            builder.append(c);
-          }
-        }
-      } else {
-        builder.append(string);
-      }
-    }
-  }
-
-  /**
-   * The resulting {@link ServletContainer} this is launched.
-   */
-  protected static class JettyServletContainer extends ServletContainer {
-    private final int actualPort;
-    private final File appRootDir;
-    private final TreeLogger logger;
-    private final Server server;
-    private final WebAppContext wac;
-
-    public JettyServletContainer(TreeLogger logger, Server server,
-        WebAppContext wac, int actualPort, File appRootDir) {
-      this.logger = logger;
-      this.server = server;
-      this.wac = wac;
-      this.actualPort = actualPort;
-      this.appRootDir = appRootDir;
-    }
-
-    @Override
-    public int getPort() {
-      return actualPort;
-    }
-
-    @Override
-    public void refresh() throws UnableToCompleteException {
-      String msg =
-          "Reloading web app to reflect changes in "
-              + appRootDir.getAbsolutePath();
-      TreeLogger branch = logger.branch(TreeLogger.INFO, msg);
-      // Temporarily log Jetty on the branch.
-      Log.setLog(new JettyTreeLogger(branch));
-      try {
-        wac.stop();
-        wac.start();
-        branch.log(TreeLogger.INFO, "Reload completed successfully");
-      } catch (Exception e) {
-        branch.log(TreeLogger.ERROR, "Unable to restart embedded Jetty server",
-            e);
-        throw new UnableToCompleteException();
-      } finally {
-        // Reset the top-level logger.
-        Log.setLog(new JettyTreeLogger(logger));
-      }
-    }
-
-    @Override
-    public void stop() throws UnableToCompleteException {
-      TreeLogger branch =
-          logger.branch(TreeLogger.INFO, "Stopping Jetty server");
-      // Temporarily log Jetty on the branch.
-      Log.setLog(new JettyTreeLogger(branch));
-      try {
-        server.stop();
-        server.setStopAtShutdown(false);
-        branch.log(TreeLogger.INFO, "Stopped successfully");
-      } catch (Exception e) {
-        branch.log(TreeLogger.ERROR, "Unable to stop embedded Jetty server", e);
-        throw new UnableToCompleteException();
-      } finally {
-        // Reset the top-level logger.
-        Log.setLog(new JettyTreeLogger(logger));
-      }
-    }
-  }
-
-  /**
-   * A {@link WebAppContext} tailored to GWT hosted mode. Features hot-reload
-   * with a new {@link WebAppClassLoader} to pick up disk changes. The default
-   * Jetty {@code WebAppContext} will create new instances of servlets, but it
-   * will not create a brand new {@link ClassLoader}. By creating a new {@code
-   * ClassLoader} each time, we re-read updated classes from disk.
-   *
-   * Also provides special class filtering to isolate the web app from the GWT
-   * hosting environment.
-   */
-  protected final class MyWebAppContext extends WebAppContext {
-    /**
-     * Parent ClassLoader for the Jetty web app, which can only load JVM
-     * classes. We would just use {@code null} for the parent ClassLoader
-     * except this makes Jetty unhappy.
-     */
-    private final ClassLoader bootStrapOnlyClassLoader =
-        new ClassLoader(null) {};
-
-    private final ClassLoader systemClassLoader =
-        Thread.currentThread().getContextClassLoader();
-
-    private MyWebAppContext(String webApp, String contextPath) {
-      super(webApp, contextPath);
-
-      // Prevent file locking on Windows; pick up file changes.
-      getInitParams().put(
-          "org.mortbay.jetty.servlet.Default.useFileMappedBuffer", "false");
-
-      // Since the parent class loader is bootstrap-only, prefer it first.
-      setParentLoaderPriority(true);
-    }
-
-    @Override
-    protected void doStart() throws Exception {
-      setClassLoader(new MyLoader(this));
-      super.doStart();
-    }
-
-    @Override
-    protected void doStop() throws Exception {
-      super.doStop();
-      setClassLoader(null);
-    }
-
-    private class MyLoader extends WebAppClassLoader {
-      MyWebAppContext ctx;
-      MyLoader(MyWebAppContext ctx) throws IOException {
-        super(bootStrapOnlyClassLoader, MyWebAppContext.this);
-        this.ctx = ctx;
-        final URLClassLoader scl = (URLClassLoader) systemClassLoader;
-        final URL[] urls = scl.getURLs();
-        for (URL u : urls) {
-          if ("file".equals(u.getProtocol())) {
-            addClassPath(u.getPath());
-          }
-        }
-      }
-
-      @Override
-      protected Class<?> findClass(String name) throws ClassNotFoundException {
-        // For system path, always prefer the outside world.
-        if (ctx.isSystemClass(name.replace('/', '.'))) {
-          try {
-            return systemClassLoader.loadClass(name);
-          } catch (ClassNotFoundException e) {
-          }
-        }
-        return super.findClass(name);
-      }
-    }
-  }
-
-  static {
-    Log.getLog();
-
-    /*
-     * Make JDT the default Ant compiler so that JSP compilation just works
-     * out-of-the-box. If we don't set this, it's very, very difficult to make
-     * JSP compilation work.
-     */
-    String antJavaC =
-        System.getProperty("build.compiler",
-            "org.eclipse.jdt.core.JDTCompilerAdapter");
-    System.setProperty("build.compiler", antJavaC);
-
-    System.setProperty("Gerrit.GwtDevMode", "" + true);
-  }
-
-  private String bindAddress = null;
-
-  @Override
-  public void setBindAddress(String bindAddress) {
-    this.bindAddress = bindAddress;
-  }
-
-  @Override
-  public ServletContainer start(TreeLogger logger, int port, File warDir)
-      throws Exception {
-    TreeLogger branch =
-        logger.branch(TreeLogger.INFO, "Starting Jetty on port " + port, null);
-    checkStartParams(branch, port, warDir);
-
-    // Setup our branch logger during startup.
-    Log.setLog(new JettyTreeLogger(branch));
-
-    // Turn off XML validation.
-    System.setProperty("org.mortbay.xml.XmlParser.Validating", "false");
-
-    Server server = new Server();
-    HttpConfiguration config = defaultConfig();
-    ServerConnector connector = new ServerConnector(server,
-        new HttpConnectionFactory(config));
-    if (bindAddress != null) {
-      connector.setHost(bindAddress);
-    }
-    connector.setPort(port);
-
-    // Don't share ports with an existing process.
-    connector.setReuseAddress(false);
-
-    // Linux keeps the port blocked after shutdown if we don't disable this.
-    connector.setSoLingerTime(0);
-
-
-    server.addConnector(connector);
-
-    File top;
-    String root = System.getProperty("gerrit.source_root");
-    if (root != null) {
-      top = new File(root);
-    } else {
-      // Under Maven warDir is "$top/gerrit-gwtui/target/gwt-hosted-mode"
-      top = warDir.getParentFile().getParentFile().getParentFile();
-    }
-
-    File app = new File(top, "gerrit-war/src/main/webapp");
-    File webxml = new File(app, "WEB-INF/web.xml");
-
-    // Jetty won't start unless this directory exists.
-    if (!warDir.exists() && !warDir.mkdirs())
-      logger.branch(TreeLogger.ERROR, "Cannot create "+warDir, null);
-
-    // Create a new web app in the war directory.
-    //
-    WebAppContext wac =
-        new MyWebAppContext(warDir.getAbsolutePath(), "/");
-    wac.setDescriptor(webxml.getAbsolutePath());
-
-    RequestLogHandler logHandler = new RequestLogHandler();
-    logHandler.setRequestLog(new JettyRequestLogger(logger));
-    logHandler.setHandler(wac);
-    server.setHandler(logHandler);
-    server.start();
-    server.setStopAtShutdown(true);
-
-    // Now that we're started, log to the top level logger.
-    Log.setLog(new JettyTreeLogger(logger));
-
-    return new JettyServletContainer(logger, server, wac,
-        connector.getLocalPort(), warDir);
-  }
-
-  protected HttpConfiguration defaultConfig() {
-    HttpConfiguration config = new HttpConfiguration();
-    config.setRequestHeaderSize(16386);
-    config.setSendServerVersion(false);
-    config.setSendDateHeader(true);
-    return config;
-  }
-
-  private void checkStartParams(TreeLogger logger, int port, File appRootDir) {
-    if (logger == null) {
-      throw new NullPointerException("logger cannot be null");
-    }
-
-    if (port < 0 || port > 65535) {
-      throw new IllegalArgumentException(
-          "port must be either 0 (for auto) or less than 65536");
-    }
-
-    if (appRootDir == null) {
-      throw new NullPointerException("app root direcotry cannot be null");
-    }
-  }
-}
diff --git a/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritGwtDebugLauncher.java b/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritGwtDebugLauncher.java
new file mode 100644
index 0000000..4282534
--- /dev/null
+++ b/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritGwtDebugLauncher.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.gwtdebug;
+
+import com.google.gerrit.pgm.Daemon;
+import com.google.gwt.dev.codeserver.CodeServer;
+import com.google.gwt.dev.codeserver.Options;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class GerritGwtDebugLauncher {
+  private static final Logger log = LoggerFactory.getLogger(GerritGwtDebugLauncher.class);
+
+  public static void main(String[] argv) throws Exception {
+    GerritGwtDebugLauncher launcher = new GerritGwtDebugLauncher();
+    launcher.mainImpl(argv);
+  }
+
+  private int mainImpl(String[] argv) {
+    List<String> sdmLauncherOptions = new ArrayList<>();
+    List<String> daemonLauncherOptions = new ArrayList<>();
+
+    // Separator between Daemon and Codeserver parameters is "--"
+    boolean daemonArgumentSeparator = false;
+    int i = 0;
+    for (; i < argv.length; i++) {
+      if (!argv[i].equals("--")) {
+        sdmLauncherOptions.add(argv[i]);
+      } else {
+        daemonArgumentSeparator = true;
+        break;
+      }
+    }
+    if (daemonArgumentSeparator) {
+      ++i;
+      for (; i < argv.length; i++) {
+        daemonLauncherOptions.add(argv[i]);
+      }
+    }
+
+    Options options = new Options();
+    if (!options.parseArgs(sdmLauncherOptions.toArray(
+        new String[sdmLauncherOptions.size()]))) {
+      log.error("Failed to parse codeserver arguments");
+      return 1;
+    }
+
+    CodeServer.main(options);
+
+    try {
+      int r = new Daemon().main(daemonLauncherOptions.toArray(
+          new String[daemonLauncherOptions.size()]));
+      if (r != 0) {
+        log.error("Daemon exited with return code: " + r);
+        return 1;
+      }
+    } catch (Exception e) {
+      log.error("Cannot start daemon", e);
+      return 1;
+    }
+
+    return 0;
+  }
+}
diff --git a/gerrit-gwtdebug/src/main/java/com/google/gwt/dev/codeserver/WebServer.java b/gerrit-gwtdebug/src/main/java/com/google/gwt/dev/codeserver/WebServer.java
new file mode 100644
index 0000000..8072d75
--- /dev/null
+++ b/gerrit-gwtdebug/src/main/java/com/google/gwt/dev/codeserver/WebServer.java
@@ -0,0 +1,541 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * 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.gwt.dev.codeserver;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.TreeLogger.Type;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.codeserver.CompileDir.PolicyFile;
+import com.google.gwt.dev.codeserver.Pages.ErrorPage;
+import com.google.gwt.dev.json.JsonObject;
+
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.server.HttpConnection;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.servlets.GzipFilter;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.servlet.DispatcherType;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * The web server for Super Dev Mode, also known as the code server. The URLs handled include:
+ * <ul>
+ *   <li>HTML pages for the front page and module pages</li>
+ *   <li>JavaScript that implementing the bookmarklets</li>
+ *   <li>The web API for recompiling a GWT app</li>
+ *   <li>The output files and log files from the GWT compiler</li>
+ *   <li>Java source code (for source-level debugging)</li>
+ * </ul>
+ *
+ * <p>EXPERIMENTAL. There is no authentication, encryption, or XSS protection, so this server is
+ * only safe to run on localhost.</p>
+ */
+// This file was copied from GWT project and adjusted to run against
+// Jetty 9.2.2. The original diff can be found here:
+// https://gwt-review.googlesource.com/#/c/7857/13/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java
+public class WebServer {
+
+  private static final Pattern SAFE_DIRECTORY =
+      Pattern.compile("([a-zA-Z0-9_-]+\\.)*[a-zA-Z0-9_-]+"); // no extension needed
+
+  private static final Pattern SAFE_FILENAME =
+      Pattern.compile("([a-zA-Z0-9_-]+\\.)+[a-zA-Z0-9_-]+"); // an extension is required
+
+  private static final Pattern SAFE_MODULE_PATH =
+      Pattern.compile("/(" + SAFE_DIRECTORY + ")/$");
+
+  static final Pattern SAFE_DIRECTORY_PATH =
+      Pattern.compile("/(" + SAFE_DIRECTORY + "/)+$");
+
+  /* visible for testing */
+  static final Pattern SAFE_FILE_PATH =
+      Pattern.compile("/(" + SAFE_DIRECTORY + "/)+" + SAFE_FILENAME + "$");
+
+  static final Pattern STRONG_NAME = Pattern.compile("[\\dA-F]{32}");
+
+  private static final Pattern CACHE_JS_FILE = Pattern.compile("/(" + STRONG_NAME + ").cache.js$");
+
+  private static final MimeTypes MIME_TYPES = new MimeTypes();
+
+  private static final String TIME_IN_THE_PAST = "Fri, 01 Jan 1990 00:00:00 GMT";
+
+  private final SourceHandler handler;
+  private final JsonExporter jsonExporter;
+  private final OutboxTable outboxes;
+  private final JobRunner runner;
+  private final JobEventTable eventTable;
+
+  private final String bindAddress;
+  private final int port;
+
+  private Server server;
+
+  WebServer(SourceHandler handler, JsonExporter jsonExporter, OutboxTable outboxes,
+      JobRunner runner, JobEventTable eventTable, String bindAddress, int port) {
+    this.handler = handler;
+    this.jsonExporter = jsonExporter;
+    this.outboxes = outboxes;
+    this.runner = runner;
+    this.eventTable = eventTable;
+    this.bindAddress = bindAddress;
+    this.port = port;
+  }
+
+  @SuppressWarnings("serial")
+  void start(final TreeLogger logger) throws UnableToCompleteException {
+
+    Server newServer = new Server();
+    ServerConnector connector = new ServerConnector(newServer);
+    connector.setHost(bindAddress);
+    connector.setPort(port);
+    connector.setReuseAddress(false);
+    connector.setSoLingerTime(0);
+
+    newServer.addConnector(connector);
+
+    ServletContextHandler newHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
+    newHandler.setContextPath("/");
+    newHandler.addServlet(new ServletHolder(new HttpServlet() {
+      @Override
+      protected void doGet(HttpServletRequest request, HttpServletResponse response)
+          throws ServletException, IOException {
+        handleRequest(request.getPathInfo(), request, response, logger);
+      }
+    }), "/*");
+    newHandler.addFilter(GzipFilter.class, "/*", EnumSet.allOf(DispatcherType.class));
+    newServer.setHandler(newHandler);
+    try {
+      newServer.start();
+    } catch (Exception e) {
+      logger.log(TreeLogger.ERROR, "cannot start web server", e);
+      throw new UnableToCompleteException();
+    }
+    this.server = newServer;
+  }
+
+  public int getPort() {
+    return port;
+  }
+
+  public void stop() throws Exception {
+    server.stop();
+    server = null;
+  }
+
+  /**
+   * Returns the location of the compiler output. (Changes after every recompile.)
+   * @param outputModuleName the module name that the GWT compiler used in its output.
+   */
+  public File getCurrentWarDir(String outputModuleName) {
+    return outboxes.findByOutputModuleName(outputModuleName).getWarDir();
+  }
+
+  private void handleRequest(String target, HttpServletRequest request,
+      HttpServletResponse response, TreeLogger parentLogger)
+      throws IOException {
+
+    if (request.getMethod().equalsIgnoreCase("get")) {
+
+      TreeLogger logger = parentLogger.branch(Type.TRACE, "GET " + target);
+
+      Response page = doGet(target, request, logger);
+      if (page == null) {
+        logger.log(Type.WARN, "not handled: " + target);
+        return;
+      }
+
+      setHandled(request);
+      if (!target.endsWith(".cache.js")) {
+        // Make sure IE9 doesn't cache any pages.
+        // (Nearly all pages may change on server restart.)
+        response.setHeader("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate");
+        response.setHeader("Pragma", "no-cache");
+        response.setHeader("Expires", TIME_IN_THE_PAST);
+        response.setDateHeader("Date", new Date().getTime());
+      }
+      page.send(request, response, logger);
+    }
+  }
+
+  /**
+   * Returns the page that should be sent in response to a GET request, or null for no response.
+   */
+  private Response doGet(String target, HttpServletRequest request, TreeLogger logger)
+      throws IOException {
+
+    if (target.equals("/")) {
+      JsonObject json = jsonExporter.exportFrontPageVars();
+      return Pages.newHtmlPage("config", json, "frontpage.html");
+    }
+
+    if (target.equals("/dev_mode_on.js")) {
+      JsonObject json = jsonExporter.exportDevModeOnVars();
+      return Responses.newJavascriptResponse("__gwt_codeserver_config", json,
+          "dev_mode_on.js");
+    }
+
+    // Recompile on request from the bookmarklet.
+    // This is a GET because a bookmarklet can call it from a different origin (JSONP).
+    if (target.startsWith("/recompile/")) {
+      String moduleName = target.substring("/recompile/".length());
+      Outbox box = outboxes.findByOutputModuleName(moduleName);
+      if (box == null) {
+        return new ErrorPage("No such module: " + moduleName);
+      }
+
+      // We are passing properties from an unauthenticated GET request directly to the compiler.
+      // This should be safe, but only because these are binding properties. For each binding
+      // property, you can only choose from a set of predefined values. So all an attacker can do is
+      // cause a spurious recompile, resulting in an unexpected permutation being loaded later.
+      //
+      // It would be unsafe to allow a configuration property to be changed.
+      Job job = box.makeJob(getBindingProperties(request), logger);
+      runner.submit(job);
+      Job.Result result = job.waitForResult();
+      JsonObject json = jsonExporter.exportRecompileResponse(result);
+      return Responses.newJsonResponse(json);
+    }
+
+    if (target.startsWith("/log/")) {
+      String moduleName = target.substring("/log/".length());
+      Outbox box = outboxes.findByOutputModuleName(moduleName);
+      if (box == null) {
+        return new ErrorPage("No such module: " + moduleName);
+      } else if (box.containsStubCompile()) {
+        return new ErrorPage("This module hasn't been compiled yet.");
+      } else {
+        return makeLogPage(box);
+      }
+    }
+
+    if (target.equals("/favicon.ico")) {
+      InputStream faviconStream = getClass().getResourceAsStream("favicon.ico");
+      if (faviconStream == null) {
+        return new ErrorPage("icon not found");
+      }
+      // IE8 will not load the favicon in an img tag with the default MIME type,
+      // so use "image/x-icon" instead.
+      return Responses.newBinaryStreamResponse("image/x-icon", faviconStream);
+    }
+
+    if (target.equals("/policies/")) {
+      return makePolicyIndexPage();
+    }
+
+    if (target.equals("/progress")) {
+      // TODO: return a list of progress objects here, one for each job.
+      JobEvent event = eventTable.getCompilingJobEvent();
+
+      JsonObject json;
+      if (event == null) {
+        json = new JsonObject();
+        json.put("status", "idle");
+      } else {
+        json = jsonExporter.exportProgressResponse(event);
+      }
+      return Responses.newJsonResponse(json);
+    }
+
+    Matcher matcher = SAFE_MODULE_PATH.matcher(target);
+    if (matcher.matches()) {
+      return makeModulePage(matcher.group(1));
+    }
+
+    matcher = SAFE_DIRECTORY_PATH.matcher(target);
+    if (matcher.matches() && SourceHandler.isSourceMapRequest(target)) {
+      return handler.handle(target, request, logger);
+    }
+
+    matcher = SAFE_FILE_PATH.matcher(target);
+    if (matcher.matches()) {
+      if (SourceHandler.isSourceMapRequest(target)) {
+        return handler.handle(target, request, logger);
+      }
+      if (target.startsWith("/policies/")) {
+        return makePolicyFilePage(target);
+      }
+      return makeCompilerOutputPage(target);
+    }
+
+    logger.log(TreeLogger.WARN, "ignored get request: " + target);
+    return null; // not handled
+  }
+
+  /**
+   * Returns a file that the compiler wrote to its war directory.
+   */
+  private Response makeCompilerOutputPage(String target) {
+
+    int secondSlash = target.indexOf('/', 1);
+    String moduleName = target.substring(1, secondSlash);
+    Outbox box = outboxes.findByOutputModuleName(moduleName);
+    if (box == null) {
+      return new ErrorPage("No such module: " + moduleName);
+    }
+
+    final String contentEncoding;
+    File file = box.getOutputFile(target);
+    if (!file.isFile()) {
+      // perhaps it's compressed
+      file = box.getOutputFile(target + ".gz");
+      if (!file.isFile()) {
+        return new ErrorPage("not found: " + file.toString());
+      }
+      contentEncoding = "gzip";
+    } else {
+      contentEncoding = null;
+    }
+
+    final String sourceMapUrl;
+    Matcher match = CACHE_JS_FILE.matcher(target);
+    if (match.matches()) {
+      String strongName = match.group(1);
+      String template = SourceHandler.sourceMapLocationTemplate(moduleName);
+      sourceMapUrl = template.replace("__HASH__", strongName);
+    } else {
+      sourceMapUrl = null;
+    }
+
+    String mimeType = guessMimeType(target);
+    final Response barePage = Responses.newFileResponse(mimeType, file);
+
+    // Wrap the response to send the extra headers.
+    return new Response() {
+      @Override
+      public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
+          throws IOException {
+        // TODO: why do we need this? Looks like Ray added it a long time ago.
+        response.setHeader("Access-Control-Allow-Origin", "*");
+
+        if (sourceMapUrl != null) {
+          response.setHeader("X-SourceMap", sourceMapUrl);
+          response.setHeader("SourceMap", sourceMapUrl);
+        }
+
+        if (contentEncoding != null) {
+          if (!request.getHeader("Accept-Encoding").contains("gzip")) {
+            response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
+            logger.log(TreeLogger.WARN, "client doesn't accept gzip; bailing");
+            return;
+          }
+          response.setHeader("Content-Encoding", "gzip");
+        }
+
+        barePage.send(request, response, logger);
+      }
+    };
+  }
+
+  private Response makeModulePage(String moduleName) {
+    Outbox box = outboxes.findByOutputModuleName(moduleName);
+    if (box == null) {
+      return new ErrorPage("No such module: " + moduleName);
+    }
+
+    JsonObject json = jsonExporter.exportModulePageVars(box);
+    return Pages.newHtmlPage("config", json, "modulepage.html");
+  }
+
+  private Response makePolicyIndexPage() {
+
+    return new Response() {
+
+      @Override
+      public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
+          throws IOException {
+        response.setContentType("text/html");
+
+        HtmlWriter out = new HtmlWriter(response.getWriter());
+
+        out.startTag("html").nl();
+        out.startTag("head").nl();
+        out.startTag("title").text("Policy Files").endTag("title").nl();
+        out.endTag("head");
+        out.startTag("body");
+
+        out.startTag("h1").text("Policy Files").endTag("h1").nl();
+
+        for (Outbox box : outboxes.getOutboxes()) {
+          List<PolicyFile> policies = box.readRpcPolicyManifest();
+          if (!policies.isEmpty()) {
+            out.startTag("h2").text(box.getOutputModuleName()).endTag("h2").nl();
+
+            out.startTag("table").nl();
+            for (PolicyFile policy : policies) {
+
+              out.startTag("tr");
+
+              out.startTag("td");
+
+              out.startTag("a", "href=", policy.getServiceSourceUrl());
+              out.text(policy.getServiceName());
+              out.endTag("a");
+
+              out.endTag("td");
+
+              out.startTag("td");
+
+              out.startTag("a", "href=", policy.getUrl());
+              out.text(policy.getName());
+              out.endTag("a");
+
+              out.endTag("td");
+
+              out.endTag("tr").nl();
+            }
+            out.endTag("table").nl();
+          }
+        }
+
+        out.endTag("body").nl();
+        out.endTag("html").nl();
+      }
+    };
+  }
+
+  private Response makePolicyFilePage(String target) {
+
+    int secondSlash = target.indexOf('/', 1);
+    if (secondSlash < 1) {
+      return new ErrorPage("invalid URL for policy file: " + target);
+    }
+
+    String rest = target.substring(secondSlash + 1);
+    if (rest.contains("/") || !rest.endsWith(".gwt.rpc")) {
+      return new ErrorPage("invalid name for policy file: " + rest);
+    }
+
+    File fileToSend = outboxes.findPolicyFile(rest);
+    if (fileToSend == null) {
+      return new ErrorPage("Policy file not found: " + rest);
+    }
+
+    return Responses.newFileResponse("text/plain", fileToSend);
+  }
+
+  /**
+   * Sends the log file as html with errors highlighted in red.
+   */
+  private Response makeLogPage(final Outbox box) {
+    final File file = box.getCompileLog();
+    if (!file.isFile()) {
+      return new ErrorPage("log file not found");
+    }
+
+    return new Response() {
+
+      @Override
+      public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
+          throws IOException {
+        BufferedReader reader = new BufferedReader(new FileReader(file));
+
+        response.setStatus(HttpServletResponse.SC_OK);
+        response.setContentType("text/html");
+        response.setHeader("Content-Style-Type", "text/css");
+
+        HtmlWriter out = new HtmlWriter(response.getWriter());
+        out.startTag("html").nl();
+        out.startTag("head").nl();
+        out.startTag("title").text(box.getOutputModuleName() + " compile log").endTag("title").nl();
+        out.startTag("style").nl();
+        out.text(".error { color: red; font-weight: bold; }").nl();
+        out.endTag("style").nl();
+        out.endTag("head").nl();
+        out.startTag("body").nl();
+        sendLogAsHtml(reader, out);
+        out.endTag("body").nl();
+        out.endTag("html").nl();
+      }
+    };
+  }
+
+  private static final Pattern ERROR_PATTERN = Pattern.compile("\\[ERROR\\]");
+
+  /**
+   * Copies in to out line by line, escaping each line for html characters and highlighting
+   * error lines. Closes <code>in</code> when done.
+   */
+  private static void sendLogAsHtml(BufferedReader in, HtmlWriter out) throws IOException {
+    try {
+      out.startTag("pre").nl();
+      String line = in.readLine();
+      while (line != null) {
+        Matcher m = ERROR_PATTERN.matcher(line);
+        boolean error = m.find();
+        if (error) {
+          out.startTag("span", "class=", "error");
+        }
+        out.text(line);
+        if (error) {
+          out.endTag("span");
+        }
+        out.nl(); // the readLine doesn't include the newline.
+        line = in.readLine();
+      }
+      out.endTag("pre").nl();
+    } finally {
+      in.close();
+    }
+  }
+
+  /* visible for testing */
+  static String guessMimeType(String filename) {
+    String mimeType = MIME_TYPES.getMimeByExtension(filename);
+    return mimeType != null ? mimeType : "";
+  }
+
+  /**
+   * Returns the binding properties from the web page where dev mode is being used. (As passed in
+   * by dev_mode_on.js in a JSONP request to "/recompile".)
+   */
+  private Map<String, String> getBindingProperties(HttpServletRequest request) {
+    Map<String, String> result = new HashMap<>();
+    for (Object key : request.getParameterMap().keySet()) {
+      String propName = (String) key;
+      if (!propName.equals("_callback")) {
+        result.put(propName, request.getParameter(propName));
+      }
+    }
+    return result;
+  }
+
+  private static void setHandled(HttpServletRequest request) {
+    Request baseRequest = (request instanceof Request) ? (Request) request :
+        HttpConnection.getCurrentConnection().getHttpChannel().getRequest();
+    baseRequest.setHandled(true);
+  }
+}
diff --git a/gerrit-gwtexpui/BUCK b/gerrit-gwtexpui/BUCK
index e06bf17..b266e126 100644
--- a/gerrit-gwtexpui/BUCK
+++ b/gerrit-gwtexpui/BUCK
@@ -8,10 +8,10 @@
     SRC + 'clippy/client/clippy.css',
     SRC + 'clippy/client/clippy.swf',
   ],
+  provided_deps = ['//lib/gwt:user'],
   deps = [
     ':SafeHtml',
     ':UserAgent',
-    '//lib/gwt:user',
     '//lib:LICENSE-clippy',
   ],
   visibility = ['PUBLIC'],
@@ -21,7 +21,7 @@
   name = 'CSS',
   srcs = glob([SRC + 'css/rebind/*.java']),
   resources = [SRC + 'css/CSS.gwt.xml'],
-  deps = ['//lib/gwt:dev'],
+  provided_deps = ['//lib/gwt:dev'],
   visibility = ['PUBLIC'],
 )
 
@@ -33,10 +33,10 @@
     SRC + 'globalkey/client/KeyConstants.properties',
     SRC + 'globalkey/client/key.css',
   ],
+  provided_deps = ['//lib/gwt:user'],
   deps = [
     ':SafeHtml',
     ':UserAgent',
-    '//lib/gwt:user',
   ],
   visibility = ['PUBLIC'],
 )
@@ -53,7 +53,7 @@
   srcs = glob([SRC + 'progress/client/*.java']),
   gwt_xml = SRC + 'progress/Progress.gwt.xml',
   resources = [SRC + 'progress/client/progress.css'],
-  deps = ['//lib/gwt:user'],
+  provided_deps = ['//lib/gwt:user'],
   visibility = ['PUBLIC'],
 )
 
@@ -62,7 +62,7 @@
   srcs = glob([SRC + 'safehtml/client/*.java']),
   gwt_xml = SRC + 'safehtml/SafeHtml.gwt.xml',
   resources = [SRC + 'safehtml/client/safehtml.css'],
-  deps = ['//lib/gwt:user'],
+  provided_deps = ['//lib/gwt:user'],
   visibility = ['PUBLIC'],
 )
 
@@ -84,7 +84,7 @@
   name = 'UserAgent',
   srcs = glob([SRC + 'user/client/*.java']),
   gwt_xml = SRC + 'user/User.gwt.xml',
-  deps = ['//lib/gwt:user'],
+  provided_deps = ['//lib/gwt:user'],
   visibility = ['PUBLIC'],
 )
 
@@ -94,3 +94,17 @@
   provided_deps = ['//lib:servlet-api-3_1'],
   visibility = ['PUBLIC'],
 )
+
+java_library(
+  name = 'client-src-lib',
+  srcs = [],
+  resources = glob(
+    [SRC + n for n in [
+      'clippy/**/*',
+      'globalkey/**/*',
+      'safehtml/**/*',
+      'user/**/*',
+    ]]
+  ),
+  visibility = ['PUBLIC'],
+)
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
index a0392f8..e34814f 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
@@ -160,10 +160,12 @@
     }
   }
 
+  @Override
   public String getText() {
     return text;
   }
 
+  @Override
   public void setText(final String newText) {
     text = newText;
     visibleLen = newText.length();
@@ -195,6 +197,7 @@
                   @Override
                   public void onKeyUp(final KeyUpEvent event) {
                     Scheduler.get().scheduleDeferred(new Command() {
+                      @Override
                       public void execute() {
                         hideTextBox();
                       }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpFlowPanel.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpFlowPanel.java
deleted file mode 100644
index 7ae7fd0..0000000
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpFlowPanel.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gwtexpui.globalkey.client;
-
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.user.client.ui.FlowPanel;
-
-public class NpFlowPanel extends FlowPanel {
-  public NpFlowPanel() {
-    addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
-  }
-}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java
index 5bb3c1e..eb87e7f 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/linker/server/UserAgentRule.java
@@ -14,9 +14,10 @@
 
 package com.google.gwtexpui.linker.server;
 
+import static java.util.regex.Pattern.compile;
+
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import static java.util.regex.Pattern.compile;
 
 import javax.servlet.http.HttpServletRequest;
 
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java
index b08b29f..6eaa7fd 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java
@@ -107,11 +107,13 @@
   }
 
   private static class AnyTag implements Tag {
+    @Override
     public void assertSafe(String name, String value) {
     }
   }
 
   private static class AnchorTag implements Tag {
+    @Override
     public void assertSafe(String name, String value) {
       if ("href".equals(name)) {
         assertNotJavascriptUrl(value);
@@ -120,6 +122,7 @@
   }
 
   private static class FormTag implements Tag {
+    @Override
     public void assertSafe(String name, String value) {
       if ("action".equals(name)) {
         assertNotJavascriptUrl(value);
@@ -128,6 +131,7 @@
   }
 
   private static class SrcTag implements Tag {
+    @Override
     public void assertSafe(String name, String value) {
       if ("src".equals(name)) {
         assertNotJavascriptUrl(value);
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/Buffer.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/Buffer.java
index d79c580..12389b4 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/Buffer.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/Buffer.java
@@ -29,5 +29,6 @@
 
   void append(String v);
 
+  @Override
   String toString();
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java
index a1801ad..83abd5d 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java
@@ -21,30 +21,37 @@
     return strbuf.length() == 0;
   }
 
+  @Override
   public void append(final boolean v) {
     strbuf.append(v);
   }
 
+  @Override
   public void append(final char v) {
     strbuf.append(v);
   }
 
+  @Override
   public void append(final int v) {
     strbuf.append(v);
   }
 
+  @Override
   public void append(final long v) {
     strbuf.append(v);
   }
 
+  @Override
   public void append(final float v) {
     strbuf.append(v);
   }
 
+  @Override
   public void append(final double v) {
     strbuf.append(v);
   }
 
+  @Override
   public void append(final String v) {
     strbuf.append(v);
   }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java
index 6b5346d..e3aed55 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java
@@ -21,30 +21,37 @@
     shb = safeHtmlBuilder;
   }
 
+  @Override
   public void append(final boolean v) {
     shb.sealElement().append(v);
   }
 
+  @Override
   public void append(final char v) {
     shb.sealElement().append(v);
   }
 
+  @Override
   public void append(final double v) {
     shb.sealElement().append(v);
   }
 
+  @Override
   public void append(final float v) {
     shb.sealElement().append(v);
   }
 
+  @Override
   public void append(final int v) {
     shb.sealElement().append(v);
   }
 
+  @Override
   public void append(final long v) {
     shb.sealElement().append(v);
   }
 
+  @Override
   public void append(final String v) {
     shb.sealElement().append(v);
   }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
index 216add1..1ca688b 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
@@ -41,6 +41,7 @@
   @Override
   public final void requestSuggestions(final Request request, final Callback cb) {
     onRequestSuggestions(request, new Callback() {
+      @Override
       public void onSuggestionsReady(final Request request,
           final Response response) {
         final String qpat = getQueryPattern(request.getQuery());
@@ -99,10 +100,12 @@
     private static native String sgi(String inString, String pat, String newHtml)
     /*-{ return inString.replace(RegExp(pat, 'gi'), newHtml); }-*/;
 
+    @Override
     public String getDisplayString() {
       return displayString;
     }
 
+    @Override
     public String getReplacementString() {
       return suggestion.getReplacementString();
     }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
index a71120a..b8f0800 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
@@ -44,26 +44,32 @@
         @Override
         public SafeHtmlCss css() {
           return new SafeHtmlCss() {
+            @Override
             public String wikiList() {
               return "wikiList";
             }
 
+            @Override
             public String wikiPreFormat() {
               return "wikiPreFormat";
             }
 
+            @Override
             public String wikiQuote() {
               return "wikiQuote";
             }
 
+            @Override
             public boolean ensureInjected() {
               return false;
             }
 
+            @Override
             public String getName() {
               return null;
             }
 
+            @Override
             public String getText() {
               return null;
             }
@@ -335,5 +341,6 @@
   }
 
   /** @return a clean HTML string safe for inclusion in any context. */
+  @Override
   public abstract String asString();
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
index 75337ac..69da38d 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
@@ -402,7 +402,7 @@
     return isElementName(name);
   }
 
-  private static abstract class Impl {
+  private abstract static class Impl {
     abstract void escapeStr(SafeHtmlBuilder b, String in);
   }
 
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java
index 54bfcd0..a438caf 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java
@@ -49,12 +49,15 @@
  * </pre>
  */
 public class CacheControlFilter implements Filter {
+  @Override
   public void init(final FilterConfig config) {
   }
 
+  @Override
   public void destroy() {
   }
 
+  @Override
   public void doFilter(final ServletRequest sreq, final ServletResponse srsp,
       final FilterChain chain) throws IOException, ServletException {
     final HttpServletRequest req = (HttpServletRequest) sreq;
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java
index bc20a9d..182eac3 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/RawFindReplaceTest.java
@@ -14,9 +14,10 @@
 
 package com.google.gwtexpui.safehtml.client;
 
-import org.junit.Test;
 import static org.junit.Assert.assertEquals;
 
+import org.junit.Test;
+
 public class RawFindReplaceTest {
   @Test
   public void testFindReplace() {
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java
index 75c3745..f89c62b 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_LinkifyTest.java
@@ -14,11 +14,11 @@
 
 package com.google.gwtexpui.safehtml.client;
 
-import org.junit.Test;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotSame;
 
+import org.junit.Test;
+
 public class SafeHtml_LinkifyTest {
   @Test
   public void testLinkify_SimpleHttp1() {
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java
index 71b55a1..4fa6254 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java
@@ -14,16 +14,16 @@
 
 package com.google.gwtexpui.safehtml.client;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+
 import org.junit.Test;
 
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertSame;
-
 public class SafeHtml_ReplaceTest {
   @Test
   public void testReplaceEmpty() {
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java
index 045555a..ea91ee3 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyListTest.java
@@ -14,11 +14,11 @@
 
 package com.google.gwtexpui.safehtml.client;
 
-import org.junit.Test;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotSame;
 
+import org.junit.Test;
+
 public class SafeHtml_WikifyListTest {
   private static final String BEGIN_LIST = "<ul class=\"wikiList\">";
   private static final String END_LIST = "</ul>";
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java
index 605185e..57399dc 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java
@@ -14,11 +14,11 @@
 
 package com.google.gwtexpui.safehtml.client;
 
-import org.junit.Test;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotSame;
 
+import org.junit.Test;
+
 public class SafeHtml_WikifyPreformatTest {
   private static final String B = "<span class=\"wikiPreFormat\">";
   private static final String E = "</span><br />";
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyQuoteTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyQuoteTest.java
index d6fba26..f6b6b91 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyQuoteTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyQuoteTest.java
@@ -14,11 +14,11 @@
 
 package com.google.gwtexpui.safehtml.client;
 
-import org.junit.Test;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotSame;
 
+import org.junit.Test;
+
 public class SafeHtml_WikifyQuoteTest {
   private static final String B = "<blockquote class=\"wikiQuote\">";
   private static final String E = "</blockquote>";
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java
index 00b29de..3c261d0 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyTest.java
@@ -14,11 +14,11 @@
 
 package com.google.gwtexpui.safehtml.client;
 
-import org.junit.Test;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotSame;
 
+import org.junit.Test;
+
 public class SafeHtml_WikifyTest {
   @Test
   public void testWikify_OneLine1() {
diff --git a/gerrit-gwtui-common/BUCK b/gerrit-gwtui-common/BUCK
index f4c2358..2eb0684 100644
--- a/gerrit-gwtui-common/BUCK
+++ b/gerrit-gwtui-common/BUCK
@@ -1,9 +1,11 @@
 SRC = 'src/main/java/com/google/gerrit/'
+DIFFY = glob(['src/main/resources/com/google/gerrit/client/diffy*.png'])
 
 gwt_module(
   name = 'client',
   srcs = glob([SRC + 'client/**/*.java']),
   gwt_xml = SRC + 'GerritGwtUICommon.gwt.xml',
+  resources = glob(['src/main/**/*']),
   deps = ['//lib/gwt:user'],
   visibility = ['PUBLIC'],
 )
@@ -28,3 +30,25 @@
   resources = glob(['src/main/**/*']),
   visibility = ['PUBLIC'],
 )
+
+prebuilt_jar(
+  name = 'diffy_logo',
+  binary_jar = ':diffy_image_files_ln',
+  deps = [
+    '//lib:LICENSE-diffy',
+    '//lib:LICENSE-CC-BY3.0',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+genrule(
+  name = 'diffy_image_files_ln',
+  cmd = 'ln -s $(location :diffy_image_files) $OUT',
+  deps = [':diffy_image_files'],
+  out = 'diffy_images.jar',
+)
+
+java_library(
+  name = 'diffy_image_files',
+  resources = DIFFY,
+)
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
new file mode 100644
index 0000000..c4e2167
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
@@ -0,0 +1,107 @@
+// Copyright (C) 2008 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client;
+
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.ImageResource;
+
+public interface Resources extends ClientBundle {
+  @Source("arrowRight.png")
+  public ImageResource arrowRight();
+
+  @Source("arrowUp.png")
+  public ImageResource arrowUp();
+
+  @Source("arrowDown.png")
+  public ImageResource arrowDown();
+
+  @Source("editText.png")
+  public ImageResource edit();
+
+  @Source("mediaFloppy.png")
+  public ImageResource save();
+
+  @Source("starOpen.png")
+  public ImageResource starOpen();
+
+  @Source("starFilled.png")
+  public ImageResource starFilled();
+
+  @Source("greenCheck.png")
+  public ImageResource greenCheck();
+
+  @Source("redNot.png")
+  public ImageResource redNot();
+
+  @Source("editUndo.png")
+  public ImageResource editUndo();
+
+  @Source("downloadIcon.png")
+  public ImageResource downloadIcon();
+
+  @Source("queryIcon.png")
+  public ImageResource queryIcon();
+
+  @Source("addFileComment.png")
+  public ImageResource addFileComment();
+
+  @Source("diffy26.png")
+  public ImageResource gerritAvatar26();
+
+  @Source("draftComments.png")
+  public ImageResource draftComments();
+
+  @Source("readOnly.png")
+  public ImageResource readOnly();
+
+  @Source("gear.png")
+  public ImageResource gear();
+
+  @Source("info.png")
+  public ImageResource info();
+
+  @Source("warning.png")
+  public ImageResource warning();
+
+  @Source("listAdd.png")
+  public ImageResource listAdd();
+
+  @Source("merge.png")
+  public ImageResource merge();
+
+  @Source("deleteNormal.png")
+  public ImageResource deleteNormal();
+
+  @Source("deleteHover.png")
+  public ImageResource deleteHover();
+
+  @Source("undoNormal.png")
+  public ImageResource undoNormal();
+
+  @Source("goPrev.png")
+  public ImageResource goPrev();
+
+  @Source("goNext.png")
+  public ImageResource goNext();
+
+  @Source("goUp.png")
+  public ImageResource goUp();
+
+  @Source("sideBySideDiff.png")
+  public ImageResource sideBySideDiff();
+
+  @Source("unifiedDiff.png")
+  public ImageResource unifiedDiff();
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/addFileComment.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/addFileComment.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/addFileComment.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/addFileComment.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/arrowDown.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrowDown.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/arrowDown.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrowDown.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrowRight.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrowRight.png
new file mode 100644
index 0000000..8549f5d
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrowRight.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/arrowUp.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrowUp.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/arrowUp.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/arrowUp.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/removeReviewerNormal.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/deleteHover.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/removeReviewerNormal.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/deleteHover.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/deleteNormal.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/deleteNormal.png
new file mode 100644
index 0000000..47a1195
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/deleteNormal.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy100.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/diffy100.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy100.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/diffy100.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy26.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/diffy26.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy26.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/diffy26.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/downloadIcon.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/downloadIcon.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/downloadIcon.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/downloadIcon.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/draftComments.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/draftComments.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/draftComments.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/draftComments.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editText.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/editText.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/editText.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/editText.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/editUndo.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/editUndo.png
new file mode 100644
index 0000000..4790e10
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/editUndo.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gear.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/gear.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/gear.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/gear.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/go-next.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goNext.png
similarity index 100%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/go-next.png
copy to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goNext.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/go-prev.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goPrev.png
similarity index 100%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/go-prev.png
copy to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goPrev.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/go-up.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goUp.png
similarity index 100%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/go-up.png
copy to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/goUp.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/greenCheck.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/greenCheck.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/greenCheck.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/greenCheck.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/info.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/info.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/info.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/info.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/listAdd.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/listAdd.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/listAdd.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/listAdd.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/mediaFloppy.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/mediaFloppy.png
new file mode 100644
index 0000000..f1d7a19
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/mediaFloppy.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/merge.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/merge.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/merge.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/merge.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/queryIcon.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/queryIcon.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/queryIcon.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/queryIcon.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/readOnly.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/readOnly.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/readOnly.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/readOnly.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/redNot.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/redNot.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/redNot.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/redNot.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/sideBySideDiff.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/sideBySideDiff.png
new file mode 100755
index 0000000..ee70080
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/sideBySideDiff.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/star_filled.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/starFilled.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/change/star_filled.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/starFilled.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/star_open.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/starOpen.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/change/star_open.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/starOpen.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/undoNormal.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/undoNormal.png
new file mode 100644
index 0000000..b780f75
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/undoNormal.png
Binary files differ
diff --git a/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/unifiedDiff.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/unifiedDiff.png
new file mode 100755
index 0000000..ec5f97a
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/unifiedDiff.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/warning.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/warning.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/warning.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/warning.png
Binary files differ
diff --git a/gerrit-gwtui/BUCK b/gerrit-gwtui/BUCK
index 89b7ef7..aad5e0b 100644
--- a/gerrit-gwtui/BUCK
+++ b/gerrit-gwtui/BUCK
@@ -2,7 +2,7 @@
 include_defs('//tools/gwt-constants.defs')
 from multiprocessing import cpu_count
 
-DEPS = [
+DEPS = GWT_COMMON_DEPS + [
   '//gerrit-gwtexpui:CSS',
   '//lib:gwtjsonrpc',
 ]
@@ -15,7 +15,7 @@
     ' gerrit_ui/gerrit_ui.nocache.js' +
     ' gerrit_ui/dbg_gerrit_ui.nocache.js;' +
     'unzip -qo $(location :ui_opt);' +
-    'mkdir -p $(dirname $OUT);' +
+    'mkdir -p \$(dirname $OUT);' +
     'zip -qr $OUT .',
   deps = [
     ':ui_dbg',
@@ -37,6 +37,17 @@
 )
 
 gwt_binary(
+  name = 'ui_soyc',
+  modules = [MODULE],
+  module_deps = [':ui_module'],
+  deps = DEPS + [':ui_dbg'],
+  local_workers = cpu_count(),
+  strict = True,
+  experimental_args = GWT_COMPILER_ARGS + ['-compileReport'],
+  vm_args = GWT_JVM_ARGS,
+)
+
+gwt_binary(
   name = 'ui_dbg',
   modules = [MODULE],
   style = 'PRETTY',
@@ -59,16 +70,14 @@
   visibility = ['//:'],
 )
 
-DIFFY = glob(['src/main/java/com/google/gerrit/client/diffy*.png'])
-
 gwt_module(
   name = 'ui_module',
   srcs = glob(['src/main/java/**/*.java']),
   gwt_xml = 'src/main/java/%s.gwt.xml' % MODULE.replace('.', '/'),
-  resources = glob(['src/main/java/**/*'], excludes = DIFFY),
+  resources = glob(['src/main/java/**/*']),
   deps = [
-    ':diffy_logo',
     ':freebie_application_icon_set',
+    '//gerrit-gwtui-common:diffy_logo',
     '//gerrit-gwtexpui:Clippy',
     '//gerrit-gwtexpui:GlobalKey',
     '//gerrit-gwtexpui:Progress',
@@ -94,15 +103,6 @@
   ],
 )
 
-prebuilt_jar(
-  name = 'diffy_logo',
-  binary_jar = ':diffy_image_files_ln',
-  deps = [
-    '//lib:LICENSE-diffy',
-    '//lib:LICENSE-CC-BY3.0',
-  ],
-)
-
 java_library(
   name = 'freebie_application_icon_set',
   deps = [
@@ -111,18 +111,6 @@
   ],
 )
 
-genrule(
-  name = 'diffy_image_files_ln',
-  cmd = 'ln -s $(location :diffy_image_files) $OUT',
-  deps = [':diffy_image_files'],
-  out = 'diffy_images.jar',
-)
-
-java_library(
-  name = 'diffy_image_files',
-  resources = DIFFY,
-)
-
 java_test(
   name = 'ui_tests',
   srcs = glob(['src/test/java/**/*.java']),
diff --git a/gerrit-gwtui/gwt.defs b/gerrit-gwtui/gwt.defs
index 0e10116..cd206c0 100644
--- a/gerrit-gwtui/gwt.defs
+++ b/gerrit-gwtui/gwt.defs
@@ -56,7 +56,7 @@
     genrule(
       name = '%s_gwtxml_gen' % gwt_name,
       cmd = 'cd $TMP;' +
-        ('mkdir -p $(dirname %s);' % gwt) +
+        ('mkdir -p \$(dirname %s);' % gwt) +
         ('echo "%s">%s;' % (xml, gwt)) +
         'zip -qr $OUT .',
       out = jar,
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
index b8102ec..fd717ee 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
@@ -39,7 +39,6 @@
   <add-linker name='xsiframe'/>
 
   <set-property name='gwt.logging.logLevel' value='SEVERE'/>
-  <set-property name='gwt.logging.popupHandler' value='DISABLED'/>
 
   <entry-point class='com.google.gerrit.client.Gerrit'/>
 </module>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
index e85633f..ab3ff5d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
@@ -25,8 +25,8 @@
 
 public class ConfirmationDialog extends AutoCenterDialogBox {
 
-
   private Button cancelButton;
+  private Button okButton;
 
   public ConfirmationDialog(final String dialogTitle, final SafeHtml message,
       final ConfirmationCallback callback) {
@@ -36,7 +36,7 @@
 
     final FlowPanel buttons = new FlowPanel();
 
-    final Button okButton = new Button();
+    okButton = new Button();
     okButton.setText(Gerrit.C.confirmationDialogOk());
     okButton.addClickHandler(new ClickHandler() {
       @Override
@@ -76,4 +76,11 @@
     GlobalKey.dialog(this);
     cancelButton.setFocus(true);
   }
+
+  public void setCancelVisible(boolean visible) {
+    cancelButton.setVisible(visible);
+    if (!visible) {
+      okButton.setFocus(true);
+    }
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffWebLinkInfo.java
similarity index 62%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffWebLinkInfo.java
index 407b7c7..bcf4256 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffWebLinkInfo.java
@@ -12,10 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.client;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+public class DiffWebLinkInfo extends WebLinkInfo {
+  public final native boolean showOnSideBySideDiffView()
+  /*-{ return this.show_on_side_by_side_diff_view || false; }-*/;
+
+  public final native boolean showOnUnifiedDiffView()
+  /*-{ return this.show_on_unified_diff_view || false; }-*/;
+
+  protected DiffWebLinkInfo() {
+  }
+}
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 2ad9a5e..e2bf142 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
@@ -56,7 +56,6 @@
 import com.google.gerrit.client.admin.CreateGroupScreen;
 import com.google.gerrit.client.admin.CreateProjectScreen;
 import com.google.gerrit.client.admin.GroupListScreen;
-import com.google.gerrit.client.admin.MyGroupsListScreen;
 import com.google.gerrit.client.admin.PluginListScreen;
 import com.google.gerrit.client.admin.ProjectAccessScreen;
 import com.google.gerrit.client.admin.ProjectBranchesScreen;
@@ -65,29 +64,26 @@
 import com.google.gerrit.client.admin.ProjectListScreen;
 import com.google.gerrit.client.admin.ProjectScreen;
 import com.google.gerrit.client.api.ExtensionScreen;
-import com.google.gerrit.client.change.ChangeScreen2;
+import com.google.gerrit.client.change.ChangeScreen;
+import com.google.gerrit.client.change.FileTable;
 import com.google.gerrit.client.changes.AccountDashboardScreen;
-import com.google.gerrit.client.changes.ChangeScreen;
 import com.google.gerrit.client.changes.CustomDashboardScreen;
-import com.google.gerrit.client.changes.PatchTable;
 import com.google.gerrit.client.changes.ProjectDashboardScreen;
-import com.google.gerrit.client.changes.PublishCommentScreen;
 import com.google.gerrit.client.changes.QueryScreen;
 import com.google.gerrit.client.dashboards.DashboardInfo;
 import com.google.gerrit.client.dashboards.DashboardList;
 import com.google.gerrit.client.diff.DisplaySide;
-import com.google.gerrit.client.diff.SideBySide2;
+import com.google.gerrit.client.diff.SideBySide;
 import com.google.gerrit.client.documentation.DocScreen;
+import com.google.gerrit.client.editor.EditScreen;
 import com.google.gerrit.client.groups.GroupApi;
 import com.google.gerrit.client.groups.GroupInfo;
-import com.google.gerrit.client.patches.PatchScreen;
+import com.google.gerrit.client.patches.UnifiedPatchScreen;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
@@ -97,19 +93,11 @@
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.RunAsyncCallback;
 import com.google.gwt.http.client.URL;
-import com.google.gwt.user.client.Cookies;
 import com.google.gwt.user.client.Window;
 import com.google.gwtorm.client.KeyUtil;
 
 public class Dispatcher {
-  public static final String COOKIE_CS2 = "gerrit_cs2";
-  public static boolean changeScreen2;
-
-  public static String toPatchSideBySide(final Patch.Key id) {
-    return toPatch("", null, id);
-  }
-
-  public static String toPatchSideBySide(PatchSet.Id diffBase, Patch.Key id) {
+  public static String toSideBySide(PatchSet.Id diffBase, Patch.Key id) {
     return toPatch("", diffBase, id);
   }
 
@@ -128,18 +116,22 @@
     return toPatch("unified", diffBase, revision, fileName, null, 0);
   }
 
-  public static String toPatchUnified(final Patch.Key id) {
-    return toPatch("unified", null, id);
-  }
-
-  public static String toPatchUnified(PatchSet.Id diffBase, Patch.Key id) {
+  public static String toUnified(PatchSet.Id diffBase, Patch.Key id) {
     return toPatch("unified", diffBase, id);
   }
 
-  private static String toPatch(String type, PatchSet.Id diffBase, Patch.Key id) {
+  public static String toPatch(String type, PatchSet.Id diffBase, Patch.Key id) {
     return toPatch(type, diffBase, id.getParentKey(), id.get(), null, 0);
   }
 
+  public static String toEditScreen(PatchSet.Id revision, String fileName) {
+    return toEditScreen(revision, fileName, 0);
+  }
+
+  public static String toEditScreen(PatchSet.Id revision, String fileName, int line) {
+    return toPatch("edit", null, revision, fileName, null, line);
+  }
+
   private static String toPatch(String type, PatchSet.Id diffBase,
       PatchSet.Id revision, String fileName, DisplaySide side, int line) {
     Change.Id c = revision.getParentKey();
@@ -148,8 +140,9 @@
     if (diffBase != null) {
       p.append(diffBase.get()).append("..");
     }
-    p.append(revision.get()).append("/").append(KeyUtil.encode(fileName));
-    if (type != null && !type.isEmpty()) {
+    p.append(revision.getId()).append("/").append(KeyUtil.encode(fileName));
+    if (type != null && !type.isEmpty()
+        && (!"sidebyside".equals(type) || preferUnified())) {
       p.append(",").append(type);
     }
     if (side == DisplaySide.A && line > 0) {
@@ -160,19 +153,6 @@
     return p.toString();
   }
 
-  public static String toPatch(final PatchScreen.Type type, final Patch.Key id) {
-    if (type == PatchScreen.Type.SIDE_BY_SIDE) {
-      return toPatchSideBySide(id);
-    } else {
-      return toPatchUnified(id);
-    }
-  }
-
-  public static String toPublish(PatchSet.Id ps) {
-    Change.Id c = ps.getParentKey();
-    return "/c/" + c + "/" + ps.get() + ",publish";
-  }
-
   public static String toGroup(final AccountGroup.Id id) {
     return ADMIN_GROUPS + id.toString();
   }
@@ -239,7 +219,7 @@
       if (defaultScreenToken != null && !MINE.equals(defaultScreenToken)) {
         select(defaultScreenToken);
       } else {
-        Gerrit.display(token, mine(token));
+        Gerrit.display(token, mine());
       }
 
     } else if (matchPrefix("/dashboard/", token)) {
@@ -248,23 +228,20 @@
     } else if (matchPrefix(PROJECTS, token)) {
       projects(token);
 
-    } else if (matchExact(SETTINGS, token) //
-        || matchPrefix("/settings/", token) //
-        || matchExact("register", token) //
-        || matchExact(REGISTER, token) //
-        || matchPrefix("/register/", token) //
-        || matchPrefix("/VE/", token) || matchPrefix("VE,", token) //
+    } else if (matchExact(SETTINGS, token)
+        || matchPrefix("/settings/", token)
+        || matchExact(MY_GROUPS, token)
+        || matchExact("register", token)
+        || matchExact(REGISTER, token)
+        || matchPrefix("/register/", token)
+        || matchPrefix("/VE/", token) || matchPrefix("VE,", token)
         || matchPrefix("/SignInFailure,", token)) {
       settings(token);
 
     } else if (matchPrefix("/admin/", token)) {
       admin(token);
 
-    } else if (matchExact(MY_GROUPS, token)) {
-      Gerrit.display(token, new MyGroupsListScreen());
-
     } else if (/* DEPRECATED URL */matchPrefix("/c2/", token)) {
-      changeScreen2 = true;
       change(token);
     } else if (/* LEGACY URL */matchPrefix("all,", token)) {
       redirectFromLegacyToken(token, legacyAll(token));
@@ -307,7 +284,7 @@
     }
 
     if (matchExact("mine,drafts", token)) {
-      return toChangeQuery("is:draft");
+      return toChangeQuery("owner:self is:draft");
     }
 
     if (matchExact("mine,comments", token)) {
@@ -373,11 +350,11 @@
 
   private static String legacyPatch(String token) {
     if (/* LEGACY URL */matchPrefix("patch,sidebyside,", token)) {
-      return toPatchSideBySide(Patch.Key.parse(skip(token)));
+      return toPatch("", null, Patch.Key.parse(skip(token)));
     }
 
     if (/* LEGACY URL */matchPrefix("patch,unified,", token)) {
-      return toPatchUnified(Patch.Key.parse(skip(token)));
+      return toPatch("unified", null, Patch.Key.parse(skip(token)));
     }
 
     return null;
@@ -432,7 +409,7 @@
     Gerrit.display(token, screen);
   }
 
-  private static Screen mine(final String token) {
+  private static Screen mine() {
     if (Gerrit.isSignedIn()) {
       return new AccountDashboardScreen(Gerrit.getUserAccount().getId());
 
@@ -522,6 +499,11 @@
     if (0 <= c) {
       panel = rest.substring(c + 1);
       rest = rest.substring(0, c);
+      int at = panel.lastIndexOf('@');
+      if (at > 0) {
+        rest += panel.substring(at);
+        panel = panel.substring(0, at);
+      }
     }
 
     Change.Id id;
@@ -535,10 +517,14 @@
     }
 
     if (rest.isEmpty()) {
-      Gerrit.display(token, panel== null
-          ? (isChangeScreen2()
-              ? new ChangeScreen2(id, null, null, false)
-              : new ChangeScreen(id))
+      FileTable.Mode mode = FileTable.Mode.REVIEW;
+      if (panel != null
+          && (panel.equals("edit") || panel.startsWith("edit/"))) {
+        mode = FileTable.Mode.EDIT;
+        panel = null;
+      }
+      Gerrit.display(token, panel == null
+          ? new ChangeScreen(id, null, null, false, mode)
           : new NotFoundScreen());
       return;
     }
@@ -553,16 +539,14 @@
       rest = "";
     }
 
-    PatchSet.Id base;
+    PatchSet.Id base = null;
     PatchSet.Id ps;
     int dotdot = psIdStr.indexOf("..");
     if (1 <= dotdot) {
       base = new PatchSet.Id(id, Integer.parseInt(psIdStr.substring(0, dotdot)));
-      ps = new PatchSet.Id(id, Integer.parseInt(psIdStr.substring(dotdot + 2)));
-    } else {
-      base = null;
-      ps = new PatchSet.Id(id, Integer.parseInt(psIdStr));
+      psIdStr = psIdStr.substring(dotdot + 2);
     }
+    ps = toPsId(id, psIdStr);
 
     if (!rest.isEmpty()) {
       DisplaySide side = DisplaySide.B;
@@ -578,25 +562,27 @@
         rest = rest.substring(0, at);
       }
       Patch.Key p = new Patch.Key(ps, KeyUtil.decode(rest));
-      patch(token, base, p, side, line, 0,
-          null, null, null, panel);
+      patch(token, base, p, side, line, panel);
     } else {
       if (panel == null) {
-        Gerrit.display(token, isChangeScreen2()
-            ? new ChangeScreen2(id,
+        Gerrit.display(token,
+            new ChangeScreen(id,
                 base != null
                     ? String.valueOf(base.get())
                     : null,
-                String.valueOf(ps.get()), false)
-            : new ChangeScreen(id));
-      } else if ("publish".equals(panel)) {
-        publish(ps);
+                String.valueOf(ps.get()), false, FileTable.Mode.REVIEW));
       } else {
         Gerrit.display(token, new NotFoundScreen());
       }
     }
   }
 
+  private static PatchSet.Id toPsId(Change.Id id, String psIdStr) {
+    return new PatchSet.Id(id, psIdStr.equals("edit")
+        ? 0
+        : Integer.parseInt(psIdStr));
+  }
+
   private static void extension(final String token) {
     ExtensionScreen view = new ExtensionScreen(skip(token));
     if (view.isFound()) {
@@ -606,132 +592,70 @@
     }
   }
 
-  public static boolean isChangeScreen2() {
-    if (!Gerrit.getConfig().getNewFeatures()) {
-      return false;
-    } else if (changeScreen2) {
-      return true;
+  private static void patch(String token,
+      PatchSet.Id baseId,
+      Patch.Key id,
+      DisplaySide side,
+      int line,
+      String panelType) {
+    String panel = panelType;
+    if (panel == null) {
+      int c = token.lastIndexOf(',');
+      panel = 0 <= c ? token.substring(c + 1) : "";
     }
 
-    AccountGeneralPreferences.ChangeScreen ui = null;
-    if (Gerrit.isSignedIn()) {
-      ui = Gerrit.getUserAccount()
-          .getGeneralPreferences()
-          .getChangeScreen();
-    }
-    String v = Cookies.getCookie(Dispatcher.COOKIE_CS2);
-    if (v != null) {
-      changeScreen2 = "1".equals(v);
-      return changeScreen2;
-    }
-    if (ui == null) {
-      ui = Gerrit.getConfig().getChangeScreen();
-    }
-    return ui == AccountGeneralPreferences.ChangeScreen.CHANGE_SCREEN2;
-  }
-
-  private static void publish(final PatchSet.Id ps) {
-    String token = toPublish(ps);
-    new AsyncSplit(token) {
-      public void onSuccess() {
-        Gerrit.display(token, select());
+    if ("".equals(panel) || /* DEPRECATED URL */"cm".equals(panel)) {
+      if (preferUnified()) {
+        unified(token, baseId, id);
+      } else {
+        codemirror(token, baseId, id, side, line, false);
       }
-
-      private Screen select() {
-        return new PublishCommentScreen(ps);
-      }
-    }.onSuccess();
+    } else if ("sidebyside".equals(panel)) {
+      codemirror(token, null, id, side, line, false);
+    } else if ("unified".equals(panel)) {
+      unified(token, baseId, id);
+    } else if ("edit".equals(panel)) {
+      codemirror(token, null, id, side, line, true);
+    } else {
+      Gerrit.display(token, new NotFoundScreen());
+    }
   }
 
-  public static void patch(String token, PatchSet.Id base, Patch.Key id,
-      int patchIndex, PatchSetDetail patchSetDetail,
-      PatchTable patchTable, PatchScreen.TopView topView) {
-    patch(token, base, id, null, 0, patchIndex,
-        patchSetDetail, patchTable, topView, null);
+  private static boolean preferUnified() {
+    return Gerrit.isSignedIn()
+        && DiffView.UNIFIED_DIFF.equals(Gerrit.getUserAccount()
+            .getGeneralPreferences()
+            .getDiffView());
   }
 
-  public static void patch(String token, final PatchSet.Id baseId, final Patch.Key id,
-      final DisplaySide side, final int line,
-      final int patchIndex, final PatchSetDetail patchSetDetail,
-      final PatchTable patchTable, final PatchScreen.TopView topView,
-      final String panelType) {
-    final PatchScreen.TopView top =  topView == null ?
-        Gerrit.getPatchScreenTopView() : topView;
-
+  private static void unified(final String token,
+      final PatchSet.Id baseId,
+      final Patch.Key id) {
     GWT.runAsync(new AsyncSplit(token) {
+      @Override
       public void onSuccess() {
-        Gerrit.display(token, select());
+        UnifiedPatchScreen.TopView top = Gerrit.getPatchScreenTopView();
+        Gerrit.display(token, new UnifiedPatchScreen(id, top, baseId));
       }
+    });
+  }
 
-      private Screen select() {
-        if (id != null) {
-          String panel = panelType;
-          if (panel == null) {
-            int c = token.lastIndexOf(',');
-            panel = 0 <= c ? token.substring(c + 1) : "";
-          }
-
-          if ("".equals(panel)) {
-            if (isChangeScreen2()) {
-              if (Gerrit.isSignedIn()
-                  && DiffView.UNIFIED_DIFF.equals(Gerrit.getUserAccount()
-                      .getGeneralPreferences().getDiffView())) {
-                return new PatchScreen.Unified(id, patchIndex, patchSetDetail,
-                    patchTable, top, baseId);
-              }
-              return new SideBySide2(baseId, id.getParentKey(), id.get(),
-                  side, line);
-            }
-            return new PatchScreen.SideBySide( //
-                id, //
-                patchIndex, //
-                patchSetDetail, //
-                patchTable, //
-                top, //
-                baseId //
-            );
-          } else if ("unified".equals(panel)) {
-            return new PatchScreen.Unified( //
-                id, //
-                patchIndex, //
-                patchSetDetail, //
-                patchTable, //
-                top, //
-                baseId //
-            );
-          } else if ("cm".equals(panel) && Gerrit.getConfig().getNewFeatures()) {
-            if (Gerrit.isSignedIn()
-                && DiffView.UNIFIED_DIFF.equals(Gerrit.getUserAccount()
-                    .getGeneralPreferences().getDiffView())) {
-              return new PatchScreen.Unified( //
-                  id, //
-                  patchIndex, //
-                  patchSetDetail, //
-                  patchTable, //
-                  top, //
-                  baseId //
-              );
-            }
-            return new SideBySide2(baseId, id.getParentKey(), id.get(),
-                side, line);
-          } else if ("".equals(panel) || "sidebyside".equals(panel)) {
-            return new PatchScreen.SideBySide(//
-                id, //
-                patchIndex,//
-                patchSetDetail,//
-                patchTable,//
-                top,//
-                baseId);//
-          }
-        }
-
-        return new NotFoundScreen();
+  private static void codemirror(final String token, final PatchSet.Id baseId,
+      final Patch.Key id, final DisplaySide side, final int line,
+      final boolean edit) {
+    GWT.runAsync(new AsyncSplit(token) {
+      @Override
+      public void onSuccess() {
+        Gerrit.display(token, edit
+            ? new EditScreen(baseId, id, line)
+            : new SideBySide(baseId, id.getParentKey(), id.get(), side, line));
       }
     });
   }
 
   private static void settings(String token) {
     GWT.runAsync(new AsyncSplit(token) {
+      @Override
       public void onSuccess() {
         Gerrit.display(token, select());
       }
@@ -765,7 +689,8 @@
           return new MyPasswordScreen();
         }
 
-        if (matchExact(SETTINGS_MYGROUPS, token)) {
+        if (matchExact(MY_GROUPS, token)
+            || matchExact(SETTINGS_MYGROUPS, token)) {
           return new MyGroupsScreen();
         }
 
@@ -799,6 +724,7 @@
 
   private static void admin(String token) {
     GWT.runAsync(new AsyncSplit(token) {
+      @Override
       public void onSuccess() {
         if (matchExact(ADMIN_GROUPS, token)
             || matchExact("/admin/groups", token)) {
@@ -920,6 +846,11 @@
             return new NotFoundScreen();
           }
 
+          int q = rest.lastIndexOf('?');
+          if (q > 0 && rest.lastIndexOf(',', q) > 0) {
+            c = rest.substring(0, q - 1).lastIndexOf(',');
+          }
+
           Project.NameKey k = Project.NameKey.parse(rest.substring(0, c));
           String panel = rest.substring(c + 1);
 
@@ -927,7 +858,8 @@
             return new ProjectInfoScreen(k);
           }
 
-          if (ProjectScreen.BRANCH.equals(panel)) {
+          if (ProjectScreen.BRANCH.equals(panel)
+              || matchPrefix(ProjectScreen.BRANCH, panel)) {
             return new ProjectBranchesScreen(k);
           }
 
@@ -963,7 +895,7 @@
     return token.substring(prefixlen);
   }
 
-  private static abstract class AsyncSplit implements RunAsyncCallback {
+  private abstract static class AsyncSplit implements RunAsyncCallback {
     private final boolean isReloadUi;
     protected final String token;
 
@@ -972,6 +904,7 @@
       this.token = token;
     }
 
+    @Override
     public final void onFailure(Throwable reason) {
       if (!isReloadUi
           && "HTTP download failed with status 404".equals(reason.getMessage())) {
@@ -988,6 +921,7 @@
 
   private static void docSearch(final String token) {
     GWT.runAsync(new AsyncSplit(token) {
+      @Override
       public void onSuccess() {
         Gerrit.display(token, new DocScreen(skip(token)));
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index 27a4b53..296f93f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -31,14 +31,13 @@
 import com.google.gerrit.client.extensions.TopMenu;
 import com.google.gerrit.client.extensions.TopMenuItem;
 import com.google.gerrit.client.extensions.TopMenuList;
-import com.google.gerrit.client.patches.PatchScreen;
+import com.google.gerrit.client.patches.UnifiedPatchScreen;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.LinkMenuBar;
 import com.google.gerrit.client.ui.LinkMenuItem;
 import com.google.gerrit.client.ui.MorphingTabPanel;
-import com.google.gerrit.client.ui.PatchLink;
 import com.google.gerrit.client.ui.ProjectLinkMenuItem;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
@@ -46,11 +45,12 @@
 import com.google.gerrit.common.data.GitwebConfig;
 import com.google.gerrit.common.data.HostPageData;
 import com.google.gerrit.common.data.SystemInfoService;
-import com.google.gerrit.extensions.webui.GerritTopMenu;
+import com.google.gerrit.extensions.client.GerritTopMenu;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.aria.client.Roles;
 import com.google.gwt.core.client.EntryPoint;
 import com.google.gwt.core.client.GWT;
@@ -102,6 +102,7 @@
   public static final SystemInfoService SYSTEM_SVC;
   public static final EventBus EVENT_BUS = GWT.create(SimpleEventBus.class);
   public static Themer THEMER = GWT.create(Themer.class);
+  public static final String PROJECT_NAME_MENU_VAR = "${projectName}";
 
   private static String myHost;
   private static GerritConfig myConfig;
@@ -110,6 +111,7 @@
   private static String defaultScreenToken;
   private static AccountDiffPreference myAccountDiffPref;
   private static String xGerritAuth;
+  private static boolean isNoteDbEnabled;
 
   private static Map<String, LinkMenuBar> menuBars;
 
@@ -122,7 +124,7 @@
   private static SearchPanel searchPanel;
   private static final Dispatcher dispatcher = new Dispatcher();
   private static ViewSite<Screen> body;
-  private static PatchScreen patchScreen;
+  private static UnifiedPatchScreen patchScreen;
   private static String lastChangeListToken;
   private static String lastViewToken;
 
@@ -136,7 +138,7 @@
     Window.Location.reload();
   }
 
-  public static PatchScreen.TopView getPatchScreenTopView() {
+  public static UnifiedPatchScreen.TopView getPatchScreenTopView() {
     if (patchScreen == null) {
       return null;
     }
@@ -202,8 +204,8 @@
    */
   public static void updateMenus(Screen view) {
     LinkMenuBar diffBar = menuBars.get(GerritTopMenu.DIFFERENCES.menuName);
-    if (view instanceof PatchScreen) {
-      patchScreen = (PatchScreen) view;
+    if (view instanceof UnifiedPatchScreen) {
+      patchScreen = (UnifiedPatchScreen) view;
       menuLeft.setVisible(diffBar, true);
       menuLeft.selectTab(menuLeft.getWidgetIndex(diffBar));
     } else {
@@ -330,6 +332,10 @@
     Location.assign(loginRedirect(token));
   }
 
+  public static boolean isNoteDbEnabled() {
+    return isNoteDbEnabled;
+  }
+
   public static String loginRedirect(String token) {
     if (token == null) {
       token = "";
@@ -427,6 +433,7 @@
         Document.get().getElementById("gerrit_hostpagedata").removeFromParent();
         myConfig = result.config;
         myTheme = result.theme;
+        isNoteDbEnabled = result.isNoteDbEnabled;
         if (result.account != null) {
           myAccount = result.account;
           xGerritAuth = result.xGerritAuth;
@@ -464,14 +471,17 @@
 
     btmmenu.add(new InlineHTML(M.poweredBy(vs)));
 
-    final String reportBugText = getConfig().getReportBugText();
-    Anchor a = new Anchor(
-        reportBugText == null ? C.reportBug() : reportBugText,
-        getConfig().getReportBugUrl());
-    a.setTarget("_blank");
-    a.setStyleName("");
-    btmmenu.add(new InlineLabel(" | "));
-    btmmenu.add(a);
+    String reportBugUrl = getConfig().getReportBugUrl();
+    if (reportBugUrl != null) {
+      String reportBugText = getConfig().getReportBugText();
+      Anchor a = new Anchor(
+          reportBugText == null ? C.reportBug() : reportBugText,
+          reportBugUrl);
+      a.setTarget("_blank");
+      a.setStyleName("");
+      btmmenu.add(new InlineLabel(" | "));
+      btmmenu.add(a);
+    }
     btmmenu.add(new InlineLabel(" | "));
     btmmenu.add(new InlineLabel(C.keyHelp()));
   }
@@ -626,12 +636,10 @@
     LinkMenuBar diffBar = new LinkMenuBar();
     menuBars.put(GerritTopMenu.DIFFERENCES.menuName, diffBar);
     menuLeft.addInvisible(diffBar, C.menuDiff());
-    addDiffLink(diffBar, CC.patchTableDiffSideBySide(), PatchScreen.Type.SIDE_BY_SIDE);
-    addDiffLink(diffBar, CC.patchTableDiffUnified(), PatchScreen.Type.UNIFIED);
-    addDiffLink(diffBar, C.menuDiffCommit(), PatchScreen.TopView.COMMIT);
-    addDiffLink(diffBar, C.menuDiffPreferences(), PatchScreen.TopView.PREFERENCES);
-    addDiffLink(diffBar, C.menuDiffPatchSets(), PatchScreen.TopView.PATCH_SETS);
-    addDiffLink(diffBar, C.menuDiffFiles(), PatchScreen.TopView.FILES);
+    addDiffLink(diffBar, C.menuDiffCommit(), UnifiedPatchScreen.TopView.COMMIT);
+    addDiffLink(diffBar, C.menuDiffPreferences(), UnifiedPatchScreen.TopView.PREFERENCES);
+    addDiffLink(diffBar, C.menuDiffPatchSets(), UnifiedPatchScreen.TopView.PATCH_SETS);
+    addDiffLink(diffBar, C.menuDiffFiles(), UnifiedPatchScreen.TopView.FILES);
 
     final LinkMenuBar projectsBar = new LinkMenuBar();
     menuBars.put(GerritTopMenu.PROJECTS.menuName, projectsBar);
@@ -642,6 +650,7 @@
     final LinkMenuItem dashboardsMenuItem =
         new ProjectLinkMenuItem(C.menuProjectsDashboards(),
             ProjectScreen.DASHBOARDS) {
+      @Override
       protected boolean match(String token) {
         return super.match(token) ||
             (!getTargetHistoryToken().isEmpty() && ("/admin" + token).startsWith(getTargetHistoryToken()));
@@ -703,6 +712,7 @@
 
         case OPENID:
           menuRight.addItem(C.menuRegister(), new Command() {
+            @Override
             public void execute() {
               String t = History.getToken();
               if (t == null) {
@@ -712,6 +722,7 @@
             }
           });
           menuRight.addItem(C.menuSignIn(), new Command() {
+            @Override
             public void execute() {
               doSignIn(History.getToken());
             }
@@ -729,6 +740,7 @@
 
         case OPENID_SSO:
           menuRight.addItem(C.menuSignIn(), new Command() {
+            @Override
             public void execute() {
               doSignIn(History.getToken());
             }
@@ -751,6 +763,7 @@
             menuRight.add(anchor(registerText, cfg.getRegisterUrl()));
           }
           menuRight.addItem(C.menuSignIn(), new Command() {
+            @Override
             public void execute() {
               doSignIn(History.getToken());
             }
@@ -763,17 +776,20 @@
       }
     }
     ConfigServerApi.topMenus(new GerritCallback<TopMenuList>() {
+      @Override
       public void onSuccess(TopMenuList result) {
         List<TopMenu> topMenuExtensions = Natives.asList(result);
         for (TopMenu menu : topMenuExtensions) {
-          LinkMenuBar existingBar = menuBars.get(menu.getName());
-          LinkMenuBar bar = existingBar != null ? existingBar : new LinkMenuBar();
+          String name = menu.getName();
+          LinkMenuBar existingBar = menuBars.get(name);
+          LinkMenuBar bar =
+              existingBar != null ? existingBar : new LinkMenuBar();
           for (TopMenuItem item : Natives.asList(menu.getItems())) {
-            addExtensionLink(bar, item);
+            addMenuLink(bar, item);
           }
           if (existingBar == null) {
-            menuBars.put(menu.getName(), bar);
-            menuLeft.add(bar, menu.getName());
+            menuBars.put(name, bar);
+            menuLeft.add(bar, name);
           }
         }
       }
@@ -880,7 +896,7 @@
   }
 
   private static void addDiffLink(final LinkMenuBar m, final String text,
-      final PatchScreen.TopView tv) {
+      final UnifiedPatchScreen.TopView tv) {
     m.addItem(new LinkMenuItem(text, "") {
         @Override
         public void go() {
@@ -892,21 +908,45 @@
       });
   }
 
-  private static void addDiffLink(final LinkMenuBar m, final String text,
-      final PatchScreen.Type type) {
-    m.addItem(new LinkMenuItem(text, "") {
+  private static LinkMenuItem addProjectLink(LinkMenuBar m, TopMenuItem item) {
+    LinkMenuItem i = new ProjectLinkMenuItem(item.getName(), item.getUrl()) {
+        @Override
+        protected void onScreenLoad(Project.NameKey project) {
+        String p =
+            panel.replace(PROJECT_NAME_MENU_VAR,
+                URL.encodeQueryString(project.get()));
+          if (!panel.startsWith("/x/") && !isAbsolute(panel)) {
+            UrlBuilder builder = new UrlBuilder();
+            builder.setProtocol(Location.getProtocol());
+            builder.setHost(Location.getHost());
+            String port = Location.getPort();
+            if (port != null && !port.isEmpty()) {
+              builder.setPort(Integer.parseInt(port));
+            }
+            builder.setPath(Location.getPath());
+            p = builder.buildString() + p;
+          }
+          getElement().setPropertyString("href", p);
+        }
+
         @Override
         public void go() {
-          if (patchScreen != null) {
-            patchScreen.setTopView(PatchScreen.TopView.MAIN);
-            if (type == patchScreen.getPatchScreenType()) {
-              AnchorElement.as(getElement()).blur();
-            } else {
-              new PatchLink("", type, patchScreen).go();
-            }
+          String href = getElement().getPropertyString("href");
+          if (href.startsWith("#")) {
+            super.go();
+          } else {
+            Window.open(href, getElement().getPropertyString("target"), "");
           }
         }
-      });
+      };
+    if (item.getTarget() != null && !item.getTarget().isEmpty()) {
+      i.getElement().setAttribute("target", item.getTarget());
+    }
+    if (item.getId() != null) {
+      i.getElement().setAttribute("id", item.getId());
+    }
+    m.addItem(i);
+    return i;
   }
 
   private static void addDocLink(final LinkMenuBar m, final String text,
@@ -916,6 +956,14 @@
     m.add(atag);
   }
 
+  private static void addMenuLink(LinkMenuBar m, TopMenuItem item) {
+    if (item.getUrl().contains(PROJECT_NAME_MENU_VAR)) {
+      addProjectLink(m, item);
+    } else {
+      addExtensionLink(m, item);
+    }
+  }
+
   private static void addExtensionLink(LinkMenuBar m, TopMenuItem item) {
     if (item.getUrl().startsWith("#")
         && (item.getTarget() == null || item.getTarget().isEmpty())) {
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 5ba520c..6bbc8f1 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
@@ -23,7 +23,7 @@
   String loadingPlugins();
 
   String signInDialogTitle();
-  String signInDialogClose();
+  String signInDialogGoAnonymous();
 
   String linkIdentityDialogTitle();
   String registerDialogTitle();
@@ -36,6 +36,9 @@
   String confirmationDialogOk();
   String confirmationDialogCancel();
 
+  String branchCreationDialogTitle();
+  String branchCreationConfirmationMessage();
+
   String branchDeletionDialogTitle();
   String branchDeletionConfirmationMessage();
 
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 58a6d08..05de983 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
@@ -4,7 +4,7 @@
 loadingPlugins = Loading plugins ...
 
 signInDialogTitle = Code Review - Sign In
-signInDialogClose = Close
+signInDialogGoAnonymous = Go Anonymous
 
 linkIdentityDialogTitle = Code Review - Link Identity
 registerDialogTitle = Code Review - Register New Account
@@ -17,6 +17,9 @@
 confirmationDialogOk = OK
 confirmationDialogCancel = Cancel
 
+branchCreationDialogTitle = Branch Creation
+branchCreationConfirmationMessage = The following branch was successfully created:
+
 branchDeletionDialogTitle = Branch Deletion
 branchDeletionConfirmationMessage = Do you really want to delete the following branches?
 
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 42a8d74..2844b5e 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
@@ -22,22 +22,16 @@
   String accountDashboard();
   String accountInfoBlock();
   String accountLinkPanel();
-  String accountName();
   String accountPassword();
   String accountUsername();
   String activeRow();
   String addBranch();
   String addMemberTextBox();
-  String addReviewer();
   String addSshKeyPanel();
   String addWatchPanel();
-  String approvalTable();
-  String approvalhint();
-  String approvalrole();
-  String approvalscore();
   String avatarInfoPanel();
-  String blockHeader();
   String bottomheader();
+  String branchTablePrevNextLinks();
   String cAPPROVAL();
   String cLastUpdate();
   String cOWNER();
@@ -45,44 +39,32 @@
   String cSUBJECT();
   String cSTATUS();
   String cellsNextToFileComment();
-  String changeComments();
-  String changeInfoBlock();
-  String changeInfoTopicPanel();
-  String changeScreen();
   String changeScreenDescription();
   String changeScreenStarIcon();
   String changeSize();
   String changeTable();
   String changeTablePrevNextLinks();
   String changeTypeCell();
-  String changeid();
-  String closedstate();
   String commentCell();
   String commentEditorPanel();
   String commentHolder();
   String commentHolderLeftmost();
   String commentPanel();
   String commentPanelAuthorCell();
-  String commentPanelBorder();
   String commentPanelButtons();
   String commentPanelContent();
   String commentPanelDateCell();
   String commentPanelHeader();
   String commentPanelLast();
-  String commentPanelMenuBar();
   String commentPanelMessage();
   String commentPanelSummary();
   String commentPanelSummaryCell();
   String commentedActionDialog();
   String commentedActionMessage();
-  String complexHeader();
-  String content();
   String contributorAgreementAlreadySubmitted();
   String contributorAgreementButton();
   String contributorAgreementLegal();
   String contributorAgreementShortDescription();
-  String coverMessage();
-  String createGroupLink();
   String createProjectPanel();
   String dataCell();
   String dataCellHidden();
@@ -93,7 +75,6 @@
   String diffTextCONTEXT();
   String diffTextDELETE();
   String diffTextFileHeader();
-  String diffTextForBinaryInSideBySide();
   String diffTextHunkHeader();
   String diffTextINSERT();
   String diffTextNoLF();
@@ -108,7 +89,6 @@
   String downloadLinkHeader();
   String downloadLinkHeaderGap();
   String downloadLinkList();
-  String downloadLinkListCell();
   String downloadLink_Active();
   String drafts();
   String editHeadButton();
@@ -117,23 +97,18 @@
   String errorDialogButtons();
   String errorDialogErrorType();
   String errorDialogGlass();
-  String errorDialogText();
   String errorDialogTitle();
   String loadingPluginsDialog();
   String fileColumnHeader();
   String fileCommentBorder();
   String fileLine();
-  String fileLineCONTEXT();
   String fileLineDELETE();
   String fileLineINSERT();
-  String fileLineMode();
-  String fileLineNone();
   String filePathCell();
   String gerritBody();
   String gerritTopMenu();
   String greenCheckClass();
   String groupDescriptionPanel();
-  String groupExternalNameFilterTextBox();
   String groupIncludesTable();
   String groupMembersTable();
   String groupName();
@@ -142,25 +117,21 @@
   String groupOptionsPanel();
   String groupOwnerPanel();
   String groupOwnerTextBox();
-  String groupTypeSelectListBox();
   String groupUUIDPanel();
   String header();
-  String hyperlink();
   String iconCell();
   String iconCellOfFileCommentRow();
   String iconHeader();
   String identityUntrustedExternalId();
   String infoBlock();
-  String infoTable();
   String inputFieldTypeHint();
-  String labelList();
   String labelNotApplicable();
   String leftMostCell();
-  String lineHeader();
   String lineNumber();
   String link();
   String linkMenuBar();
   String linkMenuItemNotLast();
+  String linkPanel();
   String maxObjectSizeLimitEffectiveLabel();
   String menuBarUserName();
   String menuBarUserNameAvatar();
@@ -168,62 +139,40 @@
   String menuBarUserNamePanel();
   String menuItem();
   String menuScreenMenuBar();
-  String missingApproval();
-  String missingApprovalList();
-  String monospace();
   String needsReview();
   String negscore();
-  String noLineLineNumber();
   String noborder();
-  String notVotable();
-  String outdated();
-  String parentsTable();
   String patchBrowserPopup();
   String patchBrowserPopupBody();
   String patchCellReverseDiff();
-  String patchComments();
   String patchContentTable();
   String patchHistoryTable();
   String patchHistoryTablePatchSetHeader();
   String patchNoDifference();
-  String patchScreenDisplayControls();
   String patchSetActions();
-  String patchSetInfoBlock();
-  String patchSetLink();
-  String patchSetRevision();
-  String patchSetUserIdentity();
   String patchSizeCell();
   String pluginProjectConfigInheritedValue();
   String pluginsTable();
   String posscore();
   String projectActions();
-  String projectAdminLabelRangeLine();
-  String projectAdminLabelValue();
   String projectFilterLabel();
   String projectFilterPanel();
   String projectNameColumn();
-  String publishCommentsScreen();
+  String rebaseContentPanel();
+  String rebaseSuggestBox();
   String registerScreenExplain();
   String registerScreenNextLinks();
   String registerScreenSection();
-  String removeReviewer();
-  String removeReviewerCell();
   String reviewedPanelBottom();
   String rightBorder();
-  String rightmost();
   String rpcStatus();
   String screen();
   String screenHeader();
-  String screenNoHeader();
   String searchPanel();
   String suggestBoxPopup();
   String sectionHeader();
-  String selectPatchSetOldVersion();
   String sideBySideScreenLinkTable();
-  String sideBySideScreenSideBySideTable();
-  String sideBySideTableBinaryHeader();
   String singleLine();
-  String skipLine();
   String smallHeading();
   String sourceFilePath();
   String specialBranchDataCell();
@@ -245,7 +194,6 @@
   String unifiedTable();
   String unifiedTableHeader();
   String userInfoPopup();
-  String useridentity();
   String usernameField();
   String watchedProjectFilter();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
index 2b78fa4..01be2f2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
@@ -14,71 +14,12 @@
 
 package com.google.gerrit.client;
 
-import com.google.gwt.resources.client.ClientBundle;
 import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.resources.client.ImageResource;
 
-public interface GerritResources extends ClientBundle {
+public interface GerritResources extends Resources {
   @Source("gerrit.css")
   GerritCss css();
 
   @Source("gwt_override.css")
   CssResource gwt_override();
-
-  @Source("arrowRight.gif")
-  public ImageResource arrowRight();
-
-  @Source("arrowUp.png")
-  public ImageResource arrowUp();
-
-  @Source("arrowDown.png")
-  public ImageResource arrowDown();
-
-  @Source("editText.png")
-  public ImageResource edit();
-
-  @Source("starOpen.gif")
-  public ImageResource starOpen();
-
-  @Source("starFilled.gif")
-  public ImageResource starFilled();
-
-  @Source("greenCheck.png")
-  public ImageResource greenCheck();
-
-  @Source("redNot.png")
-  public ImageResource redNot();
-
-  @Source("downloadIcon.png")
-  public ImageResource downloadIcon();
-
-  @Source("queryIcon.png")
-  public ImageResource queryIcon();
-
-  @Source("addFileComment.png")
-  public ImageResource addFileComment();
-
-  @Source("diffy26.png")
-  public ImageResource gerritAvatar26();
-
-  @Source("draftComments.png")
-  public ImageResource draftComments();
-
-  @Source("readOnly.png")
-  public ImageResource readOnly();
-
-  @Source("gear.png")
-  public ImageResource gear();
-
-  @Source("info.png")
-  public ImageResource info();
-
-  @Source("warning.png")
-  public ImageResource warning();
-
-  @Source("listAdd.png")
-  public ImageResource listAdd();
-
-  @Source("merge.png")
-  public ImageResource merge();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java
index 45d9a91..e38cf7d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java
@@ -69,7 +69,7 @@
       jumps.add(new KeyCommand(0, 'd', Gerrit.C.jumpMineDrafts()) {
         @Override
         public void onKeyPress(final KeyPressEvent event) {
-          Gerrit.display(PageLinks.toChangeQuery("is:draft"));
+          Gerrit.display(PageLinks.toChangeQuery("owner:self is:draft"));
         }
       });
       jumps.add(new KeyCommand(0, 'c', Gerrit.C.jumpMineDraftComments()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.java
index b6ea636..054cdb3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.java
@@ -67,7 +67,7 @@
   }
 
   @UiHandler("dismiss")
-  void onDismiss(ClickEvent e) {
+  void onDismiss(@SuppressWarnings("unused") ClickEvent e) {
     removeFromParent();
 
     for (HostPageData.Message m : motd) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/NotSignedInDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/NotSignedInDialog.java
index 6a417ca..83c32cd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/NotSignedInDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/NotSignedInDialog.java
@@ -19,23 +19,25 @@
 import com.google.gwt.event.logical.shared.CloseEvent;
 import com.google.gwt.event.logical.shared.CloseHandler;
 import com.google.gwt.user.client.History;
+import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.PopupPanel;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.user.client.AutoCenterDialogBox;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
 
 /** A dialog box telling the user they are not signed in. */
-public class NotSignedInDialog extends AutoCenterDialogBox implements CloseHandler<PopupPanel> {
-
+public class NotSignedInDialog extends PluginSafePopupPanel implements CloseHandler<PopupPanel> {
   private Button signin;
-  private boolean buttonClicked = false;
+  private boolean buttonClicked;
 
   public NotSignedInDialog() {
     super(/* auto hide */false, /* modal */true);
     setGlassEnabled(true);
-    setText(Gerrit.C.notSignedInTitle());
+    getGlassElement().addClassName(Gerrit.RESOURCES.css().errorDialogGlass());
+    addStyleName(Gerrit.RESOURCES.css().errorDialog());
 
     final FlowPanel buttons = new FlowPanel();
     signin = new Button();
@@ -52,7 +54,7 @@
 
     final Button close = new Button();
     close.getElement().getStyle().setProperty("marginLeft", "200px");
-    close.setText(Gerrit.C.signInDialogClose());
+    close.setText(Gerrit.C.signInDialogGoAnonymous());
     close.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(ClickEvent event) {
@@ -63,13 +65,18 @@
     });
     buttons.add(close);
 
-    final FlowPanel center = new FlowPanel();
+    Label title = new Label(Gerrit.C.notSignedInTitle());
+    title.setStyleName(Gerrit.RESOURCES.css().errorDialogTitle());
+
+    FlowPanel center = new FlowPanel();
+    center.add(title);
     center.add(new HTML(Gerrit.C.notSignedInBody()));
     center.add(buttons);
     add(center);
 
-    center.setWidth("400px");
-
+    int l = Window.getScrollLeft() + 20;
+    int t = Window.getScrollTop() + 20;
+    setPopupPosition(l, t);
     addCloseHandler(this);
   }
 
@@ -84,7 +91,7 @@
 
   @Override
   public void center() {
-    super.center();
+    show();
     GlobalKey.dialog(this);
     signin.setFocus(true);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
index f86e1fe..d5805ef 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.client.changes.QueryScreen;
 import com.google.gerrit.client.ui.HintTextBox;
+import com.google.gerrit.client.ui.RemoteSuggestOracle;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -54,8 +55,9 @@
       }
     });
 
-    final SuggestBox suggestBox =
-        new SuggestBox(new SearchSuggestOracle(), searchBox, suggestionDisplay);
+    final SuggestBox suggestBox = new SuggestBox(
+        new RemoteSuggestOracle(new SearchSuggestOracle()),
+        searchBox, suggestionDisplay);
     searchBox.setStyleName("gwt-TextBox");
     searchBox.setVisibleLength(70);
     searchBox.setHintText(Gerrit.C.searchHint());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
index e92e613..0e1c375 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -124,6 +124,10 @@
     suggestions.add("delta:");
     suggestions.add("size:");
 
+    if (Gerrit.isNoteDbEnabled()) {
+      suggestions.add("hashtag:");
+    }
+
     suggestions.add("AND");
     suggestions.add("OR");
     suggestions.add("NOT");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/WebLinkInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/WebLinkInfo.java
index 64b9cb8..45731f7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/WebLinkInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/WebLinkInfo.java
@@ -15,12 +15,34 @@
 package com.google.gerrit.client;
 
 import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.Image;
 
 public class WebLinkInfo extends JavaScriptObject {
 
   public final native String name() /*-{ return this.name; }-*/;
+  public final native String imageUrl() /*-{ return this.image_url; }-*/;
   public final native String url() /*-{ return this.url; }-*/;
+  public final native String target() /*-{ return this.target; }-*/;
 
   protected WebLinkInfo() {
   }
+
+  public final Anchor toAnchor() {
+    Anchor a = new Anchor();
+    a.setHref(url());
+    if (target() != null && !target().isEmpty()) {
+      a.setTarget(target());
+    }
+    if (imageUrl() != null && !imageUrl().isEmpty()) {
+      Image img = new Image();
+      img.setAltText(name());
+      img.setUrl(imageUrl());
+      img.setTitle(name());
+      a.getElement().appendChild(img.getElement());
+    } else {
+      a.setText("(" + name() + ")");
+    }
+    return a;
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
index 8906da4..59d65f6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
@@ -33,6 +33,15 @@
     return new RestApi("/accounts/").view("self");
   }
 
+  public static void suggest(String query, int limit,
+      AsyncCallback<JsArray<AccountInfo>> cb) {
+    new RestApi("/accounts/")
+      .addParameter("q", query)
+      .addParameter("n", limit)
+      .background()
+      .get(cb);
+  }
+
   public static void putDiffPreferences(DiffPreferences in,
       AsyncCallback<DiffPreferences> cb) {
     self().view("preferences.diff").put(in, cb);
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 f52eb31..4c3cc29 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
@@ -25,8 +25,6 @@
   String registeredOn();
   String accountId();
 
-  String commentVisibilityLabel();
-  String changeScreenLabel();
   String diffViewLabel();
   String maximumPageSizeFieldLabel();
   String dateFormatLabel();
@@ -34,7 +32,6 @@
   String showSiteHeader();
   String useFlashClipboard();
   String copySelfOnEmails();
-  String reversePatchSetOrder();
   String reviewCategoryLabel();
   String messageShowInReviewCategoryNone();
   String messageShowInReviewCategoryName();
@@ -45,15 +42,13 @@
   String showRelativeDateInChangeTable();
   String showSizeBarInChangeTable();
   String showLegacycidInChangeTable();
+  String muteCommonPathPrefixes();
   String myMenu();
   String myMenuInfo();
   String myMenuName();
   String myMenuUrl();
   String myMenuReset();
 
-  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 5d48bb8..36cb765 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
@@ -8,8 +8,6 @@
 showSiteHeader = Show Site Header
 useFlashClipboard = Use Flash Clipboard Widget
 copySelfOnEmails = CC Me On Comments I Write
-reversePatchSetOrder = Display Patch Sets In Reverse Order (deprecated: Old Change Screen)
-
 reviewCategoryLabel = Display In Review Category
 messageShowInReviewCategoryNone = None (default)
 messageShowInReviewCategoryName = Show Name
@@ -18,15 +16,14 @@
 messageShowInReviewCategoryAbbrev = Show Abbreviated Name
 
 maximumPageSizeFieldLabel = Maximum Page Size:
-commentVisibilityLabel = Comment Visibility (deprecated: Old Change Screen):
-changeScreenLabel = Change View:
-diffViewLabel = Diff View (New Change Screen):
+diffViewLabel = Diff View:
 dateFormatLabel = Date/Time Format:
 contextWholeFile = Whole File
 buttonSaveChanges = Save Changes
 showRelativeDateInChangeTable = Show Relative Dates In Changes Table
-showSizeBarInChangeTable = Show Change Sizes As Colored Bars In Changes Table
+showSizeBarInChangeTable = Show Change Sizes As Colored Bars
 showLegacycidInChangeTable = Show Change Number In Changes Table
+muteCommonPathPrefixes = Mute Common Path Prefixes In File List
 myMenu = My Menu
 myMenuInfo = \
   Menu items for the 'My' top level menu. \
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
index 3ac626c..1127374 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
@@ -56,7 +56,7 @@
   }
 
   public static class AvatarInfo extends JavaScriptObject {
-    public final static int DEFAULT_SIZE = 26;
+    public static final int DEFAULT_SIZE = 26;
     public final native String url() /*-{ return this.url }-*/;
     public final native int height() /*-{ return this.height || 0 }-*/;
     public final native int width() /*-{ return this.width || 0 }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
index 66f676e..b5fe70f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
@@ -194,6 +194,7 @@
     haveEmails = false;
 
     Util.ACCOUNT_SVC.myAccount(new GerritCallback<Account>() {
+      @Override
       public void onSuccess(final Account result) {
         if (!isAttached()) {
           return;
@@ -359,6 +360,7 @@
 
     Util.ACCOUNT_SEC.updateContact(newName, newEmail, info,
         new GerritCallback<Account>() {
+          @Override
           public void onSuccess(final Account result) {
             registerNewEmail.setEnabled(true);
             onSaveSuccess(result);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
index 8a4666b..292e0b9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.client.account;
 
+import com.google.gerrit.extensions.client.Theme;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Theme;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gwt.core.client.JavaScriptObject;
 
@@ -35,6 +35,7 @@
     p.showWhitespaceErrors(in.isShowWhitespaceErrors());
     p.syntaxHighlighting(in.isSyntaxHighlighting());
     p.hideTopMenu(in.isHideTopMenu());
+    p.autoHideDiffTableHeader(in.isAutoHideDiffTableHeader());
     p.hideLineNumbers(in.isHideLineNumbers());
     p.expandAllComments(in.isExpandAllComments());
     p.manualReview(in.isManualReview());
@@ -55,6 +56,7 @@
     p.setShowWhitespaceErrors(showWhitespaceErrors());
     p.setSyntaxHighlighting(syntaxHighlighting());
     p.setHideTopMenu(hideTopMenu());
+    p.setAutoHideDiffTableHeader(autoHideDiffTableHeader());
     p.setHideLineNumbers(hideLineNumbers());
     p.setExpandAllComments(expandAllComments());
     p.setManualReview(manualReview());
@@ -82,6 +84,7 @@
   public final native void showWhitespaceErrors(boolean s) /*-{ this.show_whitespace_errors = s }-*/;
   public final native void syntaxHighlighting(boolean s) /*-{ this.syntax_highlighting = s }-*/;
   public final native void hideTopMenu(boolean s) /*-{ this.hide_top_menu = s }-*/;
+  public final native void autoHideDiffTableHeader(boolean s) /*-{ this.auto_hide_diff_table_header = s }-*/;
   public final native void hideLineNumbers(boolean s) /*-{ this.hide_line_numbers = s }-*/;
   public final native void expandAllComments(boolean e) /*-{ this.expand_all_comments = e }-*/;
   public final native void manualReview(boolean r) /*-{ this.manual_review = r }-*/;
@@ -110,6 +113,7 @@
   public final native boolean showWhitespaceErrors() /*-{ return this.show_whitespace_errors || false }-*/;
   public final native boolean syntaxHighlighting() /*-{ return this.syntax_highlighting || false }-*/;
   public final native boolean hideTopMenu() /*-{ return this.hide_top_menu || false }-*/;
+  public final native boolean autoHideDiffTableHeader() /*-{ return this.auto_hide_diff_table_header || false }-*/;
   public final native boolean hideLineNumbers() /*-{ return this.hide_line_numbers || false }-*/;
   public final native boolean expandAllComments() /*-{ return this.expand_all_comments || false }-*/;
   public final native boolean manualReview() /*-{ return this.manual_review || false }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
index 3bd2e77..0908f6b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
@@ -40,6 +40,7 @@
   protected void onLoad() {
     super.onLoad();
     Util.ACCOUNT_SVC.myAgreements(new ScreenLoadCallback<AgreementInfo>(this) {
+      @Override
       public void preDisplay(final AgreementInfo result) {
         agreements.display(result);
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
index e0a6d8f..98f083c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
@@ -76,6 +76,7 @@
     super.onLoad();
     Util.ACCOUNT_SEC
         .myExternalIds(new ScreenLoadCallback<List<AccountExternalId>>(this) {
+          @Override
           public void preDisplay(final List<AccountExternalId> result) {
             identites.display(result);
           }
@@ -126,6 +127,7 @@
         deleteIdentity.setEnabled(false);
         Util.ACCOUNT_SEC.deleteExternalIds(keys,
             new GerritCallback<Set<AccountExternalId.Key>>() {
+              @Override
               public void onSuccess(final Set<AccountExternalId.Key> removed) {
                 for (int row = 1; row < table.getRowCount();) {
                   final AccountExternalId k = getRowItem(row);
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 db1acbf..803ac55 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
@@ -17,7 +17,6 @@
 import static com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DEFAULT_PAGESIZE;
 import static com.google.gerrit.reviewdb.client.AccountGeneralPreferences.PAGESIZE_CHOICES;
 
-import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.StringListPanel;
 import com.google.gerrit.client.config.ConfigServerApi;
@@ -27,7 +26,6 @@
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -49,16 +47,14 @@
   private CheckBox showSiteHeader;
   private CheckBox useFlashClipboard;
   private CheckBox copySelfOnEmails;
-  private CheckBox reversePatchSetOrder;
   private CheckBox relativeDateInChangeTable;
   private CheckBox sizeBarInChangeTable;
   private CheckBox legacycidInChangeTable;
+  private CheckBox muteCommonPathPrefixes;
   private ListBox maximumPageSize;
   private ListBox dateFormat;
   private ListBox timeFormat;
   private ListBox reviewCategoryStrategy;
-  private ListBox commentVisibilityStrategy;
-  private ListBox changeScreen;
   private ListBox diffView;
   private StringListPanel myMenus;
   private Button save;
@@ -70,7 +66,6 @@
     showSiteHeader = new CheckBox(Util.C.showSiteHeader());
     useFlashClipboard = new CheckBox(Util.C.useFlashClipboard());
     copySelfOnEmails = new CheckBox(Util.C.copySelfOnEmails());
-    reversePatchSetOrder = new CheckBox(Util.C.reversePatchSetOrder());
     maximumPageSize = new ListBox();
     for (final short v : PAGESIZE_CHOICES) {
       maximumPageSize.addItem(Util.M.rowsPerPage(v), String.valueOf(v));
@@ -93,32 +88,6 @@
         Util.C.messageShowInReviewCategoryAbbrev(),
         AccountGeneralPreferences.ReviewCategoryStrategy.ABBREV.name());
 
-    commentVisibilityStrategy = new ListBox();
-    commentVisibilityStrategy.addItem(
-        com.google.gerrit.client.changes.Util.C.messageCollapseAll(),
-        AccountGeneralPreferences.CommentVisibilityStrategy.COLLAPSE_ALL.name());
-    commentVisibilityStrategy.addItem(
-        com.google.gerrit.client.changes.Util.C.messageExpandMostRecent(),
-        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_MOST_RECENT.name());
-    commentVisibilityStrategy.addItem(
-        com.google.gerrit.client.changes.Util.C.messageExpandRecent(),
-        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_RECENT.name());
-    commentVisibilityStrategy.addItem(
-        com.google.gerrit.client.changes.Util.C.messageExpandAll(),
-        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(),
@@ -164,8 +133,9 @@
     relativeDateInChangeTable = new CheckBox(Util.C.showRelativeDateInChangeTable());
     sizeBarInChangeTable = new CheckBox(Util.C.showSizeBarInChangeTable());
     legacycidInChangeTable = new CheckBox(Util.C.showLegacycidInChangeTable());
+    muteCommonPathPrefixes = new CheckBox(Util.C.muteCommonPathPrefixes());
 
-    final Grid formGrid = new Grid(13, 2);
+    final Grid formGrid = new Grid(11, 2);
 
     int row = 0;
     formGrid.setText(row, labelIdx, "");
@@ -180,10 +150,6 @@
     formGrid.setWidget(row, fieldIdx, copySelfOnEmails);
     row++;
 
-    formGrid.setText(row, labelIdx, "");
-    formGrid.setWidget(row, fieldIdx, reversePatchSetOrder);
-    row++;
-
     formGrid.setText(row, labelIdx, Util.C.reviewCategoryLabel());
     formGrid.setWidget(row, fieldIdx, reviewCategoryStrategy);
     row++;
@@ -196,32 +162,25 @@
     formGrid.setWidget(row, fieldIdx, dateTimePanel);
     row++;
 
-    if (Gerrit.getConfig().getNewFeatures()) {
-      formGrid.setText(row, labelIdx, "");
-      formGrid.setWidget(row, fieldIdx, relativeDateInChangeTable);
-      row++;
-
-      formGrid.setText(row, labelIdx, "");
-      formGrid.setWidget(row, fieldIdx, sizeBarInChangeTable);
-      row++;
-
-      formGrid.setText(row, labelIdx, "");
-      formGrid.setWidget(row, fieldIdx, legacycidInChangeTable);
-      row++;
-    }
-
-    formGrid.setText(row, labelIdx, Util.C.commentVisibilityLabel());
-    formGrid.setWidget(row, fieldIdx, commentVisibilityStrategy);
+    formGrid.setText(row, labelIdx, "");
+    formGrid.setWidget(row, fieldIdx, relativeDateInChangeTable);
     row++;
 
-    if (Gerrit.getConfig().getNewFeatures()) {
-      formGrid.setText(row, labelIdx, Util.C.changeScreenLabel());
-      formGrid.setWidget(row, fieldIdx, changeScreen);
-      row++;
+    formGrid.setText(row, labelIdx, "");
+    formGrid.setWidget(row, fieldIdx, sizeBarInChangeTable);
+    row++;
 
-      formGrid.setText(row, labelIdx, Util.C.diffViewLabel());
-      formGrid.setWidget(row, fieldIdx, diffView);
-    }
+    formGrid.setText(row, labelIdx, "");
+    formGrid.setWidget(row, fieldIdx, legacycidInChangeTable);
+    row++;
+
+    formGrid.setText(row, labelIdx, "");
+    formGrid.setWidget(row, fieldIdx, muteCommonPathPrefixes);
+    row++;
+
+    formGrid.setText(row, labelIdx, Util.C.diffViewLabel());
+    formGrid.setWidget(row, fieldIdx, diffView);
+
     add(formGrid);
 
     save = new Button(Util.C.buttonSaveChanges());
@@ -242,17 +201,15 @@
     e.listenTo(showSiteHeader);
     e.listenTo(useFlashClipboard);
     e.listenTo(copySelfOnEmails);
-    e.listenTo(reversePatchSetOrder);
     e.listenTo(maximumPageSize);
     e.listenTo(dateFormat);
     e.listenTo(timeFormat);
     e.listenTo(relativeDateInChangeTable);
     e.listenTo(sizeBarInChangeTable);
     e.listenTo(legacycidInChangeTable);
-    e.listenTo(reviewCategoryStrategy);
-    e.listenTo(commentVisibilityStrategy);
-    e.listenTo(changeScreen);
+    e.listenTo(muteCommonPathPrefixes);
     e.listenTo(diffView);
+    e.listenTo(reviewCategoryStrategy);
   }
 
   @Override
@@ -271,16 +228,14 @@
     showSiteHeader.setEnabled(on);
     useFlashClipboard.setEnabled(on);
     copySelfOnEmails.setEnabled(on);
-    reversePatchSetOrder.setEnabled(on);
     maximumPageSize.setEnabled(on);
     dateFormat.setEnabled(on);
     timeFormat.setEnabled(on);
     relativeDateInChangeTable.setEnabled(on);
     sizeBarInChangeTable.setEnabled(on);
     legacycidInChangeTable.setEnabled(on);
+    muteCommonPathPrefixes.setEnabled(on);
     reviewCategoryStrategy.setEnabled(on);
-    commentVisibilityStrategy.setEnabled(on);
-    changeScreen.setEnabled(on);
     diffView.setEnabled(on);
   }
 
@@ -288,7 +243,6 @@
     showSiteHeader.setValue(p.showSiteHeader());
     useFlashClipboard.setValue(p.useFlashClipboard());
     copySelfOnEmails.setValue(p.copySelfOnEmail());
-    reversePatchSetOrder.setValue(p.reversePatchSetOrder());
     setListBox(maximumPageSize, DEFAULT_PAGESIZE, p.changesPerPage());
     setListBox(dateFormat, AccountGeneralPreferences.DateFormat.STD, //
         p.dateFormat());
@@ -297,15 +251,10 @@
     relativeDateInChangeTable.setValue(p.relativeDateInChangeTable());
     sizeBarInChangeTable.setValue(p.sizeBarInChangeTable());
     legacycidInChangeTable.setValue(p.legacycidInChangeTable());
+    muteCommonPathPrefixes.setValue(p.muteCommonPathPrefixes());
     setListBox(reviewCategoryStrategy,
         AccountGeneralPreferences.ReviewCategoryStrategy.NONE,
         p.reviewCategoryStrategy());
-    setListBox(commentVisibilityStrategy,
-        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_RECENT,
-        p.commentVisibilityStrategy());
-    setListBox(changeScreen,
-        null,
-        p.changeScreen());
     setListBox(diffView,
         AccountGeneralPreferences.DiffView.SIDE_BY_SIDE,
         p.diffView());
@@ -376,7 +325,6 @@
     p.setShowSiteHeader(showSiteHeader.getValue());
     p.setUseFlashClipboard(useFlashClipboard.getValue());
     p.setCopySelfOnEmails(copySelfOnEmails.getValue());
-    p.setReversePatchSetOrder(reversePatchSetOrder.getValue());
     p.setMaximumPageSize(getListBox(maximumPageSize, DEFAULT_PAGESIZE));
     p.setDateFormat(getListBox(dateFormat,
         AccountGeneralPreferences.DateFormat.STD,
@@ -387,18 +335,13 @@
     p.setRelativeDateInChangeTable(relativeDateInChangeTable.getValue());
     p.setSizeBarInChangeTable(sizeBarInChangeTable.getValue());
     p.setLegacycidInChangeTable(legacycidInChangeTable.getValue());
+    p.setMuteCommonPathPrefixes(muteCommonPathPrefixes.getValue());
     p.setReviewCategoryStrategy(getListBox(reviewCategoryStrategy,
         ReviewCategoryStrategy.NONE,
         ReviewCategoryStrategy.values()));
-    p.setCommentVisibilityStrategy(getListBox(commentVisibilityStrategy,
-        CommentVisibilityStrategy.EXPAND_RECENT,
-        CommentVisibilityStrategy.values()));
     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);
@@ -409,12 +352,11 @@
     }
 
     AccountApi.self().view("preferences")
-        .post(Preferences.create(p, items), new GerritCallback<Preferences>() {
+        .put(Preferences.create(p, items), new GerritCallback<Preferences>() {
           @Override
           public void onSuccess(Preferences prefs) {
             Gerrit.getUserAccount().setGeneralPreferences(p);
             Gerrit.applyUserPreferences();
-            Dispatcher.changeScreen2 = false;
             enable(true);
             display(prefs);
             Gerrit.refreshMenuBar();
@@ -429,20 +371,6 @@
         });
   }
 
-  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();
-    }
-  }
-
   private class MyMenuPanel extends StringListPanel {
     MyMenuPanel(Button save) {
       super(Util.C.myMenu(), Arrays.asList(Util.C.myMenuName(),
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
index a528912..a8bf3e5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.client.ui.HintTextBox;
 import com.google.gerrit.client.ui.ProjectListPopup;
 import com.google.gerrit.client.ui.ProjectNameSuggestOracle;
+import com.google.gerrit.client.ui.RemoteSuggestBox;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AccountProjectWatchInfo;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -34,21 +35,16 @@
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwt.user.client.ui.HorizontalPanel;
-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 java.util.List;
 
 public class MyWatchedProjectsScreen extends SettingsScreen {
   private Button addNew;
-  private HintTextBox nameBox;
-  private SuggestBox nameTxt;
+  private RemoteSuggestBox nameBox;
   private HintTextBox filterTxt;
   private MyWatchesTable watchesTab;
   private Button browse;
   private Button delSel;
-  private boolean submitOnSelection;
   private Grid grid;
   private ProjectListPopup projectsPopup;
 
@@ -62,7 +58,7 @@
     grid.setStyleName(Gerrit.RESOURCES.css().infoBlock());
     grid.setText(0, 0, Util.C.watchedProjectName());
     final HorizontalPanel hp = new HorizontalPanel();
-    hp.add(nameTxt);
+    hp.add(nameBox);
     hp.add(browse);
     grid.setWidget(0, 1, hp);
 
@@ -108,32 +104,13 @@
   }
 
   protected void createWidgets() {
-    nameBox = new HintTextBox();
-    nameTxt = new SuggestBox(new ProjectNameSuggestOracle(), nameBox);
+    nameBox = new RemoteSuggestBox(new ProjectNameSuggestOracle());
     nameBox.setVisibleLength(50);
     nameBox.setHintText(Util.C.defaultProjectName());
-    nameBox.addKeyPressHandler(new KeyPressHandler() {
+    nameBox.addSelectionHandler(new SelectionHandler<String>() {
       @Override
-      public void onKeyPress(KeyPressEvent event) {
-        submitOnSelection = false;
-
-        if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-          if (((DefaultSuggestionDisplay) nameTxt.getSuggestionDisplay())
-              .isSuggestionListShowing()) {
-            submitOnSelection = true;
-          } else {
-            doAddNew();
-          }
-        }
-      }
-    });
-    nameTxt.addSelectionHandler(new SelectionHandler<Suggestion>() {
-      @Override
-      public void onSelection(SelectionEvent<Suggestion> event) {
-        if (submitOnSelection) {
-          submitOnSelection = false;
-          doAddNew();
-        }
+      public void onSelection(SelectionEvent<String> event) {
+        doAddNew();
       }
     });
 
@@ -196,7 +173,7 @@
   }
 
   protected void doAddNew() {
-    final String projectName = nameTxt.getText().trim();
+    final String projectName = nameBox.getText().trim();
     if ("".equals(projectName)) {
       return;
     }
@@ -213,12 +190,13 @@
 
     Util.ACCOUNT_SVC.addProjectWatch(projectName, filter,
         new GerritCallback<AccountProjectWatchInfo>() {
+          @Override
           public void onSuccess(final AccountProjectWatchInfo result) {
             addNew.setEnabled(true);
             nameBox.setEnabled(true);
             filterTxt.setEnabled(true);
 
-            nameTxt.setText("");
+            nameBox.setText("");
             watchesTab.insertWatch(result);
           }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
index 197dcf8..67f5b4a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
@@ -67,6 +67,7 @@
     if (!ids.isEmpty()) {
       Util.ACCOUNT_SVC.deleteProjectWatches(ids,
           new GerritCallback<VoidResult>() {
+            @Override
             public void onSuccess(final VoidResult result) {
               remove(ids);
             }
@@ -165,6 +166,7 @@
         cbox.setEnabled(false);
         Util.ACCOUNT_SVC.updateProjectWatch(info.getWatch(),
             new GerritCallback<VoidResult>() {
+              @Override
               public void onSuccess(final VoidResult result) {
                 cbox.setEnabled(true);
               }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
index 275937e..95ea317 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
@@ -32,7 +32,6 @@
 import com.google.gwt.http.client.RequestCallback;
 import com.google.gwt.http.client.RequestException;
 import com.google.gwt.http.client.Response;
-import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.FormPanel;
@@ -43,6 +42,7 @@
 import com.google.gwt.user.client.ui.RadioButton;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.HashSet;
@@ -79,6 +79,7 @@
   protected void onLoad() {
     super.onLoad();
     Util.ACCOUNT_SVC.myAgreements(new GerritCallback<AgreementInfo>() {
+      @Override
       public void onSuccess(AgreementInfo result) {
         if (isAttached()) {
           mySigned = new HashSet<>(result.accepted);
@@ -88,6 +89,7 @@
     });
     Gerrit.SYSTEM_SVC
         .contributorAgreements(new GerritCallback<List<ContributorAgreement>>() {
+          @Override
           public void onSuccess(final List<ContributorAgreement> result) {
             if (isAttached()) {
               available = result;
@@ -224,6 +226,7 @@
   private void doEnterAgreement() {
     Util.ACCOUNT_SEC.enterAgreement(current.getName(),
         new GerritCallback<VoidResult>() {
+          @Override
           public void onSuccess(final VoidResult result) {
             Gerrit.display(nextToken);
           }
@@ -247,10 +250,12 @@
       }
       final RequestBuilder rb = new RequestBuilder(RequestBuilder.GET, url);
       rb.setCallback(new RequestCallback() {
+        @Override
         public void onError(Request request, Throwable exception) {
           new ErrorDialog(exception).center();
         }
 
+        @Override
         public void onResponseReceived(Request request, Response response) {
           final String ct = response.getHeader("Content-Type");
           if (response.getStatusCode() == 200 && ct != null
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Preferences.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Preferences.java
index 25036d3..e9bd5f1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Preferences.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Preferences.java
@@ -16,13 +16,11 @@
 
 import com.google.gerrit.client.extensions.TopMenuItem;
 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.ReviewCategoryStrategy;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
@@ -43,14 +41,12 @@
     p.copySelfOnEmail(in.isCopySelfOnEmails());
     p.dateFormat(in.getDateFormat());
     p.timeFormat(in.getTimeFormat());
-    p.reversePatchSetOrder(in.isReversePatchSetOrder());
     p.relativeDateInChangeTable(in.isRelativeDateInChangeTable());
     p.sizeBarInChangeTable(in.isSizeBarInChangeTable());
     p.legacycidInChangeTable(in.isLegacycidInChangeTable());
-    p.commentVisibilityStrategy(in.getCommentVisibilityStrategy());
+    p.muteCommonPathPrefixes(in.isMuteCommonPathPrefixes());
     p.reviewCategoryStrategy(in.getReviewCategoryStrategy());
     p.diffView(in.getDiffView());
-    p.changeScreen(in.getChangeScreen());
     p.setMyMenus(myMenus);
     return p;
   }
@@ -98,9 +94,6 @@
   private final native String timeFormatRaw()
   /*-{ return this.time_format }-*/;
 
-  public final native boolean reversePatchSetOrder()
-  /*-{ return this.reverse_patch_set_order || false }-*/;
-
   public final native boolean relativeDateInChangeTable()
   /*-{ return this.relative_date_in_change_table || false }-*/;
 
@@ -110,6 +103,9 @@
   public final native boolean legacycidInChangeTable()
   /*-{ return this.legacycid_in_change_table || false }-*/;
 
+  public final native boolean muteCommonPathPrefixes()
+  /*-{ return this.mute_common_path_prefixes || false }-*/;
+
   public final ReviewCategoryStrategy reviewCategoryStrategy() {
     String s = reviewCategeoryStrategyRaw();
     return s != null ? ReviewCategoryStrategy.valueOf(s) : ReviewCategoryStrategy.NONE;
@@ -117,13 +113,6 @@
   private final native String reviewCategeoryStrategyRaw()
   /*-{ return this.review_category_strategy }-*/;
 
-  public final CommentVisibilityStrategy commentVisibilityStrategy() {
-    String s = commentVisibilityStrategyRaw();
-    return s != null ? CommentVisibilityStrategy.valueOf(s) : null;
-  }
-  private final native String commentVisibilityStrategyRaw()
-  /*-{ return this.comment_visibility_strategy }-*/;
-
   public final DiffView diffView() {
     String s = diffViewRaw();
     return s != null ? DiffView.valueOf(s) : null;
@@ -131,13 +120,6 @@
   private final native String diffViewRaw()
   /*-{ return this.diff_view }-*/;
 
-  public final ChangeScreen changeScreen() {
-    String s = changeScreenRaw();
-    return s != null ? ChangeScreen.valueOf(s) : null;
-  }
-  private final native String changeScreenRaw()
-  /*-{ return this.change_screen }-*/;
-
   public final native JsArray<TopMenuItem> my()
   /*-{ return this.my; }-*/;
 
@@ -177,9 +159,6 @@
   private final native void timeFormatRaw(String f)
   /*-{ this.time_format = f }-*/;
 
-  public final native void reversePatchSetOrder(boolean r)
-  /*-{ this.reverse_patch_set_order = r }-*/;
-
   public final native void relativeDateInChangeTable(boolean d)
   /*-{ this.relative_date_in_change_table = d }-*/;
 
@@ -189,30 +168,21 @@
   public final native void legacycidInChangeTable(boolean s)
   /*-{ this.legacycid_in_change_table = s }-*/;
 
+  public final native void muteCommonPathPrefixes(boolean s)
+  /*-{ this.mute_common_path_prefixes = s }-*/;
+
   public final void reviewCategoryStrategy(ReviewCategoryStrategy s) {
     reviewCategoryStrategyRaw(s != null ? s.toString() : null);
   }
   private final native void reviewCategoryStrategyRaw(String s)
   /*-{ this.review_category_strategy = s }-*/;
 
-  public final void commentVisibilityStrategy(CommentVisibilityStrategy s) {
-    commentVisibilityStrategyRaw(s != null ? s.toString() : null);
-  }
-  private final native void commentVisibilityStrategyRaw(String s)
-  /*-{ this.comment_visibility_strategy = s }-*/;
-
   public final void diffView(DiffView d) {
     diffViewRaw(d != null ? d.toString() : null);
   }
   private final native void diffViewRaw(String d)
   /*-{ this.diff_view = d }-*/;
 
-  public final void changeScreen(ChangeScreen s) {
-    changeScreenRaw(s != null ? s.toString() : null);
-  }
-  private final native void changeScreenRaw(String s)
-  /*-{ this.change_screen = s }-*/;
-
   final void setMyMenus(List<TopMenuItem> myMenus) {
     initMy();
     for (TopMenuItem n : myMenus) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
index fb1ed8b..0cfc0984 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
@@ -159,6 +159,7 @@
     if (txt != null && txt.length() > 0) {
       addNew.setEnabled(false);
       AccountApi.addSshKey("self", txt, new GerritCallback<SshKeyInfo>() {
+        @Override
         public void onSuccess(final SshKeyInfo k) {
           addNew.setEnabled(true);
           addTxt.setText("");
@@ -198,6 +199,7 @@
     super.onLoad();
     refreshSshKeys();
     Gerrit.SYSTEM_SVC.daemonHostKeys(new GerritCallback<List<SshHostKey>>() {
+      @Override
       public void onSuccess(final List<SshHostKey> result) {
         serverKeys.clear();
         for (final SshHostKey keyInfo : result) {
@@ -272,6 +274,7 @@
         deleteKey.setEnabled(false);
         AccountApi.deleteSshKeys("self", sequenceNumbers,
             new GerritCallback<VoidResult>() {
+              @Override
               public void onSuccess(VoidResult result) {
                 for (int row = 1; row < table.getRowCount();) {
                   final SshKeyInfo k = getRowItem(row);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
index bf4d75c..9975887 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
@@ -106,20 +106,17 @@
       return;
     }
 
+    enableUI(false);
+
     String newName = userNameTxt.getText();
     if ("".equals(newName)) {
       newName = null;
     }
-    if (newName != null && !newName.matches(Account.USER_NAME_PATTERN)) {
-      invalidUserName();
-      return;
-    }
-
-    enableUI(false);
-
     final String newUserName = newName;
+
     Util.ACCOUNT_SEC.changeUserName(newUserName,
         new GerritCallback<VoidResult>() {
+          @Override
           public void onSuccess(final VoidResult result) {
             Gerrit.getUserAccount().setUserName(newUserName);
             userNameLbl.setText(newUserName);
@@ -131,8 +128,8 @@
           @Override
           public void onFailure(final Throwable caught) {
             enableUI(true);
-            if (InvalidUserNameException.MESSAGE.equals(caught.getMessage())) {
-              invalidUserName();
+            if (caught instanceof InvalidUserNameException) {
+              new ErrorDialog(Util.C.invalidUserName()).center();
             } else {
               super.onFailure(caught);
             }
@@ -140,10 +137,6 @@
         });
   }
 
-  private void invalidUserName() {
-    new ErrorDialog(Util.C.invalidUserName()).center();
-  }
-
   private void enableUI(final boolean on) {
     userNameTxt.setEnabled(on);
     setUserName.setEnabled(on);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java
index ab94a75c..2e2d314 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java
@@ -16,9 +16,11 @@
 
 import com.google.gerrit.client.api.ActionContext;
 import com.google.gerrit.client.api.ChangeGlue;
+import com.google.gerrit.client.api.EditGlue;
 import com.google.gerrit.client.api.ProjectGlue;
 import com.google.gerrit.client.api.RevisionGlue;
 import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.EditInfo;
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.projects.BranchInfo;
 import com.google.gerrit.reviewdb.client.Project;
@@ -31,30 +33,37 @@
   private final Project.NameKey project;
   private final BranchInfo branch;
   private final ChangeInfo change;
+  private final EditInfo edit;
   private final RevisionInfo revision;
   private final ActionInfo action;
   private ActionContext ctx;
 
   public ActionButton(Project.NameKey project, ActionInfo action) {
-    this(project, null, null, null, action);
+    this(project, null, null, null, null, action);
   }
 
   public ActionButton(Project.NameKey project, BranchInfo branch,
       ActionInfo action) {
-    this(project, branch, null, null, action);
+    this(project, branch, null, null, null, action);
   }
 
   public ActionButton(ChangeInfo change, ActionInfo action) {
-    this(change, null, action);
+    this(null, null, change, null, null, action);
   }
 
   public ActionButton(ChangeInfo change, RevisionInfo revision,
       ActionInfo action) {
-    this(null, null, change, revision, action);
+    this(null, null, change, null, revision, action);
+  }
+
+  public ActionButton(ChangeInfo change, EditInfo edit,
+      ActionInfo action) {
+    this(null, null, change, edit, null, action);
   }
 
   private ActionButton(Project.NameKey project, BranchInfo branch,
-      ChangeInfo change, RevisionInfo revision, ActionInfo action) {
+      ChangeInfo change, EditInfo edit, RevisionInfo revision,
+      ActionInfo action) {
     super(new SafeHtmlBuilder()
       .openDiv()
       .append(action.label())
@@ -67,6 +76,7 @@
     this.project = project;
     this.branch = branch;
     this.change = change;
+    this.edit = edit;
     this.revision = revision;
     this.action = action;
   }
@@ -81,6 +91,8 @@
 
     if (revision != null) {
       RevisionGlue.onAction(change, revision, action, this);
+    } else if (edit != null) {
+      EditGlue.onAction(change, edit, action, this);
     } else if (change != null) {
       ChangeGlue.onAction(change, action, this);
     } else if (branch != null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
index 3a0d2f7..157748f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
@@ -93,7 +93,7 @@
 
   public AccessSectionEditor(ProjectAccess access) {
     projectAccess = access;
-    permissionSelector = new ValueListBox<String>(
+    permissionSelector = new ValueListBox<>(
         new PermissionNameRenderer(access.getCapabilities()));
     permissionSelector.addValueChangeHandler(new ValueChangeHandler<String>() {
       @Override
@@ -109,17 +109,17 @@
   }
 
   @UiHandler("deleteSection")
-  void onDeleteHover(MouseOverEvent event) {
+  void onDeleteHover(@SuppressWarnings("unused") MouseOverEvent event) {
     normal.addClassName(AdminResources.I.css().deleteSectionHover());
   }
 
   @UiHandler("deleteSection")
-  void onDeleteNonHover(MouseOutEvent event) {
+  void onDeleteNonHover(@SuppressWarnings("unused") MouseOutEvent event) {
     normal.removeClassName(AdminResources.I.css().deleteSectionHover());
   }
 
   @UiHandler("deleteSection")
-  void onDeleteSection(ClickEvent event) {
+  void onDeleteSection(@SuppressWarnings("unused") ClickEvent event) {
     isDeleted = true;
 
     if (name.isVisible()
@@ -139,7 +139,7 @@
   }
 
   @UiHandler("undoDelete")
-  void onUndoDelete(ClickEvent event) {
+  void onUndoDelete(@SuppressWarnings("unused") ClickEvent event) {
     isDeleted = false;
     deleted.getStyle().setDisplay(Display.NONE);
     normal.getStyle().setDisplay(Display.BLOCK);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
index 2cacf35..cd72686 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
@@ -21,14 +21,13 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
 import com.google.gerrit.client.ui.OnEditEnabler;
-import com.google.gerrit.client.ui.RPCSuggestOracle;
+import com.google.gerrit.client.ui.RemoteSuggestBox;
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.SuggestBox;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 import com.google.gwtexpui.globalkey.client.NpTextArea;
@@ -40,8 +39,7 @@
   private NpTextBox groupNameTxt;
   private Button saveName;
 
-  private NpTextBox ownerTxtBox;
-  private SuggestBox ownerTxt;
+  private RemoteSuggestBox ownerTxt;
   private Button saveOwner;
 
   private NpTextArea descTxt;
@@ -66,7 +64,7 @@
 
   private void enableForm(final boolean canModify) {
     groupNameTxt.setEnabled(canModify);
-    ownerTxtBox.setEnabled(canModify);
+    ownerTxt.setEnabled(canModify);
     descTxt.setEnabled(canModify);
     visibleToAllCheckBox.setEnabled(canModify);
   }
@@ -96,6 +94,7 @@
         final String newName = groupNameTxt.getText().trim();
         GroupApi.renameGroup(getGroupUUID(), newName,
             new GerritCallback<com.google.gerrit.client.VoidResult>() {
+              @Override
               public void onSuccess(final com.google.gerrit.client.VoidResult result) {
                 saveName.setEnabled(false);
                 setPageTitle(Util.M.group(newName));
@@ -116,12 +115,10 @@
     ownerPanel.setStyleName(Gerrit.RESOURCES.css().groupOwnerPanel());
     ownerPanel.add(new SmallHeading(Util.C.headingOwner()));
 
-    ownerTxtBox = new NpTextBox();
-    ownerTxtBox.setVisibleLength(60);
     final AccountGroupSuggestOracle accountGroupOracle = new AccountGroupSuggestOracle();
-    ownerTxt = new SuggestBox(new RPCSuggestOracle(
-        accountGroupOracle), ownerTxtBox);
+    ownerTxt = new RemoteSuggestBox(accountGroupOracle);
     ownerTxt.setStyleName(Gerrit.RESOURCES.css().groupOwnerTextBox());
+    ownerTxt.setVisibleLength(60);
     ownerPanel.add(ownerTxt);
 
     saveOwner = new Button(Util.C.buttonChangeGroupOwner());
@@ -135,6 +132,7 @@
           String ownerId = ownerUuid != null ? ownerUuid.get() : newOwner;
           GroupApi.setGroupOwner(getGroupUUID(), ownerId,
               new GerritCallback<GroupInfo>() {
+                @Override
                 public void onSuccess(final GroupInfo result) {
                   updateOwnerGroup(result);
                   saveOwner.setEnabled(false);
@@ -165,6 +163,7 @@
         final String txt = descTxt.getText().trim();
         GroupApi.setGroupDescription(getGroupUUID(), txt,
             new GerritCallback<VoidResult>() {
+              @Override
               public void onSuccess(final VoidResult result) {
                 saveDesc.setEnabled(false);
               }
@@ -193,6 +192,7 @@
       public void onClick(final ClickEvent event) {
         GroupApi.setGroupOptions(getGroupUUID(),
             visibleToAllCheckBox.getValue(), new GerritCallback<VoidResult>() {
+              @Override
               public void onSuccess(final VoidResult result) {
                 saveGroupOptions.setEnabled(false);
               }
@@ -223,6 +223,6 @@
     saveGroupOptions.setVisible(canModify);
     new OnEditEnabler(saveDesc, descTxt);
     new OnEditEnabler(saveName, groupNameTxt);
-    new OnEditEnabler(saveOwner, ownerTxtBox);
+    new OnEditEnabler(saveOwner, ownerTxt.getTextBox());
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
index df74a41..cf3e940 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
 import com.google.gerrit.client.ui.AccountLinkPanel;
+import com.google.gerrit.client.ui.AccountSuggestOracle;
 import com.google.gerrit.client.ui.AddMemberBox;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.Hyperlink;
@@ -80,7 +81,10 @@
 
 
   private void initMemberList() {
-    addMemberBox = new AddMemberBox();
+    addMemberBox = new AddMemberBox(
+        Util.C.buttonAddGroupMember(),
+        Util.C.defaultAccountName(),
+        new AccountSuggestOracle());
 
     addMemberBox.addClickHandler(new ClickHandler() {
       @Override
@@ -172,6 +176,7 @@
     addMemberBox.setEnabled(false);
     GroupApi.addMember(getGroupUUID(), nameEmail,
         new GerritCallback<AccountInfo>() {
+          @Override
           public void onSuccess(final AccountInfo memberInfo) {
             addMemberBox.setEnabled(true);
             addMemberBox.setText("");
@@ -200,6 +205,7 @@
     addIncludeBox.setEnabled(false);
     GroupApi.addIncludedGroup(getGroupUUID(), uuid.get(),
         new GerritCallback<GroupInfo>() {
+          @Override
           public void onSuccess(final GroupInfo result) {
             addIncludeBox.setEnabled(true);
             addIncludeBox.setText("");
@@ -248,6 +254,7 @@
       if (!ids.isEmpty()) {
         GroupApi.removeMembers(getGroupUUID(), ids,
             new GerritCallback<VoidResult>() {
+              @Override
               public void onSuccess(final VoidResult result) {
                 for (int row = 1; row < table.getRowCount();) {
                   final AccountInfo i = getRowItem(row);
@@ -353,6 +360,7 @@
       if (!ids.isEmpty()) {
         GroupApi.removeIncludedGroups(getGroupUUID(), ids,
             new GerritCallback<VoidResult>() {
+              @Override
               public void onSuccess(final VoidResult result) {
                 for (int row = 1; row < table.getRowCount();) {
                   final GroupInfo i = getRowItem(row);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index a92b736..86f543a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -41,6 +41,7 @@
   String useContentMerge();
   String useContributorAgreements();
   String useSignedOffBy();
+  String createNewChangeForAllNotInTarget();
   String requireChangeID();
   String headingMaxObjectSizeLimit();
   String headingGroupOptions();
@@ -135,9 +136,14 @@
   String sectionTypeSection();
   Map<String, String> sectionNames();
 
-  String pagedProjectListPrev();
-  String pagedProjectListNext();
+  String pagedListPrev();
+  String pagedListNext();
 
-  String pagedGroupListPrev();
-  String pagedGroupListNext();
+  String buttonCreate();
+  String buttonCreateDescription();
+  String buttonCreateChange();
+  String buttonCreateChangeDescription();
+  String buttonEditConfig();
+  String buttonEditConfigDescription();
+  String editConfigMessage();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index ef35e00..4446354 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -23,6 +23,7 @@
 useContentMerge = Automatically resolve conflicts
 useContributorAgreements = Require a valid contributor agreement to upload
 useSignedOffBy = Require <code>Signed-off-by</code> in commit message
+createNewChangeForAllNotInTarget = Create a new change for every commit not in the target branch
 requireChangeID = Require <code>Change-Id</code> in commit message
 headingMaxObjectSizeLimit = Maximum Git object size limit
 headingGroupOptions = Group Options
@@ -99,11 +100,8 @@
 errorNoMatchingGroups = No Matching Groups
 errorNoGitRepository = No Git Repository
 
-pagedProjectListPrev = &#x21e6;Prev
-pagedProjectListNext = Next&#x21e8;
-
-pagedGroupListPrev = &#x21e6;Prev
-pagedGroupListNext = Next&#x21e8;
+pagedListPrev = &#x21e6;Prev
+pagedListNext = Next&#x21e8;
 
 addPermission = Add Permission ...
 
@@ -112,6 +110,7 @@
 	abandon, \
 	create, \
 	deleteDrafts, \
+	editHashtags, \
 	editTopicName, \
 	forgeAuthor, \
 	forgeCommitter, \
@@ -132,6 +131,7 @@
 abandon = Abandon
 create = Create Reference
 deleteDrafts = Delete Drafts
+editHashtags = Edit Hashtags
 editTopicName = Edit Topic Name
 forgeAuthor = Forge Author Identity
 forgeCommitter = Forge Committer Identity
@@ -162,3 +162,11 @@
 sectionNames = \
   GLOBAL_CAPABILITIES
 GLOBAL_CAPABILITIES = Global Capabilities
+
+buttonCreate = Create
+buttonCreateDescription = Insert the description of the change.
+buttonCreateChange = Create Change
+buttonCreateChangeDescription = Create change directly in the browser.
+buttonEditConfig = Edit Config
+buttonEditConfigDescription = Creates a change to edit the project configuration in the browser.
+editConfigMessage = Edit Project Config
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java
index cd366f3..f1ac27f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java
@@ -24,9 +24,6 @@
   @Source("admin.css")
   AdminCss css();
 
-  @Source("editText.png")
-  public ImageResource editText();
-
   @Source("deleteNormal.png")
   public ImageResource deleteNormal();
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java
new file mode 100644
index 0000000..1ffd6f0
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.admin;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.CreateChangeDialog;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.PopupPanel;
+
+class CreateChangeAction {
+  static void call(final Button b, final String project) {
+    // TODO Replace CreateChangeDialog with a nicer looking display.
+    b.setEnabled(false);
+    new CreateChangeDialog(new Project.NameKey(project)) {
+      {
+        sendButton.setText(Util.C.buttonCreate());
+        message.setText(Util.C.buttonCreateDescription());
+      }
+
+      @Override
+      public void onSend() {
+        ChangeApi.createChange(project, getDestinationBranch(),
+          message.getText(), null,
+          new GerritCallback<ChangeInfo>() {
+            @Override
+            public void onSuccess(ChangeInfo result) {
+              sent = true;
+              hide();
+              Gerrit.display(PageLinks.toChange(result.legacy_id()));
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+              enableButtons(true);
+              super.onFailure(caught);
+            }
+        });
+      }
+
+      @Override
+      public void onClose(CloseEvent<PopupPanel> event) {
+        super.onClose(event);
+        b.setEnabled(true);
+      }
+
+    }.center();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
index 69dff5c..a2ba5cd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
@@ -128,6 +128,7 @@
 
     addNew.setEnabled(false);
     GroupApi.createGroup(newName, new GerritCallback<GroupInfo>() {
+      @Override
       public void onSuccess(final GroupInfo result) {
         History.newItem(Dispatcher.toGroup(result.getGroupId(),
             AccountGroupScreen.MEMBERS));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
index 0379cc0..3132531 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
@@ -26,11 +26,11 @@
 import com.google.gerrit.client.projects.ProjectInfo;
 import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.HintTextBox;
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.client.ui.ProjectListPopup;
 import com.google.gerrit.client.ui.ProjectNameSuggestOracle;
 import com.google.gerrit.client.ui.ProjectsTable;
+import com.google.gerrit.client.ui.RemoteSuggestBox;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.ProjectUtil;
@@ -49,7 +49,6 @@
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.SuggestBox;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 
@@ -58,8 +57,7 @@
   private NpTextBox project;
   private Button create;
   private Button browse;
-  private HintTextBox parent;
-  private SuggestBox suggestParent;
+  private RemoteSuggestBox parent;
   private CheckBox emptyCommit;
   private CheckBox permissionsOnly;
   private ProjectsTable suggestedParentsTab;
@@ -102,8 +100,8 @@
       @Override
       protected void onMovePointerTo(String projectName) {
         // prevent user input from being overwritten by simply poping up
-        if (!projectsPopup.isPoppingUp() || "".equals(suggestParent.getText())) {
-          suggestParent.setText(projectName);
+        if (!projectsPopup.isPoppingUp() || "".equals(parent.getText())) {
+          parent.setText(projectName);
         }
       }
     };
@@ -191,9 +189,7 @@
   }
 
   private void initParentBox() {
-    parent = new HintTextBox();
-    suggestParent =
-        new SuggestBox(new ProjectNameSuggestOracle(), parent);
+    parent = new RemoteSuggestBox(new ProjectNameSuggestOracle());
     parent.setVisibleLength(50);
   }
 
@@ -210,7 +206,7 @@
 
           @Override
           public void onClick(ClickEvent event) {
-            suggestParent.setText(getRowItem(row).name());
+            parent.setText(getRowItem(row).name());
           }
         });
 
@@ -240,14 +236,14 @@
     grid.setText(0, 0, Util.C.columnProjectName() + ":");
     grid.setWidget(0, 1, project);
     grid.setText(1, 0, Util.C.headingParentProjectName() + ":");
-    grid.setWidget(1, 1, suggestParent);
+    grid.setWidget(1, 1, parent);
     grid.setWidget(1, 2, browse);
     fp.add(grid);
   }
 
   private void doCreateProject() {
     final String projectName = project.getText().trim();
-    final String parentName = suggestParent.getText().trim();
+    final String parentName = parent.getText().trim();
 
     if ("".equals(projectName)) {
       project.setFocus(true);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java
new file mode 100644
index 0000000..86a31ee
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.admin;
+
+import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gwt.user.client.ui.Button;
+
+public class EditConfigAction {
+  static void call(final Button b, final String project) {
+    b.setEnabled(false);
+
+    ChangeApi.createChange(project, RefNames.REFS_CONFIG,
+        Util.C.editConfigMessage(), null, new GerritCallback<ChangeInfo>() {
+          @Override
+          public void onSuccess(ChangeInfo result) {
+            Gerrit.display(Dispatcher.toEditScreen(
+                new PatchSet.Id(result.legacy_id(), 1), "project.config"));
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            b.setEnabled(true);
+            super.onFailure(caught);
+          }
+        });
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
index 6579f83..c5f757d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
@@ -19,10 +19,8 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.groups.GroupMap;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountScreen;
-import com.google.gerrit.client.ui.FilteredUserInterface;
 import com.google.gerrit.client.ui.Hyperlink;
-import com.google.gerrit.client.ui.IgnoreOutdatedFilterResultsCallbackWrapper;
+import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gwt.event.dom.client.KeyCodes;
@@ -33,20 +31,24 @@
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 
-public class GroupListScreen extends AccountScreen implements FilteredUserInterface {
+public class GroupListScreen extends Screen {
   private Hyperlink prev;
   private Hyperlink next;
   private GroupTable groups;
   private NpTextBox filterTxt;
-  private String subname = "";
-  private int startPosition;
   private int pageSize;
 
+  private String match = "";
+  private int start;
+  private Query query;
+
   public GroupListScreen() {
+    setRequiresSignIn(true);
     configurePageSize();
   }
 
   public GroupListScreen(String params) {
+    this();
     for (String kvPair : params.split("[,;&]")) {
       String[] kv = kvPair.split("=", 2);
       if (kv.length != 2 || kv[0].isEmpty()) {
@@ -54,14 +56,13 @@
       }
 
       if ("filter".equals(kv[0])) {
-        subname = URL.decodeQueryString(kv[1]);
+        match = URL.decodeQueryString(kv[1]);
       }
 
       if ("skip".equals(kv[0]) && URL.decodeQueryString(kv[1]).matches("^[\\d]+")) {
-        startPosition = Integer.parseInt(URL.decodeQueryString(kv[1]));
+        start = Integer.parseInt(URL.decodeQueryString(kv[1]));
       }
     }
-    configurePageSize();
   }
 
   private void configurePageSize() {
@@ -78,42 +79,7 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    display();
-    refresh(false, false);
-  }
-
-  private void refresh(final boolean open, final boolean filterModified) {
-    if (filterModified){
-      startPosition = 0;
-    }
-    setToken(getTokenForScreen(subname, startPosition));
-    // Retrieve one more group than page size to determine if there are more
-    // groups to display
-    GroupMap.match(subname, pageSize + 1, startPosition,
-        new IgnoreOutdatedFilterResultsCallbackWrapper<GroupMap>(this,
-            new GerritCallback<GroupMap>() {
-              @Override
-              public void onSuccess(GroupMap result) {
-                if (open && result.values().length() > 0) {
-                  Gerrit.display(PageLinks.toGroup(
-                      result.values().get(0).getGroupUUID()));
-                } else {
-                  if (result.size() <= pageSize) {
-                    groups.display(result, subname);
-                    next.setVisible(false);
-                  } else {
-                    groups.displaySubset(result, 0, result.size() - 1, subname);
-                    setupNavigationLink(next, subname, startPosition + pageSize);
-                  }
-                  if (startPosition > 0) {
-                    setupNavigationLink(prev, subname, startPosition - pageSize);
-                  } else {
-                    prev.setVisible(false);
-                  }
-                  groups.finishDisplay();
-                }
-              }
-            }));
+    query = new Query(match).start(start).run();
   }
 
   private void setupNavigationLink(Hyperlink link, String filter, int skip) {
@@ -138,20 +104,15 @@
   }
 
   @Override
-  public String getCurrentFilter() {
-    return subname;
-  }
-
-  @Override
   protected void onInitUI() {
     super.onInitUI();
     setPageTitle(Util.C.groupListTitle());
     initPageHeader();
 
-    prev = new Hyperlink(Util.C.pagedGroupListPrev(), true, "");
+    prev = new Hyperlink(Util.C.pagedListPrev(), true, "");
     prev.setVisible(false);
 
-    next = new Hyperlink(Util.C.pagedGroupListNext(), true, "");
+    next = new Hyperlink(Util.C.pagedListNext(), true, "");
     next.setVisible(false);
 
     groups = new GroupTable(PageLinks.ADMIN_GROUPS);
@@ -171,16 +132,20 @@
     filterLabel.setStyleName(Gerrit.RESOURCES.css().projectFilterLabel());
     hp.add(filterLabel);
     filterTxt = new NpTextBox();
-    filterTxt.setValue(subname);
+    filterTxt.setValue(match);
     filterTxt.addKeyUpHandler(new KeyUpHandler() {
       @Override
       public void onKeyUp(KeyUpEvent event) {
-        boolean enterPressed =
-            event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER;
-        boolean filterModified = !filterTxt.getValue().equals(subname);
-        if (enterPressed || filterModified) {
-          subname = filterTxt.getValue();
-          refresh(enterPressed, filterModified);
+        Query q = new Query(filterTxt.getValue())
+          .open(event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER);
+        if (match.equals(q.qMatch)) {
+          q.start(start);
+        }
+        if (q.open || !match.equals(q.qMatch)) {
+          if (query == null) {
+            q.run();
+          }
+          query = q;
         }
       }
     });
@@ -191,8 +156,8 @@
   @Override
   public void onShowView() {
     super.onShowView();
-    if (subname != null) {
-      filterTxt.setCursorPos(subname.length());
+    if (match != null) {
+      filterTxt.setCursorPos(match.length());
     }
     filterTxt.setFocus(true);
   }
@@ -202,4 +167,73 @@
     super.registerKeys();
     groups.setRegisterKeys(true);
   }
+
+  private class Query {
+    private final String qMatch;
+    private int qStart;
+    private boolean open;
+
+    Query(String match) {
+      this.qMatch = match;
+    }
+
+    Query start(int start) {
+      this.qStart = start;
+      return this;
+    }
+
+    Query open(boolean open) {
+      this.open = open;
+      return this;
+    }
+
+    Query run() {
+      int limit = open ? 1 : pageSize + 1;
+      GroupMap.match(qMatch, limit, qStart,
+          new GerritCallback<GroupMap>() {
+            @Override
+            public void onSuccess(GroupMap result) {
+              if (!isAttached()) {
+                // View has been disposed.
+              } else if (query == Query.this) {
+                query = null;
+                showMap(result);
+              } else {
+                query.run();
+              }
+            }
+          });
+      return this;
+    }
+
+    private void showMap(GroupMap result) {
+      if (open && !result.isEmpty()) {
+        Gerrit.display(PageLinks.toGroup(
+            result.values().get(0).getGroupUUID()));
+        return;
+      }
+
+      setToken(getTokenForScreen(qMatch, qStart));
+      GroupListScreen.this.match = qMatch;
+      GroupListScreen.this.start = qStart;
+
+      if (result.size() <= pageSize) {
+        groups.display(result, qMatch);
+        next.setVisible(false);
+      } else {
+        groups.displaySubset(result, 0, result.size() - 1, qMatch);
+        setupNavigationLink(next, qMatch, qStart + pageSize);
+      }
+
+      if (qStart > 0) {
+        setupNavigationLink(prev, qMatch, qStart - pageSize);
+      } else {
+        prev.setVisible(false);
+      }
+
+      if (!isCurrentView()) {
+        display();
+      }
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
index 4ddc9a8..c1082bc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
@@ -15,15 +15,10 @@
 package com.google.gerrit.client.admin;
 
 import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
-import com.google.gerrit.client.ui.RPCSuggestOracle;
+import com.google.gerrit.client.ui.RemoteSuggestBox;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.editor.client.LeafValueEditor;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.dom.client.KeyUpEvent;
-import com.google.gwt.event.dom.client.KeyUpHandler;
 import com.google.gwt.event.logical.shared.CloseEvent;
 import com.google.gwt.event.logical.shared.CloseHandler;
 import com.google.gwt.event.logical.shared.HasCloseHandlers;
@@ -33,67 +28,36 @@
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.Focusable;
-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.gwtexpui.globalkey.client.NpTextBox;
 
 public class GroupReferenceBox extends Composite implements
     LeafValueEditor<GroupReference>, HasSelectionHandlers<GroupReference>,
     HasCloseHandlers<GroupReferenceBox>, Focusable {
-  private final DefaultSuggestionDisplay suggestions;
-  private final NpTextBox textBox;
   private final AccountGroupSuggestOracle oracle;
-  private final SuggestBox suggestBox;
-
-  private boolean submitOnSelection;
+  private final RemoteSuggestBox suggestBox;
 
   public GroupReferenceBox() {
-    suggestions = new DefaultSuggestionDisplay();
-    textBox = new NpTextBox();
     oracle = new AccountGroupSuggestOracle();
-    suggestBox = new SuggestBox( //
-        new RPCSuggestOracle(oracle), //
-        textBox, //
-        suggestions);
+    suggestBox = new RemoteSuggestBox(oracle);
     initWidget(suggestBox);
 
-    textBox.addKeyPressHandler(new KeyPressHandler() {
+    suggestBox.addSelectionHandler(new SelectionHandler<String>() {
       @Override
-      public void onKeyPress(KeyPressEvent event) {
-        submitOnSelection = false;
-
-        if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-          if (suggestions.isSuggestionListShowing()) {
-            submitOnSelection = true;
-          } else {
-            SelectionEvent.fire(GroupReferenceBox.this, getValue());
-          }
-        }
+      public void onSelection(SelectionEvent<String> event) {
+        SelectionEvent.fire(GroupReferenceBox.this,
+            toValue(event.getSelectedItem()));
       }
     });
-    suggestBox.addKeyUpHandler(new KeyUpHandler() {
+    suggestBox.addCloseHandler(new CloseHandler<RemoteSuggestBox>(){
       @Override
-      public void onKeyUp(KeyUpEvent event) {
-        if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
-          suggestBox.setText("");
-          CloseEvent.fire(GroupReferenceBox.this, GroupReferenceBox.this);
-        }
-      }
-    });
-    suggestBox.addSelectionHandler(new SelectionHandler<Suggestion>() {
-      @Override
-      public void onSelection(SelectionEvent<Suggestion> event) {
-        if (submitOnSelection) {
-          submitOnSelection = false;
-          SelectionEvent.fire(GroupReferenceBox.this, getValue());
-        }
+      public void onClose(CloseEvent<RemoteSuggestBox> event) {
+        suggestBox.setText("");
+        CloseEvent.fire(GroupReferenceBox.this, GroupReferenceBox.this);
       }
     });
   }
 
   public void setVisibleLength(int len) {
-    textBox.setVisibleLength(len);
+    suggestBox.setVisibleLength(len);
   }
 
   @Override
@@ -110,12 +74,14 @@
 
   @Override
   public GroupReference getValue() {
-    String name = suggestBox.getText();
+    return toValue(suggestBox.getText());
+  }
+
+  private GroupReference toValue(String name) {
     if (name != null && !name.isEmpty()) {
       return new GroupReference(oracle.getUUID(name), name);
-    } else {
-      return null;
     }
+    return null;
   }
 
   @Override
@@ -133,6 +99,7 @@
     suggestBox.setTabIndex(index);
   }
 
+  @Override
   public void setFocus(boolean focused) {
     suggestBox.setFocus(focused);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/MyGroupsListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/MyGroupsListScreen.java
deleted file mode 100644
index cabe2f5..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/MyGroupsListScreen.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (C) 2014 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.admin;
-
-import com.google.gerrit.client.groups.GroupList;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.AccountScreen;
-
-public class MyGroupsListScreen extends AccountScreen {
-  private GroupTable groups;
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    groups = new GroupTable();
-    add(groups);
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    GroupList.my(new ScreenLoadCallback<GroupList>(this) {
-      @Override
-      protected void preDisplay(GroupList result) {
-        groups.display(result);
-        groups.finishDisplay();
-      }});
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
index b3d8564..b9baccc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
@@ -145,31 +145,31 @@
   }
 
   @UiHandler("deletePermission")
-  void onDeleteHover(MouseOverEvent event) {
+  void onDeleteHover(@SuppressWarnings("unused") MouseOverEvent event) {
     addStyleName(AdminResources.I.css().deleteSectionHover());
   }
 
   @UiHandler("deletePermission")
-  void onDeleteNonHover(MouseOutEvent event) {
+  void onDeleteNonHover(@SuppressWarnings("unused") MouseOutEvent event) {
     removeStyleName(AdminResources.I.css().deleteSectionHover());
   }
 
   @UiHandler("deletePermission")
-  void onDeletePermission(ClickEvent event) {
+  void onDeletePermission(@SuppressWarnings("unused") ClickEvent event) {
     isDeleted = true;
     normal.getStyle().setDisplay(Display.NONE);
     deleted.getStyle().setDisplay(Display.BLOCK);
   }
 
   @UiHandler("undoDelete")
-  void onUndoDelete(ClickEvent event) {
+  void onUndoDelete(@SuppressWarnings("unused") ClickEvent event) {
     isDeleted = false;
     deleted.getStyle().setDisplay(Display.NONE);
     normal.getStyle().setDisplay(Display.BLOCK);
   }
 
   @UiHandler("beginAddRule")
-  void onBeginAddRule(ClickEvent event) {
+  void onBeginAddRule(@SuppressWarnings("unused") ClickEvent event) {
     beginAddRule();
   }
 
@@ -186,7 +186,7 @@
   }
 
   @UiHandler("addRule")
-  void onAddGroupByClick(ClickEvent event) {
+  void onAddGroupByClick(@SuppressWarnings("unused") ClickEvent event) {
     GroupReference ref = groupToAdd.getValue();
     if (ref != null) {
       addGroup(ref);
@@ -204,12 +204,13 @@
   }
 
   @UiHandler("groupToAdd")
-  void onAbortAddGroup(CloseEvent<GroupReferenceBox> event) {
+  void onAbortAddGroup(
+      @SuppressWarnings("unused") CloseEvent<GroupReferenceBox> event) {
     hideAddGroup();
   }
 
   @UiHandler("hideAddGroup")
-  void hideAddGroup(ClickEvent event) {
+  void hideAddGroup(@SuppressWarnings("unused") ClickEvent event) {
     hideAddGroup();
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml
index 25995d9..ada070d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml
@@ -39,6 +39,7 @@
   }
 
   .header {
+    position: relative;
     padding-left: 5px;
     padding-right: 5px;
     padding-bottom: 1px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
index 9e719ea..f66307c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
@@ -169,6 +169,10 @@
       deleteRule.removeFromParent();
       deleteRule = null;
     }
+
+    if (name.equals(GlobalCapability.BATCH_CHANGES_LIMIT)) {
+      min.setEnabled(false);
+    }
   }
 
   boolean isDeleted() {
@@ -176,14 +180,14 @@
   }
 
   @UiHandler("deleteRule")
-  void onDeleteRule(ClickEvent event) {
+  void onDeleteRule(@SuppressWarnings("unused") ClickEvent event) {
     isDeleted = true;
     normal.getStyle().setDisplay(Display.NONE);
     deleted.getStyle().setDisplay(Display.BLOCK);
   }
 
   @UiHandler("undoDelete")
-  void onUndoDelete(ClickEvent event) {
+  void onUndoDelete(@SuppressWarnings("unused") ClickEvent event) {
     isDeleted = false;
     deleted.getStyle().setDisplay(Display.NONE);
     normal.getStyle().setDisplay(Display.BLOCK);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
index 8981e82..55bc655 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
@@ -84,7 +84,7 @@
   }
 
   @UiHandler("addSection")
-  void onAddSection(ClickEvent event) {
+  void onAddSection(@SuppressWarnings("unused") ClickEvent event) {
     int index = local.getList().size();
     local.getList().add(new AccessSection("refs/heads/*"));
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
index 9c6cc1d..b86d0a8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
@@ -159,7 +159,7 @@
   }
 
   @UiHandler("edit")
-  void onEdit(ClickEvent event) {
+  void onEdit(@SuppressWarnings("unused") ClickEvent event) {
     resetEditors();
 
     edit.setEnabled(false);
@@ -184,12 +184,12 @@
   }
 
   @UiHandler(value={"cancel1", "cancel2"})
-  void onCancel(ClickEvent event) {
+  void onCancel(@SuppressWarnings("unused") ClickEvent event) {
     Gerrit.display(PageLinks.toProjectAcceess(getProjectKey()));
   }
 
   @UiHandler("commit")
-  void onCommit(ClickEvent event) {
+  void onCommit(@SuppressWarnings("unused") ClickEvent event) {
     final ProjectAccess access = driver.flush();
 
     if (driver.hasErrors()) {
@@ -267,7 +267,7 @@
   }
 
   @UiHandler("review")
-  void onReview(ClickEvent event) {
+  void onReview(@SuppressWarnings("unused") ClickEvent event) {
     final ProjectAccess access = driver.flush();
 
     if (driver.hasErrors()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
index 577bf1d..c5f7225 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
@@ -14,12 +14,15 @@
 
 package com.google.gerrit.client.admin;
 
+import static com.google.gerrit.client.ui.Util.highlight;
+
 import com.google.gerrit.client.ConfirmationCallback;
 import com.google.gerrit.client.ConfirmationDialog;
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.GitwebLink;
 import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.WebLinkInfo;
 import com.google.gerrit.client.access.AccessMap;
 import com.google.gerrit.client.access.ProjectAccessInfo;
 import com.google.gerrit.client.actions.ActionButton;
@@ -30,10 +33,12 @@
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.HintTextBox;
+import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.client.ui.NavigationTable;
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -45,36 +50,101 @@
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
 import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.http.client.URL;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.HorizontalPanel;
 import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.InlineHTML;
 import com.google.gwt.user.client.ui.InlineLabel;
+import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.TextBox;
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
-import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
 public class ProjectBranchesScreen extends ProjectScreen {
+  private Hyperlink prev;
+  private Hyperlink next;
   private BranchesTable branchTable;
   private Button delBranch;
   private Button addBranch;
   private HintTextBox nameTxtBox;
   private HintTextBox irevTxtBox;
   private FlowPanel addPanel;
+  private int pageSize;
+  private int start;
+  private NpTextBox filterTxt;
+  private String match;
+  private Query query;
 
   public ProjectBranchesScreen(final Project.NameKey toShow) {
     super(toShow);
+    configurePageSize();
+  }
+
+  private void configurePageSize() {
+    if (Gerrit.isSignedIn()) {
+      AccountGeneralPreferences p =
+          Gerrit.getUserAccount().getGeneralPreferences();
+      short m = p.getMaximumPageSize();
+      pageSize = 0 < m ? m : AccountGeneralPreferences.DEFAULT_PAGESIZE;
+    } else {
+      pageSize = AccountGeneralPreferences.DEFAULT_PAGESIZE;
+    }
+  }
+
+  private void parseToken() {
+    String token = getToken();
+
+    for (String kvPair : token.split("[,;&/?]")) {
+      String[] kv = kvPair.split("=", 2);
+      if (kv.length != 2 || kv[0].isEmpty()) {
+        continue;
+      }
+
+      if ("filter".equals(kv[0])) {
+        match = URL.decodeQueryString(kv[1]);
+      }
+
+      if ("skip".equals(kv[0])
+          && URL.decodeQueryString(kv[1]).matches("^[\\d]+")) {
+        start = Integer.parseInt(URL.decodeQueryString(kv[1]));
+      }
+    }
+  }
+
+  private void setupNavigationLink(Hyperlink link, String filter, int skip) {
+    link.setTargetHistoryToken(getTokenForScreen(filter, skip));
+    link.setVisible(true);
+  }
+
+  private String getTokenForScreen(String filter, int skip) {
+    String token = PageLinks.toProjectBranches(getProjectKey());
+    if (filter != null && !filter.isEmpty()) {
+      token += "?filter=" + URL.encodeQueryString(filter);
+    }
+    if (skip > 0) {
+      if (token.contains("?filter=")) {
+        token += ",";
+      } else {
+        token += "?";
+      }
+      token += "skip=" + skip;
+    }
+    return token;
   }
 
   @Override
@@ -88,28 +158,10 @@
             addPanel.setVisible(result.canAddRefs());
           }
         });
-    refreshBranches();
+    query = new Query(match).start(start).run();
     savedPanel = BRANCH;
   }
 
-  private void refreshBranches() {
-    ProjectApi.getBranches(getProjectKey(),
-        new ScreenLoadCallback<JsArray<BranchInfo>>(this) {
-          @Override
-          public void preDisplay(final JsArray<BranchInfo> result) {
-            Set<String> checkedRefs = branchTable.getCheckedRefs();
-            display(Natives.asList(result));
-            branchTable.setChecked(checkedRefs);
-            updateForm();
-          }
-        });
-  }
-
-  private void display(final List<BranchInfo> branches) {
-    branchTable.display(branches);
-    delBranch.setVisible(branchTable.hasBranchCanDelete());
-  }
-
   private void updateForm() {
     branchTable.updateDeleteButton();
     addBranch.setEnabled(true);
@@ -120,6 +172,13 @@
   @Override
   protected void onInitUI() {
     super.onInitUI();
+    initPageHeader();
+
+    prev = new Hyperlink(Util.C.pagedListPrev(), true, "");
+    prev.setVisible(false);
+
+    next = new Hyperlink(Util.C.pagedListNext(), true, "");
+    next.setVisible(false);
 
     addPanel = new FlowPanel();
 
@@ -174,12 +233,41 @@
         branchTable.deleteChecked();
       }
     });
-
+    HorizontalPanel buttons = new HorizontalPanel();
+    buttons.setStyleName(Gerrit.RESOURCES.css().branchTablePrevNextLinks());
+    buttons.add(delBranch);
+    buttons.add(prev);
+    buttons.add(next);
     add(branchTable);
-    add(delBranch);
+    add(buttons);
     add(addPanel);
   }
 
+  private void initPageHeader() {
+    parseToken();
+    HorizontalPanel hp = new HorizontalPanel();
+    hp.setStyleName(Gerrit.RESOURCES.css().projectFilterPanel());
+    Label filterLabel = new Label(Util.C.projectFilter());
+    filterLabel.setStyleName(Gerrit.RESOURCES.css().projectFilterLabel());
+    hp.add(filterLabel);
+    filterTxt = new NpTextBox();
+    filterTxt.setValue(match);
+    filterTxt.addKeyUpHandler(new KeyUpHandler() {
+      @Override
+      public void onKeyUp(KeyUpEvent event) {
+        Query q = new Query(filterTxt.getValue());
+        if (match.equals(q.qMatch)) {
+          q.start(start);
+        } else if (query == null) {
+          q.run();
+          query = q;
+        }
+      }
+    });
+    hp.add(filterTxt);
+    add(hp);
+  }
+
   private void doAddNewBranch() {
     final String branchName = nameTxtBox.getText().trim();
     if ("".equals(branchName)) {
@@ -205,28 +293,49 @@
         new GerritCallback<BranchInfo>() {
           @Override
           public void onSuccess(BranchInfo branch) {
-            addBranch.setEnabled(true);
+            showAddedBranch(branch);
             nameTxtBox.setText("");
             irevTxtBox.setText("");
-            branchTable.insert(branch);
-            delBranch.setVisible(branchTable.hasBranchCanDelete());
+            query = new Query(match).start(start).run();
           }
 
-      @Override
-      public void onFailure(Throwable caught) {
-        addBranch.setEnabled(true);
-        selectAllAndFocus(nameTxtBox);
-        new ErrorDialog(caught.getMessage()).center();
-      }
-    });
+          @Override
+          public void onFailure(Throwable caught) {
+            addBranch.setEnabled(true);
+            selectAllAndFocus(nameTxtBox);
+            new ErrorDialog(caught.getMessage()).center();
+          }
+        });
   }
 
-  private static void selectAllAndFocus(final TextBox textBox) {
+  void showAddedBranch(BranchInfo branch) {
+    SafeHtmlBuilder b = new SafeHtmlBuilder();
+    b.openElement("b");
+    b.append(Gerrit.C.branchCreationConfirmationMessage());
+    b.closeElement("b");
+
+    b.openElement("p");
+    b.append(branch.ref());
+    b.closeElement("p");
+
+    ConfirmationDialog confirmationDialog =
+        new ConfirmationDialog(Gerrit.C.branchCreationDialogTitle(),
+            b.toSafeHtml(), new ConfirmationCallback() {
+      @Override
+      public void onOk() {
+        //do nothing
+      }
+    });
+    confirmationDialog.center();
+    confirmationDialog.setCancelVisible(false);
+  }
+
+  private static void selectAllAndFocus(TextBox textBox) {
     textBox.selectAll();
     textBox.setFocus(true);
   }
 
-  private class BranchesTable extends FancyFlexTable<BranchInfo> {
+  private class BranchesTable extends NavigationTable<BranchInfo> {
     private ValueChangeHandler<Boolean> updateDeleteHandler;
     boolean canDelete;
 
@@ -239,9 +348,7 @@
       fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader());
       fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
       fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
-      if (Gerrit.getGitwebLink() != null) {
-        fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
-      }
+      fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
 
       updateDeleteHandler = new ValueChangeHandler<Boolean>() {
         @Override
@@ -317,34 +424,30 @@
     private void deleteBranches(final Set<String> branches) {
       ProjectApi.deleteBranches(getProjectKey(), branches,
           new GerritCallback<VoidResult>() {
+            @Override
             public void onSuccess(VoidResult result) {
-              for (int row = 1; row < table.getRowCount();) {
-                BranchInfo k = getRowItem(row);
-                if (k != null && branches.contains(k.ref())) {
-                  table.removeRow(row);
-                } else {
-                  row++;
-                }
-              }
-              updateDeleteButton();
-              delBranch.setVisible(branchTable.hasBranchCanDelete());
+              query = new Query(match).start(start).run();
             }
 
             @Override
             public void onFailure(Throwable caught) {
-              refreshBranches();
+              query = new Query(match).start(start).run();
               super.onFailure(caught);
             }
           });
     }
 
     void display(List<BranchInfo> branches) {
+      displaySubset(branches, 0, branches.size());
+    }
+
+    void displaySubset(List<BranchInfo> branches, int fromIndex, int toIndex) {
       canDelete = false;
 
       while (1 < table.getRowCount())
         table.removeRow(table.getRowCount() - 1);
 
-      for (final BranchInfo k : branches) {
+      for (BranchInfo k : branches.subList(fromIndex, toIndex)) {
         final int row = table.getRowCount();
         table.insertRow(row);
         applyDataRowStyle(row);
@@ -352,21 +455,6 @@
       }
     }
 
-    void insert(BranchInfo info) {
-      Comparator<BranchInfo> c = new Comparator<BranchInfo>() {
-        @Override
-        public int compare(BranchInfo a, BranchInfo b) {
-          return a.ref().compareTo(b.ref());
-        }
-      };
-      int insertPos = getInsertRow(c, info);
-      if (insertPos >= 0) {
-        table.insertRow(insertPos);
-        applyDataRowStyle(insertPos);
-        populate(insertPos, info);
-      }
-    }
-
     void populate(int row, BranchInfo k) {
       final GitwebLink c = Gerrit.getGitwebLink();
 
@@ -379,7 +467,7 @@
         table.setText(row, 1, "");
       }
 
-      table.setText(row, 2, k.getShortName());
+      table.setWidget(row, 2, new InlineHTML(highlight(k.getShortName(), match)));
 
       if (k.revision() != null) {
         if ("HEAD".equals(k.getShortName())) {
@@ -396,6 +484,11 @@
         actionsPanel.add(new Anchor(c.getLinkName(), false,
             c.toBranch(new Branch.NameKey(getProjectKey(), k.ref()))));
       }
+      if (k.web_links() != null) {
+        for (WebLinkInfo webLink : Natives.asList(k.web_links())) {
+          actionsPanel.add(webLink.toAnchor());
+        }
+      }
       if (k.actions() != null) {
         k.actions().copyKeysIntoChildren("id");
         for (ActionInfo a : Natives.asList(k.actions().values())) {
@@ -416,9 +509,7 @@
       fmt.addStyleName(row, 1, iconCellStyle);
       fmt.addStyleName(row, 2, dataCellStyle);
       fmt.addStyleName(row, 3, dataCellStyle);
-      if (c != null) {
-        fmt.addStyleName(row, 4, dataCellStyle);
-      }
+      fmt.addStyleName(row, 4, dataCellStyle);
 
       setRowItem(row, k);
     }
@@ -527,5 +618,89 @@
       }
       delBranch.setEnabled(on);
     }
+
+    @Override
+    protected void onOpenRow(int row) {
+      if (row > 0) {
+        movePointerTo(row);
+      }
+    }
+
+    @Override
+    protected Object getRowItemKey(BranchInfo item) {
+      return item.ref();
+    }
+  }
+
+  @Override
+  public void onShowView() {
+    super.onShowView();
+    if (match != null) {
+      filterTxt.setCursorPos(match.length());
+    }
+    filterTxt.setFocus(true);
+  }
+
+  private class Query {
+    private String qMatch;
+    private int qStart;
+
+    Query(String match) {
+      this.qMatch = match;
+    }
+
+    Query start(int start) {
+      this.qStart = start;
+      return this;
+    }
+
+    Query run() {
+      // Retrieve one more branch than page size to determine if there are more
+      // branches to display
+      ProjectApi.getBranches(getProjectKey(), pageSize + 1, qStart, qMatch,
+              new ScreenLoadCallback<JsArray<BranchInfo>>(ProjectBranchesScreen.this) {
+                @Override
+                public void preDisplay(JsArray<BranchInfo> result) {
+                  if (!isAttached()) {
+                    // View has been disposed.
+                  } else if (query == Query.this) {
+                    query = null;
+                    showList(result);
+                  } else {
+                    query.run();
+                  }
+                }
+          });
+      return this;
+    }
+
+    void showList(JsArray<BranchInfo> result) {
+      setToken(getTokenForScreen(qMatch, qStart));
+      ProjectBranchesScreen.this.match = qMatch;
+      ProjectBranchesScreen.this.start = qStart;
+
+      if (result.length() <= pageSize) {
+        branchTable.display(Natives.asList(result));
+        next.setVisible(false);
+      } else {
+        branchTable.displaySubset(Natives.asList(result), 0,
+            result.length() - 1);
+        setupNavigationLink(next, qMatch, qStart + pageSize);
+      }
+      if (qStart > 0) {
+        setupNavigationLink(prev, qMatch, qStart - pageSize);
+      } else {
+        prev.setVisible(false);
+      }
+
+      delBranch.setVisible(branchTable.hasBranchCanDelete());
+      Set<String> checkedRefs = branchTable.getCheckedRefs();
+      branchTable.setChecked(checkedRefs);
+      updateForm();
+
+      if (!isCurrentView()) {
+        display();
+      }
+    }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index 24ee27d..6cb9295 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -35,9 +35,9 @@
 import com.google.gerrit.client.ui.NpIntTextBox;
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.extensions.api.projects.ProjectState;
-import com.google.gerrit.extensions.common.InheritableBoolean;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.event.dom.client.ChangeEvent;
@@ -79,6 +79,7 @@
   private ListBox submitType;
   private ListBox state;
   private ListBox contentMerge;
+  private ListBox newChangeForAllNotInTarget;
   private NpTextBox maxObjectSizeLimit;
   private Label effectiveMaxObjectSizeLimit;
   private Map<String, Map<String, HasEnabled>> pluginConfigWidgets;
@@ -102,6 +103,7 @@
 
     Resources.I.style().ensureInjected();
     saveProject = new Button(Util.C.buttonSaveChanges());
+    saveProject.setStyleName("");
     saveProject.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(ClickEvent event) {
@@ -157,6 +159,7 @@
     state.setEnabled(isOwner);
     submitType.setEnabled(isOwner);
     setEnabledForUseContentMerge();
+    newChangeForAllNotInTarget.setEnabled(isOwner);
     descTxt.setEnabled(isOwner);
     contributorAgreements.setEnabled(isOwner);
     signedOffBy.setEnabled(isOwner);
@@ -213,6 +216,10 @@
     saveEnabler.listenTo(contentMerge);
     grid.add(Util.C.useContentMerge(), contentMerge);
 
+    newChangeForAllNotInTarget = newInheritedBooleanBox();
+    saveEnabler.listenTo(newChangeForAllNotInTarget);
+    grid.add(Util.C.createNewChangeForAllNotInTarget(), newChangeForAllNotInTarget);
+
     requireChangeID = newInheritedBooleanBox();
     saveEnabler.listenTo(requireChangeID);
     grid.addHtml(Util.C.requireChangeID(), requireChangeID);
@@ -338,6 +345,7 @@
     setBool(contributorAgreements, result.use_contributor_agreements());
     setBool(signedOffBy, result.use_signed_off_by());
     setBool(contentMerge, result.use_content_merge());
+    setBool(newChangeForAllNotInTarget, result.create_new_change_for_all_not_in_target());
     setBool(requireChangeID, result.require_change_id());
     setSubmitType(result.submit_type());
     setState(result.state());
@@ -547,9 +555,13 @@
   private void initProjectActions(ConfigInfo info) {
     actionsGrid.clear(true);
     actionsGrid.removeAllRows();
+    boolean showCreateChange = Gerrit.isSignedIn();
 
     NativeMap<ActionInfo> actions = info.actions();
-    if (actions == null || actions.isEmpty()) {
+    if (actions == null) {
+      actions = NativeMap.create().cast();
+    }
+    if (actions.isEmpty() && !showCreateChange) {
       return;
     }
     actions.copyKeysIntoChildren("id");
@@ -558,10 +570,47 @@
     actionsPanel.setStyleName(Gerrit.RESOURCES.css().projectActions());
     actionsPanel.setVisible(true);
     actionsGrid.add(Util.C.headingCommands(), actionsPanel);
+
     for (String id : actions.keySet()) {
       actionsPanel.add(new ActionButton(getProjectKey(),
           actions.get(id)));
     }
+
+    // TODO: The user should have create permission on the branch referred to by
+    // HEAD. This would have to happen on the server side.
+    if (showCreateChange) {
+      actionsPanel.add(createChangeAction());
+    }
+
+    if (isOwner) {
+      actionsPanel.add(createEditConfigAction());
+    }
+  }
+
+  private Button createChangeAction() {
+    final Button createChange = new Button(Util.C.buttonCreateChange());
+    createChange.setStyleName("");
+    createChange.setTitle(Util.C.buttonCreateChangeDescription());
+    createChange.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        CreateChangeAction.call(createChange, getProjectKey().get());
+      }
+    });
+    return createChange;
+  }
+
+  private Button createEditConfigAction() {
+    final Button editConfig = new Button(Util.C.buttonEditConfig());
+    editConfig.setStyleName("");
+    editConfig.setTitle(Util.C.buttonEditConfigDescription());
+    editConfig.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        EditConfigAction.call(editConfig, getProjectKey().get());
+      }
+    });
+    return editConfig;
   }
 
   private void doSave() {
@@ -569,7 +618,7 @@
     saveProject.setEnabled(false);
     ProjectApi.setConfig(getProjectKey(), descTxt.getText().trim(),
         getBool(contributorAgreements), getBool(contentMerge),
-        getBool(signedOffBy), getBool(requireChangeID),
+        getBool(signedOffBy), getBool(newChangeForAllNotInTarget), getBool(requireChangeID),
         maxObjectSizeLimit.getText().trim(),
         SubmitType.valueOf(submitType.getValue(submitType.getSelectedIndex())),
         ProjectState.valueOf(state.getValue(state.getSelectedIndex())),
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
index 3bd05e0..828352c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
@@ -24,10 +24,8 @@
 import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.FilteredUserInterface;
 import com.google.gerrit.client.ui.HighlightingInlineHyperlink;
 import com.google.gerrit.client.ui.Hyperlink;
-import com.google.gerrit.client.ui.IgnoreOutdatedFilterResultsCallbackWrapper;
 import com.google.gerrit.client.ui.ProjectSearchLink;
 import com.google.gerrit.client.ui.ProjectsTable;
 import com.google.gerrit.client.ui.Screen;
@@ -47,15 +45,17 @@
 
 import java.util.List;
 
-public class ProjectListScreen extends Screen implements FilteredUserInterface {
+public class ProjectListScreen extends Screen {
   private Hyperlink prev;
   private Hyperlink next;
   private ProjectsTable projects;
   private NpTextBox filterTxt;
-  private String subname = "";
-  private int startPosition;
   private int pageSize;
 
+  private String match = "";
+  private int start;
+  private Query query;
+
   public ProjectListScreen() {
     configurePageSize();
   }
@@ -68,11 +68,11 @@
       }
 
       if ("filter".equals(kv[0])) {
-        subname = URL.decodeQueryString(kv[1]);
+        match = URL.decodeQueryString(kv[1]);
       }
 
       if ("skip".equals(kv[0]) && URL.decodeQueryString(kv[1]).matches("^[\\d]+")) {
-        startPosition = Integer.parseInt(URL.decodeQueryString(kv[1]));
+        start = Integer.parseInt(URL.decodeQueryString(kv[1]));
       }
     }
     configurePageSize();
@@ -92,41 +92,7 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    display();
-    refresh(false, false);
-  }
-
-  private void refresh(final boolean open, final boolean filterModified) {
-    if (filterModified){
-      startPosition = 0;
-    }
-    setToken(getTokenForScreen(subname, startPosition));
-    // Retrieve one more project than page size to determine if there are more
-    // projects to display
-    ProjectMap.match(subname, pageSize + 1, startPosition,
-        new IgnoreOutdatedFilterResultsCallbackWrapper<ProjectMap>(this,
-            new GerritCallback<ProjectMap>() {
-              @Override
-              public void onSuccess(ProjectMap result) {
-                if (open && result.values().length() > 0) {
-                  Gerrit.display(PageLinks.toProject(
-                      result.values().get(0).name_key()));
-                } else {
-                  if (result.size() <= pageSize) {
-                    projects.display(result);
-                    next.setVisible(false);
-                  } else {
-                    projects.displaySubset(result, 0, result.size() - 1);
-                    setupNavigationLink(next, subname, startPosition + pageSize);
-                  }
-                  if (startPosition > 0) {
-                    setupNavigationLink(prev, subname, startPosition - pageSize);
-                  } else {
-                    prev.setVisible(false);
-                  }
-                }
-              }
-            }));
+    query = new Query(match).start(start).run();
   }
 
   private void setupNavigationLink(Hyperlink link, String filter, int skip) {
@@ -151,20 +117,15 @@
   }
 
   @Override
-  public String getCurrentFilter() {
-    return subname;
-  }
-
-  @Override
   protected void onInitUI() {
     super.onInitUI();
     setPageTitle(Util.C.projectListTitle());
     initPageHeader();
 
-    prev = new Hyperlink(Util.C.pagedProjectListPrev(), true, "");
+    prev = new Hyperlink(Util.C.pagedListPrev(), true, "");
     prev.setVisible(false);
 
-    next = new Hyperlink(Util.C.pagedProjectListNext(), true, "");
+    next = new Hyperlink(Util.C.pagedListNext(), true, "");
     next.setVisible(false);
 
     projects = new ProjectsTable() {
@@ -215,7 +176,7 @@
 
         FlowPanel fp = new FlowPanel();
         fp.add(new ProjectSearchLink(k.name_key()));
-        fp.add(new HighlightingInlineHyperlink(k.name(), link(k), subname));
+        fp.add(new HighlightingInlineHyperlink(k.name(), link(k), match));
         table.setWidget(row, ProjectsTable.C_NAME, fp);
         table.setText(row, ProjectsTable.C_DESCRIPTION, k.description());
         addWebLinks(row, k);
@@ -236,12 +197,10 @@
             a.setHref(gitWebLink.toProject(k.name_key()));
             p.add(a);
           }
-
-          for (WebLinkInfo weblink : webLinks) {
-            Anchor a = new Anchor();
-            a.setText("(" + weblink.name() + ")");
-            a.setHref(weblink.url());
-            p.add(a);
+          if (webLinks != null) {
+            for (WebLinkInfo weblink : webLinks) {
+              p.add(weblink.toAnchor());
+            }
           }
         }
       }
@@ -263,16 +222,20 @@
     filterLabel.setStyleName(Gerrit.RESOURCES.css().projectFilterLabel());
     hp.add(filterLabel);
     filterTxt = new NpTextBox();
-    filterTxt.setValue(subname);
+    filterTxt.setValue(match);
     filterTxt.addKeyUpHandler(new KeyUpHandler() {
       @Override
       public void onKeyUp(KeyUpEvent event) {
-        boolean enterPressed =
-            event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER;
-        boolean filterModified = !filterTxt.getValue().equals(subname);
-        if (enterPressed || filterModified) {
-          subname = filterTxt.getValue();
-          refresh(enterPressed, filterModified);
+        Query q = new Query(filterTxt.getValue())
+          .open(event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER);
+        if (match.equals(q.qMatch)) {
+          q.start(start);
+        }
+        if (q.open || !match.equals(q.qMatch)) {
+          if (query == null) {
+            q.run();
+          }
+          query = q;
         }
       }
     });
@@ -283,8 +246,8 @@
   @Override
   public void onShowView() {
     super.onShowView();
-    if (subname != null) {
-      filterTxt.setCursorPos(subname.length());
+    if (match != null) {
+      filterTxt.setCursorPos(match.length());
     }
     filterTxt.setFocus(true);
   }
@@ -294,4 +257,72 @@
     super.registerKeys();
     projects.setRegisterKeys(true);
   }
+
+  private class Query {
+    private final String qMatch;
+    private int qStart;
+    private boolean open;
+
+    Query(String match) {
+      this.qMatch = match;
+    }
+
+    Query start(int start) {
+      this.qStart = start;
+      return this;
+    }
+
+    Query open(boolean open) {
+      this.open = open;
+      return this;
+    }
+
+    Query run() {
+      int limit = open ? 1 : pageSize + 1;
+      ProjectMap.match(qMatch, limit, qStart,
+          new GerritCallback<ProjectMap>() {
+            @Override
+            public void onSuccess(ProjectMap result) {
+              if (!isAttached()) {
+                // View has been disposed.
+              } else if (query == Query.this) {
+                query = null;
+                showMap(result);
+              } else {
+                query.run();
+              }
+            }
+          });
+      return this;
+    }
+
+    private void showMap(ProjectMap result) {
+      if (open && !result.isEmpty()) {
+        Gerrit.display(PageLinks.toProject(result.values().get(0).name_key()));
+        return;
+      }
+
+      setToken(getTokenForScreen(qMatch, qStart));
+      ProjectListScreen.this.match = qMatch;
+      ProjectListScreen.this.start = qStart;
+
+      if (result.size() <= pageSize) {
+        projects.display(result);
+        next.setVisible(false);
+      } else {
+        projects.displaySubset(result, 0, result.size() - 1);
+        setupNavigationLink(next, qMatch, qStart + pageSize);
+      }
+
+      if (qStart > 0) {
+        setupNavigationLink(prev, qMatch, qStart - pageSize);
+      } else {
+        prev.setVisible(false);
+      }
+
+      if (!isCurrentView()) {
+        display();
+      }
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RefPatternBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RefPatternBox.java
index 0cf5d48..e8e88cc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RefPatternBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RefPatternBox.java
@@ -27,16 +27,19 @@
 
 public class RefPatternBox extends ValueBox<String> {
   private static final Renderer<String> RENDERER = new Renderer<String>() {
+    @Override
     public String render(String ref) {
       return ref;
     }
 
+    @Override
     public void render(String ref, Appendable dst) throws IOException {
       dst.append(render(ref));
     }
   };
 
   private static final Parser<String> PARSER = new Parser<String>() {
+    @Override
     public String parse(CharSequence text) throws ParseException {
       String ref = text.toString();
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java
index 5b8fe38..81286ea 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.client.admin;
 
 import com.google.gerrit.common.data.ProjectAdminService;
-import com.google.gerrit.extensions.api.projects.ProjectState;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gwt.core.client.GWT;
 import com.google.gwtjsonrpc.client.JsonUtil;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.java
index a7463b4..60b11c2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.java
@@ -86,6 +86,7 @@
     editPanel.setVisible(true);
   }
 
+  @Override
   public ValueBoxEditor<T> asEditor() {
     if (editProxy == null) {
       editProxy = new EditorProxy();
@@ -133,6 +134,7 @@
     startHandlers.enabled = enabled;
   }
 
+  @Override
   public void showErrors(List<EditorError> errors) {
     StringBuilder buf = new StringBuilder();
     for (EditorError error : errors) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml
index 9862848..e5f6649 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml
@@ -18,7 +18,7 @@
   xmlns:ui='urn:ui:com.google.gwt.uibinder'
   xmlns:g='urn:import:com.google.gwt.user.client.ui'
   >
-<ui:with field='res' type='com.google.gerrit.client.admin.AdminResources'/>
+<ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
 <ui:style>
   .panel {
     position: relative;
@@ -49,7 +49,7 @@
 <g:HTMLPanel stylePrimaryName='{style.panel}'>
   <g:Image
       ui:field='editIcon'
-      resource='{res.editText}'
+      resource='{ico.edit}'
       stylePrimaryName='{style.editIcon}'
       title='Edit'>
     <ui:attribute name='title'/>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/editText.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/editText.png
deleted file mode 100644
index 2927275..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/editText.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java
index 7490d82..2b40954 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.client.actions.ActionButton;
 import com.google.gerrit.client.actions.ActionInfo;
 import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.EditInfo;
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.projects.BranchInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
@@ -39,6 +40,7 @@
       go: Gerrit.go,
       refresh: Gerrit.refresh,
       refreshMenuBar: Gerrit.refreshMenuBar,
+      isSignedIn: Gerrit.isSignedIn,
       showError: Gerrit.showError,
 
       br: function(){return doc.createElement('br')},
@@ -137,6 +139,7 @@
 
   final native void set(ActionInfo a) /*-{ this.action=a; }-*/;
   final native void set(ChangeInfo c) /*-{ this.change=c; }-*/;
+  final native void set(EditInfo e) /*-{ this.edit=e; }-*/;
   final native void set(Project.NameKey p) /*-{ this.project=p; }-*/;
   final native void set(BranchInfo b) /*-{ this.branch=b }-*/;
   final native void set(RevisionInfo r) /*-{ this.revision=r; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
index 8da896e..b52efe8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.account.AccountInfo;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.History;
@@ -41,6 +42,7 @@
       plugins: {},
       screens: {},
       change_actions: {},
+      edit_actions: {},
       revision_actions: {},
       project_actions: {},
       branch_actions: {},
@@ -64,13 +66,16 @@
       go: @com.google.gerrit.client.api.ApiGlue::go(Ljava/lang/String;),
       refresh: @com.google.gerrit.client.api.ApiGlue::refresh(),
       refreshMenuBar: @com.google.gerrit.client.api.ApiGlue::refreshMenuBar(),
+      isSignedIn: @com.google.gerrit.client.api.ApiGlue::isSignedIn(),
       showError: @com.google.gerrit.client.api.ApiGlue::showError(Ljava/lang/String;),
+      getCurrentUser: @com.google.gerrit.client.api.ApiGlue::getCurrentUser(),
 
       on: function (e,f){(this.events[e] || (this.events[e]=[])).push(f)},
       onAction: function (t,n,c){this._onAction(this.getPluginName(),t,n,c)},
       _onAction: function (p,t,n,c) {
         var i = p+'~'+n;
         if ('change' == t) this.change_actions[i]=c;
+        else if ('edit' == t) this.edit_actions[i]=c;
         else if ('revision' == t) this.revision_actions[i]=c;
         else if ('project' == t) this.project_actions[i]=c;
         else if ('branch' == t) this.branch_actions[i]=c;
@@ -192,10 +197,18 @@
     Gerrit.display(History.getToken());
   }
 
+  private static final AccountInfo getCurrentUser() {
+    return Gerrit.getUserAccountInfo();
+  }
+
   private static final void refreshMenuBar() {
     Gerrit.refreshMenuBar();
   }
 
+  private static final boolean isSignedIn() {
+    return Gerrit.isSignedIn();
+  }
+
   private static final void showError(String message) {
     new ErrorDialog(message).center();
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java
new file mode 100644
index 0000000..ebcafb8
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.api;
+
+import com.google.gerrit.client.actions.ActionButton;
+import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.EditInfo;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class EditGlue {
+  public static void onAction(
+      ChangeInfo change,
+      EditInfo edit,
+      ActionInfo action,
+      ActionButton button) {
+    RestApi api = ChangeApi.edit(
+          change.legacy_id().get())
+      .view(action.id());
+
+    JavaScriptObject f = get(action.id());
+    if (f != null) {
+      ActionContext c = ActionContext.create(api);
+      c.set(action);
+      c.set(change);
+      c.set(edit);
+      c.button(button);
+      ApiGlue.invoke(f, c);
+    } else {
+      DefaultActions.invoke(change, action, api);
+    }
+  }
+
+  private static final native JavaScriptObject get(String id) /*-{
+    return $wnd.Gerrit.edit_actions[id];
+  }-*/;
+
+  private EditGlue() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java
index 7ef022a..931a04d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java
@@ -50,9 +50,11 @@
     var G = $wnd.Gerrit;
     @com.google.gerrit.client.api.Plugin::TYPE.prototype = {
       getPluginName: function(){return this.name},
+      getCurrentUser: @com.google.gerrit.client.api.ApiGlue::getCurrentUser(),
       go: @com.google.gerrit.client.api.ApiGlue::go(Ljava/lang/String;),
       refresh: @com.google.gerrit.client.api.ApiGlue::refresh(),
       refreshMenuBar: @com.google.gerrit.client.api.ApiGlue::refreshMenuBar(),
+      isSignedIn: @com.google.gerrit.client.api.ApiGlue::isSignedIn(),
       showError: @com.google.gerrit.client.api.ApiGlue::showError(Ljava/lang/String;),
       on: function(e,f){G.on(e,f)},
       onAction: function(t,n,c){G._onAction(this.name,t,n,c)},
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java
index bd90cd3..67b8a98 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java
@@ -81,6 +81,7 @@
 
   /** Extracts URL from the stack frame. */
   static class PluginNameMoz extends PluginName {
+    @Override
     String findCallerUrl() {
       return getUrl(makeException());
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/arrowRight.gif b/gerrit-gwtui/src/main/java/com/google/gerrit/client/arrowRight.gif
deleted file mode 100644
index d9e63a5..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/arrowRight.gif
+++ /dev/null
Binary files differ
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 60fbee4..54b83f1 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
@@ -30,6 +30,7 @@
     this.id = id;
   }
 
+  @Override
   void send(String message) {
     ChangeApi.abandon(id.get(), message, new GerritCallback<ChangeInfo>() {
       @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java
index 45f122c..c560f6d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java
@@ -98,7 +98,7 @@
   }
 
   @UiHandler("send")
-  void onSend(ClickEvent e) {
+  void onSend(@SuppressWarnings("unused") ClickEvent e) {
     send(message.getValue().trim());
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
index 906efca..edf1105 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
@@ -38,15 +38,12 @@
   private static final String[] CORE = {
     "abandon", "restore", "revert", "topic",
     "cherrypick", "submit", "rebase", "message",
-    "publish", "/"};
+    "publish", "followup", "/"};
 
   interface Binder extends UiBinder<FlowPanel, Actions> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
   @UiField Button cherrypick;
-  @UiField Button deleteChange;
-  @UiField Button deleteRevision;
-  @UiField Button publish;
   @UiField Button rebase;
   @UiField Button revert;
   @UiField Button submit;
@@ -57,12 +54,17 @@
   @UiField Button restore;
   private RestoreAction restoreAction;
 
+  @UiField Button followUp;
+  private FollowUpAction followUpAction;
+
   private Change.Id changeId;
   private ChangeInfo changeInfo;
   private String revision;
   private String project;
   private String subject;
   private String message;
+  private String branch;
+  private String key;
   private boolean canSubmit;
 
   Actions() {
@@ -80,6 +82,8 @@
     project = info.project();
     subject = commit.subject();
     message = commit.message();
+    branch = info.branch();
+    key = info.change_id();
     changeInfo = info;
 
     initChangeActions(info, hasUser);
@@ -93,10 +97,10 @@
     actions.copyKeysIntoChildren("id");
 
     if (hasUser) {
-      a2b(actions, "/", deleteChange);
       a2b(actions, "abandon", abandon);
       a2b(actions, "restore", restore);
       a2b(actions, "revert", revert);
+      a2b(actions, "followup", followUp);
       for (String id : filterNonCore(actions)) {
         add(new ActionButton(info, actions.get(id)));
       }
@@ -116,15 +120,18 @@
       if (canSubmit) {
         ActionInfo action = actions.get("submit");
         submit.setTitle(action.title());
+        submit.setEnabled(action.enabled());
         submit.setHTML(new SafeHtmlBuilder()
             .openDiv()
             .append(action.label())
             .closeDiv());
       }
-      a2b(actions, "/", deleteRevision);
       a2b(actions, "cherrypick", cherrypick);
-      a2b(actions, "publish", publish);
       a2b(actions, "rebase", rebase);
+      if (rebase.isVisible()) {
+        // it is the rebase button in RebaseDialog that the server wants to disable
+        rebase.setEnabled(true);
+      }
       for (String id : filterNonCore(actions)) {
         add(new ActionButton(info, revInfo, actions.get(id)));
       }
@@ -147,35 +154,25 @@
     submit.setVisible(canSubmit);
   }
 
-  boolean isSubmitEnabled() {
-    return submit.isVisible() && submit.isEnabled();
+  @UiHandler("followUp")
+  void onFollowUp(@SuppressWarnings("unused") ClickEvent e) {
+    if (followUpAction == null) {
+      followUpAction = new FollowUpAction(followUp, project,
+          branch, key);
+    }
+    followUpAction.show();
   }
 
   @UiHandler("abandon")
-  void onAbandon(ClickEvent e) {
+  void onAbandon(@SuppressWarnings("unused") ClickEvent e) {
     if (abandonAction == null) {
       abandonAction = new AbandonAction(abandon, changeId);
     }
     abandonAction.show();
   }
 
-  @UiHandler("publish")
-  void onPublish(ClickEvent e) {
-    DraftActions.publish(changeId, revision);
-  }
-
-  @UiHandler("deleteRevision")
-  void onDeleteRevision(ClickEvent e) {
-    DraftActions.delete(changeId, revision);
-  }
-
-  @UiHandler("deleteChange")
-  void onDeleteChange(ClickEvent e) {
-    DraftActions.delete(changeId);
-  }
-
   @UiHandler("restore")
-  void onRestore(ClickEvent e) {
+  void onRestore(@SuppressWarnings("unused") ClickEvent e) {
     if (restoreAction == null) {
       restoreAction = new RestoreAction(restore, changeId);
     }
@@ -183,29 +180,40 @@
   }
 
   @UiHandler("rebase")
-  void onRebase(ClickEvent e) {
-    RebaseAction.call(changeId, revision);
+  void onRebase(@SuppressWarnings("unused") ClickEvent e) {
+    boolean enabled = true;
+    RevisionInfo revInfo = changeInfo.revision(revision);
+    if (revInfo.has_actions()) {
+        NativeMap<ActionInfo> actions = revInfo.actions();
+        if (actions.containsKey("rebase")) {
+          enabled = actions.get("rebase").enabled();
+        }
+    }
+    RebaseAction.call(rebase, project, changeInfo.branch(), changeId, revision,
+        enabled);
   }
 
   @UiHandler("submit")
-  void onSubmit(ClickEvent e) {
+  void onSubmit(@SuppressWarnings("unused") ClickEvent e) {
     SubmitAction.call(changeInfo, changeInfo.revision(revision));
   }
 
   @UiHandler("cherrypick")
-  void onCherryPick(ClickEvent e) {
+  void onCherryPick(@SuppressWarnings("unused") ClickEvent e) {
     CherryPickAction.call(cherrypick, changeInfo, revision, project, message);
   }
 
   @UiHandler("revert")
-  void onRevert(ClickEvent e) {
-    RevertAction.call(revert, changeId, revision, project, subject);
+  void onRevert(@SuppressWarnings("unused") ClickEvent e) {
+    RevertAction.call(revert, changeId, revision, subject);
   }
 
   private static void a2b(NativeMap<ActionInfo> actions, String a, Button b) {
     if (actions.containsKey(a)) {
       b.setVisible(true);
-      b.setTitle(actions.get(a).title());
+      ActionInfo actionInfo = actions.get(a);
+      b.setTitle(actionInfo.title());
+      b.setEnabled(actionInfo.enabled());
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
index cb2b37f..40d732a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
@@ -18,6 +18,8 @@
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:style>
+    @def BUTTON_HEIGHT 14px;
+
     #change_actions {
       padding-top: 2px;
       padding-bottom: 20px;
@@ -25,39 +27,23 @@
 
     #change_actions button {
       margin: 6px 3px 0 0;
-      border-color: rgba(0, 0, 0, 0.1);
       text-align: center;
       font-size: 8pt;
       font-weight: bold;
-      border: 1px solid;
+      border: 2px solid;
       cursor: pointer;
-      color: #444;
+      color: rgba(0, 0, 0, 0.15);
       background-color: #f5f5f5;
-      background-image: -webkit-linear-gradient(top, #f5f5f5, #f1f1f1);
       -webkit-border-radius: 2px;
       -webkit-box-sizing: content-box;
     }
     #change_actions button div {
-      color: #444; 
-      height: 10px;
-      min-width: 54px;
-      line-height: 10px;
-      white-space: nowrap;
-    }
-
-    #change_actions button {
       color: #444;
-      background-color: #f5f5f5;
-      background-image: -webkit-linear-gradient(top, #f5f5f5, #f1f1f1);
+      min-width: 54px;
+      white-space: nowrap;
+      height: BUTTON_HEIGHT;
+      line-height: BUTTON_HEIGHT;
     }
-    #change_actions button div {color: #444;}
-
-    #change_actions button.red {
-      color: #d14836;
-      background-color: #d14836;
-      background-image: -webkit-linear-gradient(top, #d14836, #d14836);
-    }
-    #change_actions button.red div {color: #fff;}
 
     #change_actions button.submit {
       float: right;
@@ -83,22 +69,15 @@
     <g:Button ui:field='revert' styleName='' visible='false'>
       <div><ui:msg>Revert</ui:msg></div>
     </g:Button>
-    <g:Button ui:field='deleteChange' styleName='' visible='false'>
-      <div><ui:msg>Delete Change</ui:msg></div>
-    </g:Button>
-    <g:Button ui:field='deleteRevision' styleName='' visible='false'>
-      <div><ui:msg>Delete Revision</ui:msg></div>
-    </g:Button>
-    <g:Button ui:field='publish' styleName='' visible='false'>
-      <div><ui:msg>Publish</ui:msg></div>
-    </g:Button>
-
-    <g:Button ui:field='abandon' styleName='{style.red}' visible='false'>
+    <g:Button ui:field='abandon' styleName='' visible='false'>
       <div><ui:msg>Abandon</ui:msg></div>
     </g:Button>
-    <g:Button ui:field='restore' styleName='{style.red}' visible='false'>
+    <g:Button ui:field='restore' styleName='' visible='false'>
       <div><ui:msg>Restore</ui:msg></div>
     </g:Button>
+    <g:Button ui:field='followUp' styleName='' visible='false'>
+      <div><ui:msg>Follow-Up</ui:msg></div>
+    </g:Button>
 
     <g:Button ui:field='submit' styleName='{style.submit}' visible='false'/>
   </g:FlowPanel>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileAction.java
new file mode 100644
index 0000000..f74ebf6
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileAction.java
@@ -0,0 +1,74 @@
+//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.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+class AddFileAction {
+  private final Change.Id changeId;
+  private final RevisionInfo revision;
+  private final ChangeScreen.Style style;
+  private final Widget addButton;
+  private final FileTable files;
+
+  private AddFileBox addBox;
+  private PopupPanel popup;
+
+  AddFileAction(Change.Id changeId, RevisionInfo revision,
+      ChangeScreen.Style style, Widget addButton, FileTable files) {
+    this.changeId = changeId;
+    this.revision = revision;
+    this.style = style;
+    this.addButton = addButton;
+    this.files = files;
+  }
+
+  public void onEdit() {
+    if (popup != null) {
+      popup.hide();
+      return;
+    }
+
+    files.unregisterKeys();
+    if (addBox == null) {
+      addBox = new AddFileBox(changeId, revision, files);
+    }
+    addBox.clearPath();
+
+    final PluginSafePopupPanel p = new PluginSafePopupPanel(true);
+    p.setStyleName(style.replyBox());
+    p.addAutoHidePartner(addButton.getElement());
+    p.addCloseHandler(new CloseHandler<PopupPanel>() {
+      @Override
+      public void onClose(CloseEvent<PopupPanel> event) {
+        if (popup == p) {
+          popup = null;
+        }
+      }
+    });
+    p.add(addBox);
+    p.showRelativeTo(addButton);
+    GlobalKey.dialog(p);
+    addBox.setFocus(true);
+    popup = p;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.java
new file mode 100644
index 0000000..7245e47
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.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.client.change;
+
+import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.ui.RemoteSuggestBox;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+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.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.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+class AddFileBox extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, AddFileBox> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  private final Change.Id changeId;
+  private final RevisionInfo revision;
+  private final FileTable fileTable;
+
+  @UiField Button open;
+  @UiField Button cancel;
+
+  @UiField(provided = true)
+  RemoteSuggestBox path;
+
+  AddFileBox(Change.Id changeId, RevisionInfo revision, FileTable files) {
+    this.changeId = changeId;
+    this.revision = revision;
+    this.fileTable = files;
+
+    path = new RemoteSuggestBox(new PathSuggestOracle(changeId, revision));
+    path.addSelectionHandler(new SelectionHandler<String>() {
+      @Override
+      public void onSelection(SelectionEvent<String> event) {
+        open(event.getSelectedItem());
+      }
+    });
+    path.addCloseHandler(new CloseHandler<RemoteSuggestBox>() {
+      @Override
+      public void onClose(CloseEvent<RemoteSuggestBox> event) {
+        hide();
+        fileTable.registerKeys();
+      }
+    });
+
+    initWidget(uiBinder.createAndBindUi(this));
+  }
+
+  void setFocus(boolean focus) {
+    path.setFocus(focus);
+  }
+
+  void clearPath() {
+    path.setText("");
+  }
+
+  @UiHandler("open")
+  void onOpen(@SuppressWarnings("unused") ClickEvent e) {
+    open(path.getText());
+  }
+
+  private void open(String path) {
+    hide();
+    Gerrit.display(Dispatcher.toEditScreen(
+        new PatchSet.Id(changeId, revision._number()),
+        path));
+  }
+
+  @UiHandler("cancel")
+  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
+    hide();
+    fileTable.registerKeys();
+  }
+
+  private void hide() {
+    for (Widget w = getParent(); w != null; w = w.getParent()) {
+      if (w instanceof PopupPanel) {
+        ((PopupPanel) w).hide();
+        break;
+      }
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.ui.xml
similarity index 71%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.ui.xml
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.ui.xml
index 9df4bbc..d8236e6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.ui.xml
@@ -16,30 +16,22 @@
 -->
 <ui:UiBinder
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'
-    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'>
+    xmlns:u='urn:import:com.google.gerrit.client.ui'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
   <ui:style>
-    .commitMessage {
-      background-color: white;
-      font-family: monospace;
-    }
     .cancel { float: right; }
   </ui:style>
   <g:HTMLPanel>
     <div class='{res.style.section}'>
-      <c:NpTextArea
-         visibleLines='30'
-         characterWidth='78'
-         styleName='{style.commitMessage}'
-         ui:field='message'/>
+      <ui:msg>Path: <u:RemoteSuggestBox ui:field='path' visibleLength='86'/></ui:msg>
     </div>
     <div class='{res.style.section}'>
-      <g:Button ui:field='save'
-          title='Create new patch set with updated commit message'
+      <g:Button ui:field='open'
+          title='Open file in editor'
           styleName='{res.style.button}'>
         <ui:attribute name='title'/>
-        <div><ui:msg>Save</ui:msg></div>
+        <div><ui:msg>Open</ui:msg></div>
       </g:Button>
       <g:Button ui:field='cancel'
           styleName='{res.style.button}'
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java
index 11a1824..be6879e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java
@@ -21,6 +21,10 @@
   String nextChange();
   String openChange();
   String reviewedFileTitle();
+  String editMessage();
+  String editFileInline();
+  String removeFileInline();
+  String restoreFileInline();
 
   String openLastFile();
   String openCommitMessage();
@@ -43,4 +47,10 @@
   String sameTopicTooltip();
   String noChanges();
   String indirectAncestor();
+  String merged();
+  String abandoned();
+
+  String deleteChangeEdit();
+  String deleteDraftChange();
+  String deleteDraftRevision();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties
index 289e9b4..682cd18 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.properties
@@ -2,6 +2,10 @@
 nextChange = Next related change
 openChange = Open related change
 reviewedFileTitle = Mark file as reviewed (Shortcut: r)
+editMessage = Edit commit message
+editFileInline = Edit file inline
+removeFileInline = Remove file inline
+restoreFileInline = Restore file inline
 
 openLastFile = Open last file
 openCommitMessage = Open commit message
@@ -24,3 +28,11 @@
 sameTopicTooltip = Changes with the same topic
 noChanges = No Changes
 indirectAncestor = Indirect ancestor
+merged = Merged
+abandoned = Abandoned
+
+deleteChangeEdit = Delete Change Edit?\n\
+  \n\
+  All changes made in the edit revision will be lost.
+deleteDraftChange = Delete Draft Change?
+deleteDraftRevision = Delete Draft Revision?
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java
index 595a6c9..8d5b72c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java
@@ -17,7 +17,7 @@
 import com.google.gwt.i18n.client.Messages;
 
 public interface ChangeMessages extends Messages {
-  String patchSets(int currentlyViewedPatchSet, int currentPatchSet);
+  String patchSets(String currentlyViewedPatchSet, int currentPatchSet);
   String changeWithNoRevisions(int changeId);
   String relatedChanges(int count);
   String relatedChanges(String count);
@@ -27,4 +27,5 @@
   String cherryPicks(String count);
   String sameTopic(int count);
   String sameTopic(String count);
+  String editPatchSet(int patchSet);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties
index 5fddd8e..6e095fb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties
@@ -4,3 +4,4 @@
 conflictingChanges = Conflicts With ({0})
 cherryPicks = Cherry-Picks ({0})
 sameTopic = Same Topic ({0})
+editPatchSet = edit:{0}
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/ChangeScreen.java
similarity index 68%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
index 69cd3fc..db7b485 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/ChangeScreen.java
@@ -24,6 +24,8 @@
 import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.changes.ChangeInfo.EditInfo;
+import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
 import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.changes.ChangeList;
@@ -50,8 +52,8 @@
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.client.ui.UserActivityMonitor;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.common.ListChangesOption;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -75,6 +77,7 @@
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.EventListener;
+import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Button;
@@ -86,15 +89,18 @@
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 import com.google.gwtorm.client.KeyUtil;
 
+import net.codemirror.lib.CodeMirror;
+
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.List;
 
-public class ChangeScreen2 extends Screen {
-  interface Binder extends UiBinder<HTMLPanel, ChangeScreen2> {}
+public class ChangeScreen extends Screen {
+  interface Binder extends UiBinder<HTMLPanel, ChangeScreen> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
   interface Style extends CssResource {
@@ -107,14 +113,15 @@
     String label_need();
     String replyBox();
     String selected();
+    String hashtagName();
   }
 
-  static ChangeScreen2 get(NativeEvent in) {
+  static ChangeScreen get(NativeEvent in) {
     Element e = in.getEventTarget().cast();
     for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
       EventListener l = DOM.getEventListener(e);
-      if (l instanceof ChangeScreen2) {
-        return (ChangeScreen2) l;
+      if (l instanceof ChangeScreen) {
+        return (ChangeScreen) l;
       }
     }
     return null;
@@ -125,6 +132,7 @@
   private String revision;
   private ChangeInfo changeInfo;
   private CommentLinkProcessor commentLinkProcessor;
+  private EditInfo edit;
 
   private KeyCommandSet keysNavigation;
   private KeyCommandSet keysAction;
@@ -134,6 +142,7 @@
   private UpdateAvailableBar updateAvailable;
   private boolean openReplyBox;
   private boolean loaded;
+  private FileTable.Mode fileTableMode;
 
   @UiField HTMLPanel headerLine;
   @UiField Style style;
@@ -142,6 +151,8 @@
 
   @UiField Element ccText;
   @UiField Reviewers reviewers;
+  @UiField Hashtags hashtags;
+  @UiField Element hashtagTableRow;
   @UiField FlowPanel ownerPanel;
   @UiField InlineHyperlink ownerLink;
   @UiField Element statusText;
@@ -169,23 +180,37 @@
   @UiField Element patchSetsText;
   @UiField Button download;
   @UiField Button reply;
+  @UiField Button publishEdit;
+  @UiField Button rebaseEdit;
+  @UiField Button deleteEdit;
+  @UiField Button publish;
+  @UiField Button deleteChange;
+  @UiField Button deleteRevision;
   @UiField Button openAll;
+  @UiField Button editMode;
+  @UiField Button reviewMode;
+  @UiField Button addFile;
+  @UiField Button deleteFile;
+  @UiField Button renameFile;
   @UiField Button expandAll;
   @UiField Button collapseAll;
-  @UiField Button editMessage;
   @UiField QuickApprove quickApprove;
 
   private ReplyAction replyAction;
-  private EditMessageAction editMessageAction;
   private IncludedInAction includedInAction;
   private PatchSetsAction patchSetsAction;
   private DownloadAction downloadAction;
+  private AddFileAction addFileAction;
+  private DeleteFileAction deleteFileAction;
+  private RenameFileAction renameFileAction;
 
-  public ChangeScreen2(Change.Id changeId, String base, String revision, boolean openReplyBox) {
+  public ChangeScreen(Change.Id changeId, String base, String revision,
+      boolean openReplyBox, FileTable.Mode mode) {
     this.changeId = changeId;
     this.base = normalize(base);
     this.revision = normalize(revision);
     this.openReplyBox = openReplyBox;
+    this.fileTableMode = mode;
     add(uiBinder.createAndBindUi(this));
   }
 
@@ -196,21 +221,35 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    loadChangeInfo(true, new GerritCallback<ChangeInfo>() {
-      @Override
-      public void onSuccess(ChangeInfo info) {
-        info.init();
-        loadConfigInfo(info, base);
-      }
-    });
+    CallbackGroup group = new CallbackGroup();
+    if (Gerrit.isSignedIn()) {
+      ChangeApi.editWithFiles(changeId.get(), group.add(
+          new AsyncCallback<EditInfo>() {
+            @Override
+            public void onSuccess(EditInfo result) {
+              edit = result;
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+            }
+          }));
+    }
+    loadChangeInfo(true, group.addFinal(
+        new GerritCallback<ChangeInfo>() {
+          @Override
+          public void onSuccess(ChangeInfo info) {
+            info.init();
+            loadConfigInfo(info, base);
+          }
+        }));
   }
 
   void loadChangeInfo(boolean fg, AsyncCallback<ChangeInfo> cb) {
     RestApi call = ChangeApi.detail(changeId.get());
     ChangeList.addOptions(call, EnumSet.of(
       ListChangesOption.CURRENT_ACTIONS,
-      ListChangesOption.ALL_REVISIONS,
-      ListChangesOption.WEB_LINKS));
+      ListChangesOption.ALL_REVISIONS));
     if (!fg) {
       call.background();
     }
@@ -239,8 +278,9 @@
     setHeaderVisible(false);
     Resources.I.style().ensureInjected();
     star.setVisible(Gerrit.isSignedIn());
-    labels.init(style, statusText);
+    labels.init(style);
     reviewers.init(style, ccText);
+    hashtags.init(style);
 
     keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
     keysNavigation.add(new KeyCommand(0, 'u', Util.C.upToChangeList()) {
@@ -306,6 +346,17 @@
     }
   }
 
+  private void initReplyButton(ChangeInfo info, String revision) {
+    if (!info.revision(revision).is_edit()) {
+      reply.setTitle(Gerrit.getConfig().getReplyTitle());
+      reply.setHTML(new SafeHtmlBuilder()
+        .openDiv()
+        .append(Gerrit.getConfig().getReplyLabel())
+        .closeDiv());
+      reply.setVisible(true);
+    }
+  }
+
   private void gotoSibling(final int offset) {
     if (offset > 0 && changeInfo.current_revision().equals(revision)) {
       return;
@@ -339,6 +390,19 @@
     }
   }
 
+  private void initChangeAction(ChangeInfo info) {
+    if (info.status() == Status.DRAFT) {
+      NativeMap<ActionInfo> actions = info.has_actions()
+          ? info.actions()
+          : NativeMap.<ActionInfo> create();
+      actions.copyKeysIntoChildren("id");
+      if (actions.containsKey("/")) {
+        deleteChange.setVisible(true);
+        deleteChange.setTitle(actions.get("/").title());
+      }
+    }
+  }
+
   private void initRevisionsAction(ChangeInfo info, String revision) {
     int currentPatchSet;
     if (info.current_revision() != null
@@ -350,12 +414,37 @@
       currentPatchSet = revList.get(revList.length() - 1)._number();
     }
 
-    int currentlyViewedPatchSet = info.revision(revision)._number();
+    String currentlyViewedPatchSet;
+    if (info.revision(revision).id().equals("edit")) {
+      currentlyViewedPatchSet =
+          Resources.M.editPatchSet(RevisionInfo.findEditParent(info.revisions()
+              .values()));
+      currentPatchSet = info.revisions().values().length() - 1;
+    } else {
+      currentlyViewedPatchSet = info.revision(revision).id();
+    }
     patchSetsText.setInnerText(Resources.M.patchSets(
         currentlyViewedPatchSet, currentPatchSet));
     patchSetsAction = new PatchSetsAction(
         info.legacy_id(), revision,
         style, headerLine, patchSets);
+
+    RevisionInfo revInfo = info.revision(revision);
+    if (revInfo.draft()) {
+      NativeMap<ActionInfo> actions = revInfo.has_actions()
+          ? revInfo.actions()
+          : NativeMap.<ActionInfo> create();
+      actions.copyKeysIntoChildren("id");
+
+      if (actions.containsKey("publish")) {
+        publish.setVisible(true);
+        publish.setTitle(actions.get("publish").title());
+      }
+      if (actions.containsKey("/")) {
+        deleteRevision.setVisible(true);
+        deleteRevision.setTitle(actions.get("/").title());
+      }
+    }
   }
 
   private void initDownloadAction(ChangeInfo info, String revision) {
@@ -392,23 +481,85 @@
                 null)));
   }
 
-  private void initEditMessageAction(ChangeInfo info, String revision) {
-    NativeMap<ActionInfo> actions = info.revision(revision).actions();
-    if (actions != null && actions.containsKey("message")) {
-      editMessage.setVisible(true);
-      editMessageAction = new EditMessageAction(
-          info.legacy_id(),
-          revision,
-          info.revision(revision).commit().message(),
-          style,
-          editMessage,
-          reply);
-      keysAction.add(new KeyCommand(0, 'e', Util.C.keyEditMessage()) {
-        @Override
-        public void onKeyPress(KeyPressEvent event) {
-          editMessageAction.onEdit();
+  private void initEditMode(ChangeInfo info, String revision) {
+    if (Gerrit.isSignedIn() && info.status().isOpen()) {
+      RevisionInfo rev = info.revision(revision);
+      if (isEditModeEnabled(info, rev)) {
+        editMode.setVisible(fileTableMode == FileTable.Mode.REVIEW);
+        addFile.setVisible(!editMode.isVisible());
+        deleteFile.setVisible(!editMode.isVisible());
+        renameFile.setVisible(!editMode.isVisible());
+        reviewMode.setVisible(!editMode.isVisible());
+        addFileAction = new AddFileAction(
+            changeId, info.revision(revision),
+            style, addFile, files);
+        deleteFileAction = new DeleteFileAction(
+            changeId, info.revision(revision),
+            style, addFile);
+        renameFileAction = new RenameFileAction(
+            changeId, info.revision(revision),
+            style, addFile);
+      } else {
+        editMode.setVisible(false);
+        addFile.setVisible(false);
+        reviewMode.setVisible(false);
+      }
+
+      if (rev.is_edit()) {
+        if (info.hasEditBasedOnCurrentPatchSet()) {
+          publishEdit.setVisible(true);
+        } else {
+          rebaseEdit.setVisible(true);
         }
-      });
+        deleteEdit.setVisible(true);
+      }
+    }
+  }
+
+  private boolean isEditModeEnabled(ChangeInfo info, RevisionInfo rev) {
+    if (rev.is_edit()) {
+      return true;
+    }
+    if (edit == null) {
+      return revision.equals(info.current_revision());
+    }
+    return rev._number() == RevisionInfo.findEditParent(
+        info.revisions().values());
+  }
+
+  @UiHandler("publishEdit")
+  void onPublishEdit(@SuppressWarnings("unused") ClickEvent e) {
+    EditActions.publishEdit(changeId);
+  }
+
+  @UiHandler("rebaseEdit")
+  void onRebaseEdit(@SuppressWarnings("unused") ClickEvent e) {
+    EditActions.rebaseEdit(changeId);
+  }
+
+  @UiHandler("deleteEdit")
+  void onDeleteEdit(@SuppressWarnings("unused") ClickEvent e) {
+    if (Window.confirm(Resources.C.deleteChangeEdit())) {
+      EditActions.deleteEdit(changeId);
+    }
+  }
+
+  @UiHandler("publish")
+  void onPublish(@SuppressWarnings("unused") ClickEvent e) {
+    DraftActions.publish(changeId, revision);
+  }
+
+  @UiHandler("deleteRevision")
+  void onDeleteRevision(@SuppressWarnings("unused") ClickEvent e) {
+    if (Window.confirm(Resources.C.deleteDraftRevision())) {
+      DraftActions.delete(changeId, revision);
+    }
+  }
+
+  @UiHandler("deleteChange")
+  void onDeleteChange(@SuppressWarnings("unused") ClickEvent e) {
+    if (Window.confirm(Resources.C.deleteDraftChange())) {
+      DraftActions.delete(changeId);
     }
   }
 
@@ -438,16 +589,19 @@
     }
 
     ChangeGlue.fireShowChange(changeInfo, changeInfo.revision(revision));
+    CodeMirror.preload();
     startPoller();
-    if (NewChangeScreenBar.show()) {
-      add(new NewChangeScreenBar(changeId));
-    }
   }
 
   private void scrollToPath(String token) {
     int s = token.indexOf('/');
     try {
-      if (s < 0 || !changeId.equals(Change.Id.parse(token.substring(0, s)))) {
+      String c = token.substring(0, s);
+      int editIndex = c.indexOf(",edit");
+      if (editIndex > 0) {
+        c = c.substring(0, editIndex);
+      }
+      if (s < 0 || !changeId.equals(Change.Id.parse(c))) {
         return; // Unrelated URL, do not scroll.
       }
     } catch (IllegalArgumentException e) {
@@ -477,22 +631,22 @@
   }
 
   @UiHandler("includedIn")
-  void onIncludedIn(ClickEvent e) {
+  void onIncludedIn(@SuppressWarnings("unused") ClickEvent e) {
     includedInAction.show();
   }
 
   @UiHandler("download")
-  void onDownload(ClickEvent e) {
+  void onDownload(@SuppressWarnings("unused") ClickEvent e) {
     downloadAction.show();
   }
 
   @UiHandler("patchSets")
-  void onPatchSets(ClickEvent e) {
+  void onPatchSets(@SuppressWarnings("unused") ClickEvent e) {
     patchSetsAction.show();
   }
 
   @UiHandler("reply")
-  void onReply(ClickEvent e) {
+  void onReply(@SuppressWarnings("unused") ClickEvent e) {
     onReply();
   }
 
@@ -510,18 +664,58 @@
     }
   }
 
-  @UiHandler("editMessage")
-  void onEditMessage(ClickEvent e) {
-    editMessageAction.onEdit();
-  }
-
   @UiHandler("openAll")
-  void onOpenAll(ClickEvent e) {
+  void onOpenAll(@SuppressWarnings("unused") ClickEvent e) {
     files.openAll();
   }
 
+  @UiHandler("editMode")
+  void onEditMode(@SuppressWarnings("unused") ClickEvent e) {
+    fileTableMode = FileTable.Mode.EDIT;
+    refreshFileTable();
+    editMode.setVisible(false);
+    addFile.setVisible(true);
+    deleteFile.setVisible(true);
+    renameFile.setVisible(true);
+    reviewMode.setVisible(true);
+  }
+
+  @UiHandler("reviewMode")
+  void onReviewMode(@SuppressWarnings("unused") ClickEvent e) {
+    fileTableMode = FileTable.Mode.REVIEW;
+    refreshFileTable();
+    editMode.setVisible(true);
+    addFile.setVisible(false);
+    deleteFile.setVisible(false);
+    renameFile.setVisible(false);
+    reviewMode.setVisible(false);
+  }
+
+  @UiHandler("addFile")
+  void onAddFile(@SuppressWarnings("unused") ClickEvent e) {
+    addFileAction.onEdit();
+  }
+
+  @UiHandler("deleteFile")
+  void onDeleteFile(@SuppressWarnings("unused") ClickEvent e) {
+    deleteFileAction.onDelete();
+  }
+
+  @UiHandler("renameFile")
+  void onRenameFile(@SuppressWarnings("unused") ClickEvent e) {
+    renameFileAction.onRename();
+  }
+
+  private void refreshFileTable() {
+    int idx = diffBase.getSelectedIndex();
+    if (0 <= idx) {
+      String n = diffBase.getValue(idx);
+      loadConfigInfo(changeInfo, !n.isEmpty() ? n : null);
+    }
+  }
+
   @UiHandler("expandAll")
-  void onExpandAll(ClickEvent e) {
+  void onExpandAll(@SuppressWarnings("unused") ClickEvent e) {
     int n = history.getWidgetCount();
     for (int i = 0; i < n; i++) {
       ((Message) history.getWidget(i)).setOpen(true);
@@ -531,7 +725,7 @@
   }
 
   @UiHandler("collapseAll")
-  void onCollapseAll(ClickEvent e) {
+  void onCollapseAll(@SuppressWarnings("unused") ClickEvent e) {
     int n = history.getWidgetCount();
     for (int i = 0; i < n; i++) {
       ((Message) history.getWidget(i)).setOpen(false);
@@ -541,7 +735,7 @@
   }
 
   @UiHandler("diffBase")
-  void onChangeRevision(ChangeEvent e) {
+  void onChangeRevision(@SuppressWarnings("unused") ChangeEvent e) {
     int idx = diffBase.getSelectedIndex();
     if (0 <= idx) {
       String n = diffBase.getValue(idx);
@@ -551,11 +745,50 @@
 
   private void loadConfigInfo(final ChangeInfo info, final String base) {
     info.revisions().copyKeysIntoChildren("name");
+    if (edit != null) {
+      edit.set_name(edit.commit().commit());
+      info.set_edit(edit);
+      if (edit.has_files()) {
+        edit.files().copyKeysIntoChildren("path");
+      }
+      info.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
+      JsArray<RevisionInfo> list = info.revisions().values();
+
+      // Edit is converted to a regular revision (with number = 0) and
+      // added to the list of revisions. Additionally under certain
+      // circumstances change edit is assigned to be the current revision
+      // and is selected to be shown on the change screen.
+      // We have two different strategies to assign edit to the current ps:
+      // 1. revision == null: no revision is selected, so use the edit only
+      //    if it is based on the latest patch set
+      // 2. edit was selected explicitly from ps drop down:
+      //    use the edit regardless of which patch set it is based on
+      if (revision == null) {
+        RevisionInfo.sortRevisionInfoByNumber(list);
+        RevisionInfo rev = list.get(list.length() - 1);
+        if (rev.is_edit()) {
+          info.set_current_revision(rev.name());
+        }
+      } else if (revision.equals("edit") || revision.equals("0")) {
+        for (int i = 0; i < list.length(); i++) {
+          RevisionInfo r = list.get(i);
+          if (r.is_edit()) {
+            info.set_current_revision(r.name());
+            break;
+          }
+        }
+      }
+    }
     final RevisionInfo rev = resolveRevisionToDisplay(info);
     final RevisionInfo b = resolveRevisionOrPatchSetId(info, base, null);
 
     CallbackGroup group = new CallbackGroup();
-    loadDiff(b, rev, myLastReply(info), group);
+    Timestamp lastReply = myLastReply(info);
+    if (rev.is_edit()) {
+      loadFileList(b, rev, lastReply, group, null, null);
+    } else {
+      loadDiff(b, rev, lastReply, group);
+    }
     loadCommit(rev, group);
 
     if (loaded) {
@@ -594,24 +827,9 @@
       final Timestamp myLastReply, CallbackGroup group) {
     final List<NativeMap<JsArray<CommentInfo>>> comments = loadComments(rev, group);
     final List<NativeMap<JsArray<CommentInfo>>> drafts = loadDrafts(rev, group);
-    DiffApi.list(changeId.get(),
-      base != null ? base.name() : null,
-      rev.name(),
-      group.add(new AsyncCallback<NativeMap<FileInfo>>() {
-        @Override
-        public void onSuccess(NativeMap<FileInfo> m) {
-          files.setRevisions(
-              base != null ? new PatchSet.Id(changeId, base._number()) : null,
-              new PatchSet.Id(changeId, rev._number()));
-          files.setValue(m, myLastReply, comments.get(0), drafts.get(0));
-        }
+    loadFileList(base, rev, myLastReply, group, comments, drafts);
 
-        @Override
-        public void onFailure(Throwable caught) {
-        }
-      }));
-
-    if (Gerrit.isSignedIn()) {
+    if (Gerrit.isSignedIn() && fileTableMode == FileTable.Mode.REVIEW) {
       ChangeApi.revision(changeId.get(), rev.name())
         .view("files")
         .addParameterTrue("reviewed")
@@ -628,6 +846,31 @@
     }
   }
 
+  private void loadFileList(final RevisionInfo base, final RevisionInfo rev,
+      final Timestamp myLastReply, CallbackGroup group,
+      final List<NativeMap<JsArray<CommentInfo>>> comments,
+      final List<NativeMap<JsArray<CommentInfo>>> drafts) {
+    DiffApi.list(changeId.get(),
+      base != null ? base.name() : null,
+      rev.name(),
+      group.add(new AsyncCallback<NativeMap<FileInfo>>() {
+        @Override
+        public void onSuccess(NativeMap<FileInfo> m) {
+          files.set(
+              base != null ? new PatchSet.Id(changeId, base._number()) : null,
+              new PatchSet.Id(changeId, rev._number()),
+              style, reply, fileTableMode, edit != null);
+          files.setValue(m, myLastReply,
+              comments != null ? comments.get(0) : null,
+              drafts != null ? drafts.get(0) : null);
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+        }
+      }));
+  }
+
   private List<NativeMap<JsArray<CommentInfo>>> loadComments(
       RevisionInfo rev, CallbackGroup group) {
     final int id = rev._number();
@@ -671,18 +914,21 @@
   }
 
   private void loadCommit(final RevisionInfo rev, CallbackGroup group) {
-    ChangeApi.revision(changeId.get(), rev.name())
-      .view("commit")
-      .get(group.add(new AsyncCallback<CommitInfo>() {
-        @Override
-        public void onSuccess(CommitInfo info) {
-          rev.set_commit(info);
-        }
+    if (rev.is_edit()) {
+      return;
+    }
 
-        @Override
-        public void onFailure(Throwable caught) {
-        }
-      }));
+    ChangeApi.commitWithLinks(changeId.get(), rev.name(),
+        group.add(new AsyncCallback<CommitInfo>() {
+          @Override
+          public void onSuccess(CommitInfo info) {
+            rev.set_commit(info);
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+          }
+        }));
   }
 
   private void loadSubmitType(final Change.Status status, final boolean canSubmit) {
@@ -769,28 +1015,60 @@
     return revOrId != null ? info.revision(revOrId) : null;
   }
 
+  private boolean isSubmittable(ChangeInfo info) {
+    boolean canSubmit = info.status().isOpen();
+    if (canSubmit && info.status() == Change.Status.NEW) {
+      for (String name : info.labels()) {
+        LabelInfo label = info.label(name);
+        switch (label.status()) {
+          case NEED:
+            statusText.setInnerText("Needs " + name);
+            canSubmit = false;
+            break;
+          case REJECT:
+          case IMPOSSIBLE:
+            if (label.blocking()) {
+              statusText.setInnerText("Not " + name);
+              canSubmit = false;
+            }
+            break;
+          default:
+            break;
+          }
+      }
+    }
+    return canSubmit;
+  }
+
   private void renderChangeInfo(ChangeInfo info) {
     changeInfo = info;
     lastDisplayedUpdate = info.updated();
+    RevisionInfo revisionInfo = info.revision(revision);
     boolean current = info.status().isOpen()
-        && revision.equals(info.current_revision());
+        && revision.equals(info.current_revision())
+        && !revisionInfo.is_edit();
 
-    if (!current && info.status() == Change.Status.NEW) {
+    if (revisionInfo.is_edit()) {
+      statusText.setInnerText(Util.C.changeEdit());
+    } else if (!current && info.status() == Change.Status.NEW) {
       statusText.setInnerText(Util.C.notCurrent());
       labels.setVisible(false);
     } else {
       statusText.setInnerText(Util.toLongString(info.status()));
     }
-    boolean canSubmit = labels.set(info, current);
+    labels.set(info);
 
     renderOwner(info);
     renderActionTextDate(info);
     renderDiffBaseListBox(info);
+    initReplyButton(info, revision);
     initIncludedInAction(info);
+    initChangeAction(info);
     initRevisionsAction(info, revision);
     initDownloadAction(info, revision);
     initProjectLinks(info);
     initBranchLink(info);
+    initEditMode(info, revision);
     actions.display(info, revision);
 
     star.setValue(info.starred());
@@ -800,15 +1078,25 @@
     commit.set(commentLinkProcessor, info, revision);
     related.set(info, revision);
     reviewers.set(info);
+    if (Gerrit.isNoteDbEnabled()) {
+      hashtags.set(info);
+    } else {
+      setVisible(hashtagTableRow, false);
+    }
 
     if (Gerrit.isSignedIn()) {
-      initEditMessageAction(info, revision);
       replyAction = new ReplyAction(info, revision,
           style, commentLinkProcessor, reply, quickApprove);
       if (topic.canEdit()) {
         keysAction.add(new KeyCommand(0, 't', Util.C.keyEditTopic()) {
           @Override
           public void onKeyPress(KeyPressEvent event) {
+            // In Firefox this event is mistakenly called when F5 is pressed so
+            // differentiate F5 from 't' by checking the charCode(F5=0, t=116).
+            if (event.getNativeEvent().getCharCode() == 0) {
+              Window.Location.reload();
+              return;
+            }
             topic.onEdit();
           }
         });
@@ -818,7 +1106,7 @@
 
     if (current) {
       quickApprove.set(info, revision, replyAction);
-      loadSubmitType(info.status(), canSubmit);
+      loadSubmitType(info.status(), isSubmittable(info));
     } else {
       quickApprove.setVisible(false);
       setVisible(strategy, false);
@@ -882,7 +1170,7 @@
     for (int i = list.length() - 1; i >= 0; i--) {
       RevisionInfo r = list.get(i);
       diffBase.addItem(
-        r._number() + ": " + r.name().substring(0, 6),
+        r.id() + ": " + r.name().substring(0, 6),
         r.name());
       if (r.name().equals(revision)) {
         SelectElement.as(diffBase.getElement()).getOptions()
@@ -924,6 +1212,7 @@
           Gerrit.display(PageLinks.toChange(changeId));
         }
 
+        @Override
         void onIgnore(Timestamp newTime) {
           lastDisplayedUpdate = newTime;
         }
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/ChangeScreen.ui.xml
similarity index 77%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
index 2b09ef9..6f8d3aa 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/ChangeScreen.ui.xml
@@ -21,12 +21,14 @@
     xmlns:x='urn:import:com.google.gerrit.client.ui'>
   <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style type='com.google.gerrit.client.change.ChangeScreen2.Style'>
+  <ui:style type='com.google.gerrit.client.change.ChangeScreen.Style'>
     @eval textColor com.google.gerrit.client.Gerrit.getTheme().textColor;
     @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
 
     @def COMMIT_WIDTH 560px;
-    @def HEADER_HEIGHT 29px;
+    @def HEADER_HEIGHT 30px;
+
+    @def BUTTON_HEIGHT 14px;
 
     .cs2 {
       margin-bottom: 1em;
@@ -56,13 +58,6 @@
       text-overflow: ellipsis;
       white-space: nowrap;
     }
-    .subjectButtons {
-      position: absolute;
-      top: 0;
-      right: 3px;
-      height: HEADER_HEIGHT;
-      line-height: HEADER_HEIGHT;
-    }
 
     .infoLine {
       position: absolute;
@@ -75,8 +70,6 @@
     .infoLineHeaderButtons {
       display: inline-block;
       height: HEADER_HEIGHT;
-      line-height: HEADER_HEIGHT;
-      vertical-align: top;
     }
     .statusRight {
       position: absolute;
@@ -119,8 +112,6 @@
       cursor: pointer;
       height: 25px;
       border: none;
-      border-left: 2px solid #fff;
-      border-right: 2px solid #fff;
       background-color: trimColor;
       margin: 0 0 0 -2px;
       padding-left: 2px;
@@ -236,39 +227,57 @@
     .label_need {color: #000;}
     .label_may {color: #777;}
 
+    .hashtagName {
+      display: inline-block;
+      margin-bottom: 2px;
+      padding: 1px 3px 0px 3px;
+      border-radius: 5px;
+      -webkit-border-radius: 5px;
+      background: #E2F5FF;
+      border: 1px solid #579FDA;
+      white-space: nowrap;
+    }
+
+    .hashtagName button {
+      cursor: pointer;
+      padding: 0;
+      margin: 0 0 0 5px;
+      border: 0;
+      background-color: transparent;
+      white-space: nowrap;
+    }
+
     .headerButtons button {
-      margin: 6px 3px 0 0;
-      border-color: rgba(0, 0, 0, 0.1);
+      margin: 5.286px 3px 0 0;
       text-align: center;
       font-size: 8pt;
       font-weight: bold;
-      border: 1px solid;
       cursor: pointer;
-      color: #444;
+      border: 2px solid;
+      color: rgba(0, 0, 0, 0.15);
       background-color: #f5f5f5;
-      background-image: -webkit-linear-gradient(top, #f5f5f5, #f1f1f1);
       -webkit-border-radius: 2px;
       -webkit-box-sizing: content-box;
     }
     .headerButtons button div {
       color: #444;
-      height: 10px;
       min-width: 54px;
-      line-height: 10px;
       white-space: nowrap;
+      height: BUTTON_HEIGHT;
+      line-height: BUTTON_HEIGHT;
     }
-    button.quickApprove {
+    button.highlight {
       background-color: #4d90fe;
-      background-image: -webkit-linear-gradient(top, #4d90fe, #4d90fe);
     }
-    button.quickApprove div { color: #fff; }
+    button.highlight div { color: #fff; }
 
     .sectionHeader {
       position: relative;
       background-color: trimColor;
       font-weight: bold;
       color: textColor;
-      height: 18px;
+      height: 20px;
+      line-height: 20px;
       margin: 0 -5px;
       padding: 5px 5px;
     }
@@ -295,8 +304,12 @@
     }
     .diffBase select {
       margin: 0;
-      border: 1px solid #bbb;
-      font-size: smaller;
+      border: 2px solid rgba(0, 0, 0, 0.15);
+      height: 20px;
+      font-size: 8pt;
+      font-weight: bold;
+      border-radius: 2px;
+      background-color: #f5f5f5
     }
 
     .replyBox {
@@ -321,30 +334,42 @@
             </g:Anchor> - <span ui:field='statusText' class='{style.statusText}'/></ui:msg>
           </span>
         </div>
-        <div class='{style.subjectButtons} {style.headerButtons}'>
-          <g:Button ui:field='editMessage'
-              styleName=''
-              visible='false'
-              title='Edit commit message (Shortcut: e)'>
-            <ui:attribute name='title'/>
-            <div><ui:msg>Edit Message</ui:msg></div>
-          </g:Button>
-        </div>
       </div>
 
       <div class='{style.infoLine}'>
         <div class='{style.headerButtons} {style.infoLineHeaderButtons}'>
           <g:Button ui:field='reply'
               styleName=''
-              title='Reply and score (Shortcut: a)'>
+              title=''
+              visible='false'>
             <ui:attribute name='title'/>
-            <div><ui:msg>Reply&#8230;</ui:msg></div>
           </g:Button>
           <c:QuickApprove ui:field='quickApprove'
-              styleName='{style.quickApprove}'
+              styleName='{style.highlight}'
               title='Apply score with one click'>
             <ui:attribute name='title'/>
           </c:QuickApprove>
+          <g:Button ui:field='publishEdit'
+              styleName='{style.highlight}' visible='false'>
+            <div><ui:msg>Publish Edit</ui:msg></div>
+          </g:Button>
+          <g:Button ui:field='rebaseEdit'
+              styleName='{style.highlight}' visible='false'>
+            <div><ui:msg>Rebase Edit</ui:msg></div>
+          </g:Button>
+          <g:Button ui:field='deleteEdit' styleName='' visible='false'>
+            <div><ui:msg>Delete Edit</ui:msg></div>
+          </g:Button>
+          <g:Button ui:field='publish'
+              styleName='{style.highlight}' visible='false'>
+            <div><ui:msg>Publish</ui:msg></div>
+          </g:Button>
+          <g:Button ui:field='deleteChange' styleName='' visible='false'>
+            <div><ui:msg>Delete Change</ui:msg></div>
+          </g:Button>
+          <g:Button ui:field='deleteRevision' styleName='' visible='false'>
+            <div><ui:msg>Delete Revision</ui:msg></div>
+          </g:Button>
         </div>
       </div>
 
@@ -428,7 +453,7 @@
                      class='{style.notMergeable}'
                      style='display: none'
                      aria-hidden='true'
-                     title='The change cannot be merged due to a path conflict. Rebase the change locally and upload the rebased commit for review.'>
+                     title='The change cannot be merged due to a path conflict. Rebase the change and upload the rebased commit for review.'>
                   <ui:attribute name='title'/>
                   <ui:msg>Cannot Merge</ui:msg>
                 </div>
@@ -438,6 +463,11 @@
               <th ui:field='actionText'/>
               <td ui:field='actionDate'/>
             </tr>
+            <tr ui:field='hashtagTableRow'>
+              <td colspan='2'>
+                <c:Hashtags ui:field='hashtags'/>
+              </td>
+            </tr>
             <tr><td colspan='2'><c:Actions ui:field='actions'/></td></tr>
           </table>
           <hr/>
@@ -450,8 +480,29 @@
       </tr>
     </table>
 
-    <div class='{style.sectionHeader}'>
+    <div class='{style.sectionHeader} {style.headerButtons}'>
       <ui:msg>Files</ui:msg>
+      <g:Button ui:field='addFile'
+         title='Add file to this change'
+         styleName=''
+         visible='false'>
+        <ui:attribute name='title'/>
+        <div><ui:msg>Add&#8230;</ui:msg></div>
+      </g:Button>
+      <g:Button ui:field='deleteFile'
+         title='Delete file from the repository'
+         styleName=''
+         visible='false'>
+        <ui:attribute name='title'/>
+        <div><ui:msg>Delete&#8230;</ui:msg></div>
+      </g:Button>
+      <g:Button ui:field='renameFile'
+         title='Rename file in the repository'
+         styleName=''
+         visible='false'>
+        <ui:attribute name='title'/>
+        <div><ui:msg>Rename&#8230;</ui:msg></div>
+      </g:Button>
       <div class='{style.headerButtons}'>
         <g:Button ui:field='openAll'
             styleName=''
@@ -462,6 +513,20 @@
         <div class='{style.diffBase}'>
           <ui:msg>Diff against: <g:ListBox ui:field='diffBase' styleName=''/></ui:msg>
         </div>
+        <g:Button ui:field='editMode'
+            styleName=''
+            visible='false'
+            title='Switch file table to edit mode'>
+          <ui:attribute name='title'/>
+          <div><ui:msg>Edit</ui:msg></div>
+        </g:Button>
+        <g:Button ui:field='reviewMode'
+            styleName=''
+            visible='false'
+            title='Done with edit mode'>
+          <ui:attribute name='title'/>
+          <div><ui:msg>Done Editing</ui:msg></div>
+        </g:Button>
       </div>
     </div>
     <c:FileTable ui:field='files'/>
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 9f64fa0..bcd8f6b 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
@@ -33,7 +33,7 @@
       String project, final String commitMessage) {
     // TODO Replace CherryPickDialog with a nicer looking display.
     b.setEnabled(false);
-    new CherryPickDialog(b, new Project.NameKey(project)) {
+    new CherryPickDialog(new Project.NameKey(project)) {
       {
         sendButton.setText(Util.C.buttonCherryPickChangeSend());
         if (info.status() == Change.Status.MERGED) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
index 4572cf8..93db1a7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
@@ -19,20 +19,21 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.GitwebLink;
 import com.google.gerrit.client.WebLinkInfo;
+import com.google.gerrit.client.actions.ActionInfo;
 import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
 import com.google.gerrit.client.changes.ChangeInfo.GitPerson;
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.common.PageLinks;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JsArray;
-import com.google.gwt.dom.client.AnchorElement;
 import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.TableCellElement;
+import com.google.gwt.dom.client.TableRowElement;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.resources.client.CssResource;
 import com.google.gwt.uibinder.client.UiBinder;
@@ -47,6 +48,7 @@
 import com.google.gwt.user.client.ui.HTMLPanel;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.ScrollPanel;
+import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
@@ -66,8 +68,8 @@
   @UiField FlowPanel committerPanel;
   @UiField Image mergeCommit;
   @UiField CopyableLabel commitName;
-  @UiField TableCellElement webLinkCell;
-  @UiField Element parents;
+  @UiField FlowPanel webLinkPanel;
+  @UiField TableRowElement firstParent;
   @UiField FlowPanel parentCommits;
   @UiField FlowPanel parentWebLinks;
   @UiField InlineHyperlink authorNameEmail;
@@ -78,6 +80,7 @@
   @UiField HTML text;
   @UiField ScrollPanel scroll;
   @UiField Button more;
+  @UiField Element parentNotCurrentText;
   private boolean expanded;
 
   CommitBox() {
@@ -90,7 +93,7 @@
   }
 
   @UiHandler("more")
-  void onMore(ClickEvent e) {
+  void onMore(@SuppressWarnings("unused") ClickEvent e) {
     if (expanded) {
       removeStyleName(style.expanded());
       addStyleName(style.collapsed());
@@ -122,48 +125,95 @@
     if (revInfo.commit().parents().length() > 1) {
       mergeCommit.setVisible(true);
     }
+
     setParents(change.project(), revInfo.commit().parents());
+
+    // display the orange ball if parent has moved on (not current)
+    boolean parentNotCurrent = false;
+    if (revInfo.has_actions()) {
+      NativeMap<ActionInfo> actions = revInfo.actions();
+      if (actions.containsKey("rebase")) {
+        parentNotCurrent = actions.get("rebase").enabled();
+      }
+    }
+    UIObject.setVisible(parentNotCurrentText, parentNotCurrent);
+    parentNotCurrentText.setInnerText(parentNotCurrent ? "\u25CF" : "");
   }
 
   private void setWebLinks(ChangeInfo change, String revision,
       RevisionInfo revInfo) {
     GitwebLink gw = Gerrit.getGitwebLink();
     if (gw != null && gw.canLink(revInfo)) {
-      addWebLink(gw.toRevision(change.project(), revision), gw.getLinkName());
+      toAnchor(gw.toRevision(change.project(), revision),
+          gw.getLinkName());
     }
 
-    JsArray<WebLinkInfo> links = revInfo.web_links();
+    JsArray<WebLinkInfo> links = revInfo.commit().web_links();
     if (links != null) {
       for (WebLinkInfo link : Natives.asList(links)) {
-        addWebLink(link.url(), parenthesize(link.name()));
+        webLinkPanel.add(link.toAnchor());
       }
     }
   }
 
-  private void addWebLink(String href, String name) {
-    AnchorElement a = DOM.createAnchor().cast();
+  private void toAnchor(String href, String name) {
+    Anchor a = new Anchor();
     a.setHref(href);
-    a.setInnerText(name);
-    webLinkCell.appendChild(a);
+    a.setText(name);
+    webLinkPanel.add(a);
   }
 
   private void setParents(String project, JsArray<CommitInfo> commits) {
-    setVisible(parents, true);
+    setVisible(firstParent, true);
+    TableRowElement next = firstParent;
+    TableRowElement previous = null;
     for (CommitInfo c : Natives.asList(commits)) {
-      CopyableLabel copyLabel = new CopyableLabel(c.commit());
-      copyLabel.setStyleName(style.clippy());
-      parentCommits.add(copyLabel);
+      if (next == firstParent) {
+        CopyableLabel copyLabel = getCommitLabel(c);
+        parentCommits.add(copyLabel);
+        addLinks(project, c, parentWebLinks);
+      } else {
+        next.appendChild(DOM.createTD());
+        Element td1 = DOM.createTD();
+        td1.appendChild(getCommitLabel(c).getElement());
+        next.appendChild(td1);
+        FlowPanel linksPanel = new FlowPanel();
+        linksPanel.addStyleName(style.parentWebLink());
+        addLinks(project, c, linksPanel);
+        Element td2 = DOM.createTD();
+        td2.appendChild(linksPanel.getElement());
+        next.appendChild(td2);
+        previous.getParentElement().insertAfter(next, previous);
+      }
+      previous = next;
+      next = DOM.createTR().cast();
+    }
+  }
 
-      GitwebLink gw = Gerrit.getGitwebLink();
-      if (gw != null) {
-        Anchor a =
-            new Anchor(gw.getLinkName(), gw.toRevision(project, c.commit()));
-        a.setStyleName(style.parentWebLink());
-        parentWebLinks.add(a);
+  private void addLinks(String project, CommitInfo c, FlowPanel panel) {
+    GitwebLink gw = Gerrit.getGitwebLink();
+    if (gw != null) {
+      Anchor a =
+          new Anchor(gw.getLinkName(), gw.toRevision(project, c.commit()));
+      a.setStyleName(style.parentWebLink());
+      panel.add(a);
+    }
+    JsArray<WebLinkInfo> links = c.web_links();
+    if (links != null) {
+      for (WebLinkInfo link : Natives.asList(links)) {
+        panel.add(link.toAnchor());
       }
     }
   }
 
+  private CopyableLabel getCommitLabel(CommitInfo c) {
+    CopyableLabel copyLabel;
+    copyLabel = new CopyableLabel(c.commit());
+    copyLabel.setTitle(c.subject());
+    copyLabel.setStyleName(style.clippy());
+    return copyLabel;
+  }
+
   private static void formatLink(GitPerson person, FlowPanel p,
       InlineHyperlink name, Element date, ChangeInfo change) {
     // only try to fetch the avatar image for author and committer if an avatar
@@ -186,14 +236,6 @@
     date.setInnerText(FormatUtil.mediumFormat(person.date()));
   }
 
-  private static String parenthesize(String str) {
-    return new StringBuilder()
-        .append("(")
-        .append(str)
-        .append(")")
-        .toString();
-  }
-
   private static String renderName(GitPerson person) {
     return person.name() + " <" + person.email() + ">";
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
index 7d0bb00..93312fa 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
@@ -20,7 +20,7 @@
     xmlns:x='urn:import:com.google.gerrit.client.ui'
     xmlns:clippy='urn:import:com.google.gwtexpui.clippy.client'>
   <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
-  <ui:image field="toggle" src="more_less.png"/>
+  <ui:image field="toggle" src="moreLess.png"/>
   <ui:style type='com.google.gerrit.client.change.CommitBox.Style'>
     @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
 
@@ -68,7 +68,7 @@
       padding: 0;
       width: 560px;
     }
-    .header th { width: 70px; }
+    .header th { width: 72px; }
     .header td { white-space: nowrap; }
     .date { width: 132px; }
 
@@ -81,12 +81,18 @@
       right: -16px;
     }
     <!-- To make room for the copyableLabel from the adjacent column -->
-    .webLinkCell a:first-child {
+    .webLinkPanel a:first-child {
       margin-left:16px;
     }
-    .parentWebLink {
+    .webLinkPanel>a {
+      margin-left:2px;
+    }
+
+    .parentWebLink a:first-child {
       margin-left:16px;
-      display: block;
+    }
+    .parentWebLink>a {
+      margin-left:2px;
     }
 
     .commit {
@@ -100,6 +106,16 @@
       height: 16px !important;
       vertical-align: bottom;
     }
+
+    .parent {
+      margin-right: 3px;
+      float: left;
+    }
+    .parentNotCurrent {
+      color: #FFA62F;   <!-- orange -->
+      font-weight: bold;
+    }
+
   </ui:style>
   <g:HTMLPanel>
     <g:ScrollPanel styleName='{style.scroll}' ui:field='scroll'>
@@ -152,15 +168,25 @@
           </g:Image>
         </th>
         <td><clippy:CopyableLabel styleName='{style.clippy}' ui:field='commitName'/></td>
-        <td ui:field='webLinkCell' class='{style.webLinkCell}'></td>
+        <td>
+            <g:FlowPanel ui:field='webLinkPanel' styleName='{style.webLinkPanel}'/>
+        </td>
       </tr>
-      <tr ui:field='parents' style='display: none'>
-        <th><ui:msg>Parent(s)</ui:msg></th>
+      <tr ui:field='firstParent' style='display: none'>
+        <th>
+          <div class='{style.parent}'>
+            <ui:msg>Parent(s)</ui:msg>
+          </div>
+          <div ui:field='parentNotCurrentText'
+              title='Not current - rebase possible'
+              class='{style.parentNotCurrent}'
+              style='display: none' aria-hidden='true'/>
+        </th>
         <td>
           <g:FlowPanel ui:field='parentCommits'/>
         </td>
         <td>
-          <g:FlowPanel ui:field='parentWebLinks'/>
+          <g:FlowPanel ui:field='parentWebLinks' styleName='{style.parentWebLink}'/>
         </td>
       </tr>
       <tr>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileAction.java
new file mode 100644
index 0000000..ee94564
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileAction.java
@@ -0,0 +1,71 @@
+//Copyright (C) 2015 The Android Open Source Project
+//
+//Licensed under the Apache License, Version 2.0 (the "License");
+//you may not use this file except in compliance with the License.
+//You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing, software
+//distributed under the License is distributed on an "AS IS" BASIS,
+//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//See the License for the specific language governing permissions and
+//limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+class DeleteFileAction {
+  private final Change.Id changeId;
+  private final RevisionInfo revision;
+  private final ChangeScreen.Style style;
+  private final Widget deleteButton;
+
+  private DeleteFileBox deleteBox;
+  private PopupPanel popup;
+
+  DeleteFileAction(Change.Id changeId, RevisionInfo revision,
+      ChangeScreen.Style style, Widget deleteButton) {
+    this.changeId = changeId;
+    this.revision = revision;
+    this.style = style;
+    this.deleteButton = deleteButton;
+  }
+
+  void onDelete() {
+    if (popup != null) {
+      popup.hide();
+      return;
+    }
+
+    if (deleteBox == null) {
+      deleteBox = new DeleteFileBox(changeId, revision);
+    }
+    deleteBox.clearPath();
+
+    final PluginSafePopupPanel p = new PluginSafePopupPanel(true);
+    p.setStyleName(style.replyBox());
+    p.addAutoHidePartner(deleteButton.getElement());
+    p.addCloseHandler(new CloseHandler<PopupPanel>() {
+      @Override
+      public void onClose(CloseEvent<PopupPanel> event) {
+        if (popup == p) {
+          popup = null;
+        }
+      }
+    });
+    p.add(deleteBox);
+    p.showRelativeTo(deleteButton);
+    GlobalKey.dialog(p);
+    deleteBox.setFocus(true);
+    popup = p;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.java
new file mode 100644
index 0000000..e55b7ed
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.java
@@ -0,0 +1,113 @@
+//Copyright (C) 2015 The Android Open Source Project
+//
+//Licensed under the Apache License, Version 2.0 (the "License");
+//you may not use this file except in compliance with the License.
+//You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing, software
+//distributed under the License is distributed on an "AS IS" BASIS,
+//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//See the License for the specific language governing permissions and
+//limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.changes.ChangeEditApi;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.ui.RemoteSuggestBox;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+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.AsyncCallback;
+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.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+class DeleteFileBox extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, DeleteFileBox> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  private final Change.Id changeId;
+
+  @UiField Button delete;
+  @UiField Button cancel;
+
+  @UiField(provided = true)
+  RemoteSuggestBox path;
+
+  DeleteFileBox(Change.Id changeId, RevisionInfo revision) {
+    this.changeId = changeId;
+
+    path = new RemoteSuggestBox(new PathSuggestOracle(changeId, revision));
+    path.addSelectionHandler(new SelectionHandler<String>() {
+      @Override
+      public void onSelection(SelectionEvent<String> event) {
+        delete(event.getSelectedItem());
+      }
+    });
+    path.addCloseHandler(new CloseHandler<RemoteSuggestBox>() {
+      @Override
+      public void onClose(CloseEvent<RemoteSuggestBox> event) {
+        hide();
+      }
+    });
+
+    initWidget(uiBinder.createAndBindUi(this));
+  }
+
+  void setFocus(boolean focus) {
+    path.setFocus(focus);
+  }
+
+  void clearPath() {
+    path.setText("");
+  }
+
+  @UiHandler("delete")
+  void onDelete(@SuppressWarnings("unused") ClickEvent e) {
+    delete(path.getText());
+  }
+
+  private void delete(String path) {
+    hide();
+    ChangeEditApi.delete(changeId.get(), path,
+        new AsyncCallback<VoidResult>() {
+          @Override
+          public void onSuccess(VoidResult result) {
+            Gerrit.display(PageLinks.toChangeInEditMode(changeId));
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+          }
+        });
+  }
+
+  @UiHandler("cancel")
+  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
+    hide();
+  }
+
+  private void hide() {
+    for (Widget w = getParent(); w != null; w = w.getParent()) {
+      if (w instanceof PopupPanel) {
+        ((PopupPanel) w).hide();
+        break;
+      }
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.ui.xml
similarity index 71%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.ui.xml
copy to gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.ui.xml
index 9df4bbc..4e7b2ba 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.ui.xml
@@ -16,30 +16,22 @@
 -->
 <ui:UiBinder
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'
-    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'>
+    xmlns:u='urn:import:com.google.gerrit.client.ui'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
   <ui:style>
-    .commitMessage {
-      background-color: white;
-      font-family: monospace;
-    }
     .cancel { float: right; }
   </ui:style>
   <g:HTMLPanel>
     <div class='{res.style.section}'>
-      <c:NpTextArea
-         visibleLines='30'
-         characterWidth='78'
-         styleName='{style.commitMessage}'
-         ui:field='message'/>
+      <ui:msg>Path: <u:RemoteSuggestBox ui:field='path' visibleLength='86'/></ui:msg>
     </div>
     <div class='{res.style.section}'>
-      <g:Button ui:field='save'
-          title='Create new patch set with updated commit message'
+      <g:Button ui:field='delete'
+          title='Delete file from the repository'
           styleName='{res.style.button}'>
         <ui:attribute name='title'/>
-        <div><ui:msg>Save</ui:msg></div>
+        <div><ui:msg>Delete</ui:msg></div>
       </g:Button>
       <g:Button ui:field='cancel'
           styleName='{res.style.button}'
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java
index 891cc10..b1bb4e0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java
@@ -25,7 +25,7 @@
   DownloadAction(
       ChangeInfo info,
       String revision,
-      ChangeScreen2.Style style,
+      ChangeScreen.Style style,
       UIObject relativeTo,
       Widget downloadButton) {
     super(style, relativeTo, downloadButton);
@@ -34,6 +34,7 @@
             info.revision(revision)._number()));
   }
 
+  @Override
   Widget getWidget() {
     return downloadBox;
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
index 09335c1..49389f3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
@@ -18,12 +18,13 @@
 import com.google.gerrit.client.account.AccountApi;
 import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.EditInfo;
 import com.google.gerrit.client.changes.ChangeInfo.FetchInfo;
 import com.google.gerrit.client.changes.ChangeList;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -78,23 +79,38 @@
   @Override
   protected void onLoad() {
     if (fetch == null) {
-      RestApi call = ChangeApi.detail(change.legacy_id().get());
-      ChangeList.addOptions(call, EnumSet.of(
-          revision.equals(change.current_revision())
-             ? ListChangesOption.CURRENT_REVISION
-             : ListChangesOption.ALL_REVISIONS,
-          ListChangesOption.DOWNLOAD_COMMANDS));
-      call.get(new AsyncCallback<ChangeInfo>() {
-        @Override
-        public void onSuccess(ChangeInfo result) {
-          fetch = result.revision(revision).fetch();
-          renderScheme();
-        }
+      if (psId.get() == 0) {
+        ChangeApi.editWithCommands(change.legacy_id().get()).get(
+            new AsyncCallback<EditInfo>() {
+          @Override
+          public void onSuccess(EditInfo result) {
+            fetch = result.fetch();
+            renderScheme();
+          }
 
-        @Override
-        public void onFailure(Throwable caught) {
-        }
-      });
+          @Override
+          public void onFailure(Throwable caught) {
+          }
+        });
+      } else {
+        RestApi call = ChangeApi.detail(change.legacy_id().get());
+        ChangeList.addOptions(call, EnumSet.of(
+            revision.equals(change.current_revision())
+               ? ListChangesOption.CURRENT_REVISION
+               : ListChangesOption.ALL_REVISIONS,
+            ListChangesOption.DOWNLOAD_COMMANDS));
+        call.get(new AsyncCallback<ChangeInfo>() {
+          @Override
+          public void onSuccess(ChangeInfo result) {
+            fetch = result.revision(revision).fetch();
+            renderScheme();
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+          }
+        });
+      }
     }
   }
 
@@ -254,7 +270,7 @@
       PreferenceInput in = PreferenceInput.create();
       in.download_scheme(scheme);
       AccountApi.self().view("preferences")
-          .post(in, new AsyncCallback<JavaScriptObject>() {
+          .put(in, new AsyncCallback<JavaScriptObject>() {
             @Override
             public void onSuccess(JavaScriptObject result) {
             }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DraftActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DraftActions.java
index d0666c6..634190a2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DraftActions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DraftActions.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.changes.SubmitFailureDialog;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
@@ -40,10 +39,12 @@
   public static GerritCallback<JavaScriptObject> cs(
       final Change.Id id) {
     return new GerritCallback<JavaScriptObject>() {
+      @Override
       public void onSuccess(JavaScriptObject result) {
         Gerrit.display(PageLinks.toChange(id));
       }
 
+      @Override
       public void onFailure(Throwable err) {
         if (SubmitFailureDialog.isConflict(err)) {
           new SubmitFailureDialog(err.getMessage()).center();
@@ -57,10 +58,12 @@
 
   private static AsyncCallback<JavaScriptObject> mine() {
     return new GerritCallback<JavaScriptObject>() {
+      @Override
       public void onSuccess(JavaScriptObject result) {
         Gerrit.display(PageLinks.MINE);
       }
 
+      @Override
       public void onFailure(Throwable err) {
         if (SubmitFailureDialog.isConflict(err)) {
           new SubmitFailureDialog(err.getMessage()).center();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java
new file mode 100644
index 0000000..d11cf7e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class EditActions {
+
+  static void deleteEdit(Change.Id id) {
+    ChangeApi.deleteEdit(id.get(), cs(id));
+  }
+
+  static void publishEdit(Change.Id id) {
+    ChangeApi.publishEdit(id.get(), cs(id));
+  }
+
+  static void rebaseEdit(Change.Id id) {
+    ChangeApi.rebaseEdit(id.get(), cs(id));
+  }
+
+  public static GerritCallback<JavaScriptObject> cs(
+      final Change.Id id) {
+    return new GerritCallback<JavaScriptObject>() {
+      @Override
+      public void onSuccess(JavaScriptObject result) {
+        Gerrit.display(PageLinks.toChange(id));
+      }
+
+      @Override
+      public void onFailure(Throwable err) {
+        if (SubmitFailureDialog.isConflict(err)) {
+          new SubmitFailureDialog(err.getMessage()).center();
+          Gerrit.display(PageLinks.toChange(id));
+        } else {
+          super.onFailure(err);
+        }
+      }
+    };
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageAction.java
deleted file mode 100644
index a0f7c9d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageAction.java
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.change;
-
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.user.client.PluginSafePopupPanel;
-
-class EditMessageAction {
-  private final Change.Id changeId;
-  private final String revision;
-  private final String originalMessage;
-  private final ChangeScreen2.Style style;
-  private final Widget editMessageButton;
-  private final Widget replyButton;
-
-  private EditMessageBox editBox;
-  private PopupPanel popup;
-
-  EditMessageAction(
-      Change.Id changeId,
-      String revision,
-      String originalMessage,
-      ChangeScreen2.Style style,
-      Widget editButton,
-      Widget replyButton) {
-    this.changeId = changeId;
-    this.revision = revision;
-    this.originalMessage = originalMessage;
-    this.style = style;
-    this.editMessageButton = editButton;
-    this.replyButton = replyButton;
-  }
-
-  void onEdit() {
-    if (popup != null) {
-      popup.hide();
-      return;
-    }
-
-    if (editBox == null) {
-      editBox = new EditMessageBox(
-          changeId,
-          revision,
-          originalMessage);
-    }
-
-    final PluginSafePopupPanel p = new PluginSafePopupPanel(true);
-    p.setStyleName(style.replyBox());
-    p.addAutoHidePartner(editMessageButton.getElement());
-    p.addCloseHandler(new CloseHandler<PopupPanel>() {
-      @Override
-      public void onClose(CloseEvent<PopupPanel> event) {
-        if (popup == p) {
-          popup = null;
-        }
-      }
-    });
-    p.add(editBox);
-    p.showRelativeTo(replyButton);
-    GlobalKey.dialog(p);
-    popup = p;
-  }
-}
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
deleted file mode 100644
index 0fe91ca..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java
+++ /dev/null
@@ -1,108 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.change;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.TextBoxChangeListener;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.event.dom.client.ClickEvent;
-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.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.PopupPanel;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.NpTextArea;
-
-class EditMessageBox extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, EditMessageBox> {}
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  private final Change.Id changeId;
-  private final String revision;
-  private String originalMessage;
-
-  @UiField NpTextArea message;
-  @UiField Button save;
-  @UiField Button cancel;
-
-  EditMessageBox(
-      Change.Id changeId,
-      String revision,
-      String msg) {
-    this.changeId = changeId;
-    this.revision = revision;
-    this.originalMessage = msg.trim();
-    initWidget(uiBinder.createAndBindUi(this));
-    message.getElement().setAttribute("wrap", "off");
-    message.setText("");
-    new TextBoxChangeListener(message) {
-      public void onTextChanged(String newText) {
-        save.setEnabled(!newText.trim()
-            .equals(originalMessage));
-      }
-    };
-  }
-
-  @Override
-  protected void onLoad() {
-    if (message.getText().isEmpty()) {
-      message.setText(originalMessage);
-      save.setEnabled(false);
-    }
-    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
-      @Override
-      public void execute() {
-        message.setFocus(true);
-      }});
-  }
-
-  @UiHandler("save")
-  void onSave(ClickEvent e) {
-    save.setEnabled(false);
-    ChangeApi.message(changeId.get(), revision, message.getText().trim(),
-        new GerritCallback<JavaScriptObject>() {
-          @Override
-          public void onSuccess(JavaScriptObject msg) {
-            Gerrit.display(PageLinks.toChange(changeId));
-            hide();
-          }
-        });
-  }
-
-  @UiHandler("cancel")
-  void onCancel(ClickEvent e) {
-    message.setText("");
-    hide();
-  }
-
-  private void hide() {
-    for (Widget w = getParent(); w != null; w = w.getParent()) {
-      if (w instanceof PopupPanel) {
-        ((PopupPanel) w).hide();
-        break;
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
index e097006..2947be8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
@@ -16,7 +16,9 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeEditApi;
 import com.google.gerrit.client.changes.CommentInfo;
 import com.google.gerrit.client.changes.ReviewInfo;
 import com.google.gerrit.client.changes.Util;
@@ -27,6 +29,8 @@
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.ui.NavigationTable;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -46,8 +50,11 @@
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.EventListener;
 import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.ImageResourceRenderer;
+import com.google.gwt.user.client.ui.Widget;
 import com.google.gwt.user.client.ui.impl.HyperlinkImpl;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.progress.client.ProgressBar;
@@ -55,7 +62,7 @@
 
 import java.sql.Timestamp;
 
-class FileTable extends FlowPanel {
+public class FileTable extends FlowPanel {
   static final FileTableResources R = GWT
       .create(FileTableResources.class);
 
@@ -80,20 +87,36 @@
     String deltaColumn2();
     String inserted();
     String deleted();
+    String removeButton();
   }
 
+  public static enum Mode {
+    REVIEW,
+    EDIT
+  }
+
+  private static final String DELETE;
+  private static final String RESTORE;
   private static final String REVIEWED;
   private static final String OPEN;
   private static final int C_PATH = 3;
   private static final HyperlinkImpl link = GWT.create(HyperlinkImpl.class);
 
   static {
+    DELETE = DOM.createUniqueId().replace('-', '_');
+    RESTORE = DOM.createUniqueId().replace('-', '_');
     REVIEWED = DOM.createUniqueId().replace('-', '_');
     OPEN = DOM.createUniqueId().replace('-', '_');
-    init(REVIEWED, OPEN);
+    init(DELETE, RESTORE, REVIEWED, OPEN);
   }
 
-  private static final native void init(String r, String o) /*-{
+  private static final native void init(String d, String t, String r, String o) /*-{
+    $wnd[d] = $entry(function(e,i) {
+      @com.google.gerrit.client.change.FileTable::onDelete(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i)
+    });
+    $wnd[t] = $entry(function(e,i) {
+      @com.google.gerrit.client.change.FileTable::onRestore(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i)
+    });
     $wnd[r] = $entry(function(e,i) {
       @com.google.gerrit.client.change.FileTable::onReviewed(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i)
     });
@@ -102,6 +125,24 @@
     });
   }-*/;
 
+  private static void onDelete(NativeEvent e, int idx) {
+    MyTable t = getMyTable(e);
+    if (t != null) {
+      t.onDelete(idx);
+    }
+  }
+
+  private static boolean onRestore(NativeEvent e, int idx) {
+    MyTable t = getMyTable(e);
+    if (t != null) {
+      t.onRestore(idx);
+      e.preventDefault();
+      e.stopPropagation();
+      return false;
+    }
+    return true;
+  }
+
   private static void onReviewed(NativeEvent e, int idx) {
     MyTable t = getMyTable(e);
     if (t != null) {
@@ -139,6 +180,10 @@
   private boolean register;
   private JsArrayString reviewed;
   private String scrollToPath;
+  private ChangeScreen.Style style;
+  private Widget replyButton;
+  private boolean editExists;
+  private Mode mode;
 
   @Override
   protected void onLoad() {
@@ -146,15 +191,20 @@
     R.css().ensureInjected();
   }
 
-  void setRevisions(PatchSet.Id base, PatchSet.Id curr) {
+  public void set(PatchSet.Id base, PatchSet.Id curr, ChangeScreen.Style style,
+      Widget replyButton, Mode mode, boolean editExists) {
     this.base = base;
     this.curr = curr;
+    this.style = style;
+    this.replyButton = replyButton;
+    this.mode = mode;
+    this.editExists = editExists;
   }
 
   void setValue(NativeMap<FileInfo> fileMap,
       Timestamp myLastReply,
-      NativeMap<JsArray<CommentInfo>> comments,
-      NativeMap<JsArray<CommentInfo>> drafts) {
+      @Nullable NativeMap<JsArray<CommentInfo>> comments,
+      @Nullable NativeMap<JsArray<CommentInfo>> drafts) {
     JsArray<FileInfo> list = fileMap.values();
     FileInfo.sortFileInfoByPath(list);
 
@@ -174,6 +224,14 @@
     }
   }
 
+  void unregisterKeys() {
+    register = false;
+
+    if (table != null) {
+      table.setRegisterKeys(false);
+    }
+  }
+
   void registerKeys() {
     register = true;
 
@@ -220,7 +278,9 @@
   private String url(FileInfo info) {
     return info.binary()
       ? Dispatcher.toUnified(base, curr, info.path())
-      : Dispatcher.toSideBySide(base, curr, info.path());
+      : mode == Mode.REVIEW
+            ? Dispatcher.toSideBySide(base, curr, info.path())
+            : Dispatcher.toEditScreen(curr, info.path());
   }
 
   private final class MyTable extends NavigationTable<FileInfo> {
@@ -262,6 +322,38 @@
           + curr.toString());
     }
 
+    void onDelete(int idx) {
+      String path = list.get(idx).path();
+      ChangeEditApi.delete(curr.getParentKey().get(), path,
+          new AsyncCallback<VoidResult>() {
+            @Override
+            public void onSuccess(VoidResult result) {
+              Gerrit.display(PageLinks.toChangeInEditMode(
+                  curr.getParentKey()));
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+            }
+          });
+    }
+
+    void onRestore(int idx) {
+      String path = list.get(idx).path();
+      ChangeEditApi.restore(curr.getParentKey().get(), path,
+          new AsyncCallback<VoidResult>() {
+            @Override
+            public void onSuccess(VoidResult result) {
+              Gerrit.display(PageLinks.toChangeInEditMode(
+                  curr.getParentKey()));
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+            }
+          });
+    }
+
     void onReviewed(InputElement checkbox, int idx) {
       setReviewed(list.get(idx), checkbox.isChecked());
     }
@@ -352,12 +444,13 @@
 
   private final class DisplayCommand implements RepeatingCommand {
     private final SafeHtmlBuilder sb = new SafeHtmlBuilder();
-    private final MyTable table;
+    private final MyTable myTable;
     private final JsArray<FileInfo> list;
     private final Timestamp myLastReply;
     private final NativeMap<JsArray<CommentInfo>> comments;
     private final NativeMap<JsArray<CommentInfo>> drafts;
     private final boolean hasUser;
+    private final boolean showChangeSizeBars;
     private boolean attached;
     private int row;
     private double start;
@@ -370,17 +463,20 @@
     private DisplayCommand(NativeMap<FileInfo> map,
         JsArray<FileInfo> list,
         Timestamp myLastReply,
-        NativeMap<JsArray<CommentInfo>> comments,
-        NativeMap<JsArray<CommentInfo>> drafts) {
-      this.table = new MyTable(map, list);
+        @Nullable NativeMap<JsArray<CommentInfo>> comments,
+        @Nullable NativeMap<JsArray<CommentInfo>> drafts) {
+      this.myTable = new MyTable(map, list);
       this.list = list;
       this.myLastReply = myLastReply;
       this.comments = comments;
       this.drafts = drafts;
       this.hasUser = Gerrit.isSignedIn();
-      table.addStyleName(R.css().table());
+      this.showChangeSizeBars = !hasUser
+          || Gerrit.getUserAccount().getGeneralPreferences().isSizeBarInChangeTable();
+      myTable.addStyleName(R.css().table());
     }
 
+    @Override
     public boolean execute() {
       boolean attachedNow = isAttached();
       if (!attached && attachedNow) {
@@ -408,9 +504,9 @@
         }
       }
       footer(sb);
-      table.resetHtml(sb);
-      table.finishDisplay();
-      setTable(table);
+      myTable.resetHtml(sb);
+      myTable.finishDisplay();
+      setTable(myTable);
       return false;
     }
 
@@ -449,7 +545,11 @@
     private void header(SafeHtmlBuilder sb) {
       sb.openTr().setStyleName(R.css().nohover());
       sb.openTh().setStyleName(R.css().pointer()).closeTh();
-      sb.openTh().setStyleName(R.css().reviewed()).closeTh();
+      if (mode == Mode.REVIEW) {
+        sb.openTh().setStyleName(R.css().reviewed()).closeTh();
+      } else {
+        sb.openTh().setStyleName(R.css().removeButton()).closeTh();
+      }
       sb.openTh().setStyleName(R.css().status()).closeTh();
       sb.openTh().append(Util.C.patchTableColumnName()).closeTh();
       sb.openTh()
@@ -466,7 +566,11 @@
     private void render(SafeHtmlBuilder sb, FileInfo info) {
       sb.openTr();
       sb.openTd().setStyleName(R.css().pointer()).closeTd();
-      columnReviewed(sb, info);
+      if (mode == Mode.REVIEW) {
+        columnReviewed(sb, info);
+      } else {
+        columnDeleteRestore(sb, info);
+      }
       columnStatus(sb, info);
       columnPath(sb, info);
       columnComments(sb, info);
@@ -487,6 +591,32 @@
       sb.closeTd();
     }
 
+    private void columnDeleteRestore(SafeHtmlBuilder sb, FileInfo info) {
+      sb.openTd().setStyleName(R.css().removeButton());
+      if (hasUser) {
+        if (!Patch.COMMIT_MSG.equals(info.path())) {
+          boolean editable = isEditable(info);
+          sb.openElement("button")
+            .setAttribute("title", editable
+                ? Resources.C.removeFileInline()
+                : Resources.C.restoreFileInline())
+            .setAttribute("onclick", (editable ? DELETE : RESTORE)
+                + "(event," + info._row() + ")")
+            .append(new ImageResourceRenderer().render(editable
+                ? Gerrit.RESOURCES.redNot()
+                : Gerrit.RESOURCES.editUndo()))
+            .closeElement("button");
+        }
+      }
+      sb.closeTd();
+    }
+
+    private boolean isEditable(FileInfo info) {
+      String status = info.status();
+      return status == null
+          || !ChangeType.DELETED.matches(status);
+    }
+
     private void columnStatus(SafeHtmlBuilder sb, FileInfo info) {
       sb.openTd().setStyleName(R.css().status());
       if (!Patch.COMMIT_MSG.equals(info.path())
@@ -500,14 +630,20 @@
     private void columnPath(SafeHtmlBuilder sb, FileInfo info) {
       sb.openTd()
         .setStyleName(R.css().pathColumn())
-        .openAnchor()
-        .setAttribute("href", "#" + url(info))
-        .setAttribute("onclick", OPEN + "(event," + info._row() + ")");
+        .openAnchor();
 
       String path = info.path();
+      if (mode == Mode.EDIT && !isEditable(info)) {
+        sb.setAttribute("onclick", RESTORE + "(event," + info._row() + ")");
+      } else {
+        sb.setAttribute("href", "#" + url(info))
+          .setAttribute("onclick", OPEN + "(event," + info._row() + ")");
+      }
+
       if (Patch.COMMIT_MSG.equals(path)) {
         sb.append(Util.C.commitMessage());
-      } else {
+      } else if (!hasUser || Gerrit.getUserAccount().getGeneralPreferences()
+          .isMuteCommonPathPrefixes()) {
         int commonPrefixLen = commonPrefix(path);
         if (commonPrefixLen > 0) {
           sb.openSpan().setStyleName(R.css().commonPrefix())
@@ -516,6 +652,8 @@
         }
         sb.append(path.substring(commonPrefixLen));
         lastPath = path;
+      } else {
+        sb.append(path);
       }
 
       sb.closeAnchor();
@@ -581,7 +719,10 @@
     }
 
     private JsArray<CommentInfo> get(String p, NativeMap<JsArray<CommentInfo>> m) {
-      JsArray<CommentInfo> r =  m.get(p);
+      JsArray<CommentInfo> r = null;
+      if (m != null) {
+        r = m.get(p);
+      }
       if (r == null) {
         r = JsArray.createArray().cast();
       }
@@ -591,14 +732,27 @@
     private void columnDelta1(SafeHtmlBuilder sb, FileInfo info) {
       sb.openTd().setStyleName(R.css().deltaColumn1());
       if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()) {
-        sb.append(info.lines_inserted() + info.lines_deleted());
+        if (showChangeSizeBars) {
+          sb.append(info.lines_inserted() + info.lines_deleted());
+        } else if (!ChangeType.DELETED.matches(info.status())) {
+          if (ChangeType.ADDED.matches(info.status())) {
+            sb.append(info.lines_inserted())
+              .append(" lines");
+          } else {
+            sb.append("+")
+              .append(info.lines_inserted())
+              .append(", -")
+              .append(info.lines_deleted());
+          }
+        }
       }
       sb.closeTd();
     }
 
     private void columnDelta2(SafeHtmlBuilder sb, FileInfo info) {
       sb.openTd().setStyleName(R.css().deltaColumn2());
-      if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()
+      if (showChangeSizeBars
+          && !Patch.COMMIT_MSG.equals(info.path()) && !info.binary()
           && (info.lines_inserted() != 0 || info.lines_deleted() != 0)) {
         int w = 80;
         int t = inserted + deleted;
@@ -629,7 +783,11 @@
     private void footer(SafeHtmlBuilder sb) {
       sb.openTr().setStyleName(R.css().nohover());
       sb.openTh().setStyleName(R.css().pointer()).closeTh();
-      sb.openTh().setStyleName(R.css().reviewed()).closeTh();
+      if (mode == Mode.REVIEW) {
+        sb.openTh().setStyleName(R.css().reviewed()).closeTh();
+      } else {
+        sb.openTh().setStyleName(R.css().removeButton()).closeTh();
+      }
       sb.openTh().setStyleName(R.css().status()).closeTh();
       sb.openTd().closeTd(); // path
       sb.openTd().setAttribute("colspan", 3).closeTd(); // comments
@@ -641,26 +799,28 @@
 
       // delta2
       sb.openTh().setStyleName(R.css().deltaColumn2());
-      int w = 80;
-      int t = inserted + deleted;
-      int i = Math.max(1, (int) (((double) w) * inserted / t));
-      int d = Math.max(1, (int) (((double) w) * deleted / t));
-      if (i + d > w && i > d) {
-        i = w - d;
-      } else if (i + d > w && d > i) {
-        d = w - i;
-      }
-      if (0 < inserted) {
-        sb.openDiv()
-        .setStyleName(R.css().inserted())
-        .setAttribute("style", "width:" + i + "px")
-        .closeDiv();
-      }
-      if (0 < deleted) {
-        sb.openDiv()
-          .setStyleName(R.css().deleted())
-          .setAttribute("style", "width:" + d + "px")
+      if (showChangeSizeBars) {
+        int w = 80;
+        int t = inserted + deleted;
+        int i = Math.max(1, (int) (((double) w) * inserted / t));
+        int d = Math.max(1, (int) (((double) w) * deleted / t));
+        if (i + d > w && i > d) {
+          i = w - d;
+        } else if (i + d > w && d > i) {
+          d = w - i;
+        }
+        if (0 < inserted) {
+          sb.openDiv()
+          .setStyleName(R.css().inserted())
+          .setAttribute("style", "width:" + i + "px")
           .closeDiv();
+        }
+        if (0 < deleted) {
+          sb.openDiv()
+            .setStyleName(R.css().deleted())
+            .setAttribute("style", "width:" + d + "px")
+            .closeDiv();
+        }
       }
       sb.closeTh();
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FollowUpAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FollowUpAction.java
new file mode 100644
index 0000000..5a7df72
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FollowUpAction.java
@@ -0,0 +1,47 @@
+// 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.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.PageLinks;
+import com.google.gwt.user.client.ui.Button;
+
+class FollowUpAction extends ActionMessageBox {
+  private final String project;
+  private final String branch;
+  private final String base;
+
+  FollowUpAction(Button b, String project, String branch, String key) {
+    super(b);
+    this.project = project;
+    this.branch = branch;
+    this.base = project + "~" + branch + "~" + key;
+  }
+
+  @Override
+  void send(String message) {
+    ChangeApi.createChange(project, branch, message, base,
+        new GerritCallback<ChangeInfo>() {
+          @Override
+          public void onSuccess(ChangeInfo result) {
+            Gerrit.display(PageLinks.toChange(result.legacy_id()));
+            hide();
+          }
+        });
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
new file mode 100644
index 0000000..faba10d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
@@ -0,0 +1,271 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+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.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.DOM;
+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.UIObject;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+import java.util.Iterator;
+
+public class Hashtags extends Composite {
+
+  interface Binder extends UiBinder<HTMLPanel, Hashtags> {}
+  private static final int VISIBLE_LENGTH = 55;
+  private static final Binder uiBinder = GWT.create(Binder.class);
+  private static final String REMOVE;
+  private static final String DATA_ID = "data-id";
+
+  static {
+    REMOVE = DOM.createUniqueId().replace('-', '_');
+    init(REMOVE);
+  }
+
+  private static final native void init(String r) /*-{
+    $wnd[r] = $entry(function(e) {
+      @com.google.gerrit.client.change.Hashtags::onRemove(Lcom/google/gwt/dom/client/NativeEvent;)(e)
+    });
+  }-*/;
+
+  private static void onRemove(NativeEvent event) {
+    String hashtags = getDataId(event);
+    if (hashtags != null) {
+      final ChangeScreen screen = ChangeScreen.get(event);
+      ChangeApi.hashtags(screen.getChangeId().get()).post(
+          PostInput.create(null, hashtags), new GerritCallback<JavaScriptObject>() {
+            @Override
+            public void onSuccess(JavaScriptObject result) {
+              if (screen.isCurrentView()) {
+                Gerrit.display(PageLinks.toChange(screen.getChangeId()));
+              }
+            }
+          });
+    }
+  }
+
+  private static String getDataId(NativeEvent event) {
+    Element e = event.getEventTarget().cast();
+    while (e != null) {
+      String v = e.getAttribute(DATA_ID);
+      if (!v.isEmpty()) {
+        return v;
+      }
+      e = e.getParentElement();
+    }
+    return null;
+  }
+
+  @UiField Element hashtagsText;
+  @UiField Button openForm;
+  @UiField Element form;
+  @UiField Element error;
+  @UiField NpTextBox hashtagTextBox;
+
+  private ChangeScreen.Style style;
+  private Change.Id changeId;
+
+  public Hashtags() {
+
+    initWidget(uiBinder.createAndBindUi(this));
+
+    hashtagTextBox.setVisibleLength(VISIBLE_LENGTH);
+    hashtagTextBox.addKeyDownHandler(new KeyDownHandler() {
+      @Override
+      public void onKeyDown(KeyDownEvent e) {
+        if (e.getNativeEvent().getKeyCode() == KeyCodes.KEY_ESCAPE) {
+          onCancel(null);
+        } else if (e.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
+          onAdd(null);
+        }
+      }
+    });
+  }
+
+  void init(ChangeScreen.Style style){
+    this.style = style;
+  }
+
+  void set(ChangeInfo info) {
+    this.changeId = info.legacy_id();
+    display(info);
+    openForm.setVisible(Gerrit.isSignedIn());
+  }
+
+  @UiHandler("openForm")
+  void onOpenForm(@SuppressWarnings("unused") ClickEvent e) {
+    onOpenForm();
+  }
+
+  void onOpenForm() {
+    UIObject.setVisible(form, true);
+    UIObject.setVisible(error, false);
+    openForm.setVisible(false);
+    hashtagTextBox.setFocus(true);
+  }
+
+  private void display(ChangeInfo info) {
+    hashtagsText.setInnerSafeHtml(formatHashtags(info));
+  }
+  private void display(JsArrayString hashtags) {
+    hashtagsText.setInnerSafeHtml(formatHashtags(hashtags));
+  }
+
+  private SafeHtmlBuilder formatHashtags(ChangeInfo info) {
+    if (info.hashtags() != null) {
+      return formatHashtags(info.hashtags());
+    }
+    return new SafeHtmlBuilder();
+  }
+
+  private SafeHtmlBuilder formatHashtags(JsArrayString hashtags) {
+    SafeHtmlBuilder html = new SafeHtmlBuilder();
+    Iterator<String> itr = Natives.asList(hashtags).iterator();
+    while (itr.hasNext()) {
+      String hashtagName = itr.next();
+      html.openSpan()
+          .setAttribute(DATA_ID, hashtagName)
+          .setStyleName(style.hashtagName())
+          .openAnchor()
+          .setAttribute("href",
+              "#" + PageLinks.toChangeQuery("hashtag:\"" + hashtagName + "\""))
+          .setAttribute("role", "listitem")
+          .append("#").append(hashtagName)
+          .closeAnchor()
+          .openElement("button")
+          .setAttribute("title", "Remove hashtag")
+          .setAttribute("onclick", REMOVE + "(event)")
+          .append("×")
+          .closeElement("button")
+          .closeSpan();
+      if (itr.hasNext()) {
+        html.append(' ');
+      }
+    }
+    return html;
+  }
+
+  @UiHandler("cancel")
+  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
+    openForm.setVisible(true);
+    UIObject.setVisible(form, false);
+    hashtagTextBox.setFocus(false);
+  }
+
+  @UiHandler("add")
+  void onAdd(@SuppressWarnings("unused") ClickEvent e) {
+    String hashtag = hashtagTextBox.getText();
+    if (!hashtag.isEmpty()) {
+      addHashtag(hashtag);
+    }
+  }
+
+  private void addHashtag(final String hashtags) {
+    ChangeApi.hashtags(changeId.get()).post(
+        PostInput.create(hashtags, null),
+        new GerritCallback<JsArrayString>() {
+          @Override
+          public void onSuccess(JsArrayString result) {
+            hashtagTextBox.setEnabled(true);
+            UIObject.setVisible(error, false);
+            error.setInnerText("");
+            hashtagTextBox.setText("");
+
+            if (result != null && result.length() > 0) {
+              updateHashtagList(result);
+            }
+          }
+
+          @Override
+          public void onFailure(Throwable err) {
+            UIObject.setVisible(error, true);
+            error.setInnerText(err instanceof StatusCodeException
+                ? ((StatusCodeException) err).getEncodedResponse()
+                : err.getMessage());
+            hashtagTextBox.setEnabled(true);
+          }
+        });
+  }
+
+  protected void updateHashtagList() {
+    ChangeApi.detail(changeId.get(),
+        new GerritCallback<ChangeInfo>() {
+          @Override
+          public void onSuccess(ChangeInfo result) {
+            display(result);
+          }
+        });
+  }
+
+  protected void updateHashtagList(JsArrayString hashtags){
+    display(hashtags);
+  }
+
+  public static class PostInput extends JavaScriptObject {
+    public static PostInput create(String add, String remove) {
+      PostInput input = createObject().cast();
+      input.init(toJsArrayString(add), toJsArrayString(remove));
+      return input;
+    }
+    private static JsArrayString toJsArrayString(String commaSeparated){
+      if (commaSeparated == null || commaSeparated.equals("")) {
+        return null;
+      }
+      JsArrayString array = JsArrayString.createArray().cast();
+      for (String hashtag : commaSeparated.split(",")){
+        array.push(hashtag.trim());
+      }
+      return array;
+    }
+
+    private native void init(JsArrayString add, JsArrayString remove) /*-{
+      this.add = add;
+      this.remove = remove;
+    }-*/;
+
+    protected PostInput() {
+    }
+  }
+
+  public static class Result extends JavaScriptObject {
+    public final native JsArrayString hashtags() /*-{ return this.hashtags; }-*/;
+    public final native String error() /*-{ return this.error; }-*/;
+
+    protected Result() {
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.ui.xml
new file mode 100644
index 0000000..dd06c77
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.ui.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<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>
+    button.openAdd {
+      margin: 3px 3px 0 0;
+      float: right;
+      color: rgba(0, 0, 0, 0.15);
+      background-color: #f5f5f5;
+      background-image: -webkit-linear-gradient(top, #f5f5f5, #f1f1f1);
+      -webkit-border-radius: 2px;
+      -moz-border-radius: 2px;
+      border-radius: 2px;
+      -webkit-box-sizing: content-box;
+      -moz-box-sizing: content-box;
+      box-sizing: content-box;
+    }
+    button.openAdd div {
+      width: auto;
+      color: #444;
+    }
+
+    .hashtagTextBox {
+      margin-bottom: 2px;
+    }
+
+    .error {
+      color: #D33D3D;
+      font-weight: bold;
+    }
+
+    .cancel {
+      float: right;
+    }
+  </ui:style>
+  <g:HTMLPanel>
+    <div>
+      <span ui:field='hashtagsText'/>
+      <g:Button ui:field='openForm'
+         title='Add hashtags to this change'
+         styleName='{res.style.button}'
+         addStyleNames='{style.openAdd}'
+         visible='false'>
+       <ui:attribute name='title'/>
+       <div><ui:msg>Add #...</ui:msg></div>
+      </g:Button>
+    </div>
+    <div ui:field='form' style='display: none' aria-hidden='true'>
+      <c:NpTextBox ui:field='hashtagTextBox' styleName='{style.hashtagTextBox}'/>
+      <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>
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/History.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/History.java
index 36be692..7635d81 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/History.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/History.java
@@ -65,7 +65,7 @@
         }
         add(ui);
       }
-      autoOpen(ChangeScreen2.myLastReply(info));
+      autoOpen(ChangeScreen.myLastReply(info));
     }
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInAction.java
index 58b286c..479670f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInAction.java
@@ -23,13 +23,14 @@
 
   IncludedInAction(
       Change.Id changeId,
-      ChangeScreen2.Style style,
+      ChangeScreen.Style style,
       UIObject relativeTo,
       Widget includedInButton) {
     super(style, relativeTo, includedInButton);
     this.includedInBox = new IncludedInBox(changeId);
   }
 
+  @Override
   Widget getWidget() {
     return includedInBox;
   }
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 2680347..a416894 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
@@ -26,13 +26,11 @@
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.NativeEvent;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.ImageResourceRenderer;
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
@@ -66,7 +64,7 @@
   private static void onRemove(NativeEvent event) {
     Integer user = getDataId(event);
     if (user != null) {
-      final ChangeScreen2 screen = ChangeScreen2.get(event);
+      final ChangeScreen screen = ChangeScreen.get(event);
       ChangeApi.reviewer(screen.getChangeId().get(), user).delete(
           new GerritCallback<JavaScriptObject>() {
             @Override
@@ -91,19 +89,16 @@
     return null;
   }
 
-  private ChangeScreen2.Style style;
-  private Element statusText;
+  private ChangeScreen.Style style;
 
-  void init(ChangeScreen2.Style style, Element statusText) {
+  void init(ChangeScreen.Style style) {
     this.style = style;
-    this.statusText = statusText;
   }
 
-  boolean set(ChangeInfo info, boolean current) {
+  void set(ChangeInfo info) {
     List<String> names = new ArrayList<>(info.labels());
     Collections.sort(names);
 
-    boolean canSubmit = info.status().isOpen();
     resize(names.size(), 2);
 
     for (int row = 0; row < names.size(); row++) {
@@ -115,30 +110,7 @@
       }
       getCellFormatter().setStyleName(row, 0, style.labelName());
       getCellFormatter().addStyleName(row, 0, getStyleForLabel(label));
-
-      if (canSubmit && info.status() == Change.Status.NEW) {
-        switch (label.status()) {
-          case NEED:
-            if (current) {
-              statusText.setInnerText("Needs " + name);
-            }
-            canSubmit = false;
-            break;
-          case REJECT:
-          case IMPOSSIBLE:
-            if (label.blocking()) {
-              if (current) {
-                statusText.setInnerText("Not " + name);
-              }
-              canSubmit = false;
-            }
-            break;
-          default:
-            break;
-          }
-      }
     }
-    return canSubmit;
   }
 
   private Widget renderUsers(LabelInfo label) {
@@ -222,7 +194,7 @@
     }
   }
 
-  static SafeHtml formatUserList(ChangeScreen2.Style style,
+  static SafeHtml formatUserList(ChangeScreen.Style style,
       Collection<? extends AccountInfo> in,
       Set<Integer> removable,
       Map<Integer, VotableInfo> votable) {
@@ -301,7 +273,7 @@
         html.openElement("button")
             .setAttribute("title", Util.M.removeReviewer(name))
             .setAttribute("onclick", REMOVE + "(event)")
-            .append(new ImageResourceRenderer().render(Resources.I.remove_reviewer()))
+            .append("×")
             .closeElement("button");
       }
       html.closeSpan();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
index 6540638..8fa5a68 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
@@ -19,7 +19,7 @@
 import com.google.gerrit.client.diff.DisplaySide;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.common.changes.Side;
+import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.Element;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
index e3799ca..22d39a7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
@@ -89,7 +89,7 @@
     this.history = parent;
     this.info = info;
 
-    name.setInnerText(authorName(info));
+    setName(false);
     date.setInnerText(FormatUtil.shortFormatDayTime(info.date()));
     if (info.message() != null) {
       String msg = info.message().trim();
@@ -129,6 +129,7 @@
         commentList = Collections.emptyList();
       }
     }
+    setName(open);
 
     UIObject.setVisible(summary, !open);
     UIObject.setVisible(message, open);
@@ -140,6 +141,18 @@
     }
   }
 
+  private void setName(boolean open) {
+    name.setInnerText(open ? authorName(info) : elide(authorName(info), 20));
+  }
+
+  private static String elide(final String s, final int len) {
+    if (s == null || s.length() <= len || len <= 10) {
+      return s;
+    }
+    int i = (len - 3) / 2;
+    return s.substring(0, i) + "..." + s.substring(s.length() - i);
+  }
+
   void autoOpen() {
     if (commentList == null) {
       autoOpen = true;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml
index fe0c97b..42cb39b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml
@@ -64,9 +64,8 @@
       font-weight: bold;
     }
     .closed .name {
-      width: 120px;
+      width: 150px;
       overflow: hidden;
-      text-overflow: ellipsis;
       font-weight: normal;
     }
 
@@ -74,7 +73,7 @@
       color: #777;
       position: absolute;
       top: 0;
-      left: 120px;
+      left: 150px;
       width: 880px;
       overflow: hidden;
       text-overflow: ellipsis;
@@ -103,7 +102,7 @@
       font-size: 18px;
     }
     .closed .reply {
-      visibility: HIDDEN;
+      visibility: hidden;
     }
     .comment {
     }
@@ -125,11 +124,13 @@
           <div>&#x21a9;</div>
         </g:Button>
       </g:HTMLPanel>
-      <div ui:field='message'
-           aria-hidden='true'
-           style='display: NONE'
-           styleName='{style.comment}'/>
-      <g:FlowPanel ui:field='comments' visible='false'/>
+      <div style='overflow: auto'>
+        <div ui:field='message'
+            aria-hidden='true'
+            style='display: NONE'
+            styleName='{style.comment}'/>
+        <g:FlowPanel ui:field='comments' visible='false'/>
+      </div>
     </div>
   </g:HTMLPanel>
 </ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/NewChangeScreenBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/NewChangeScreenBar.java
deleted file mode 100644
index fa65514..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/NewChangeScreenBar.java
+++ /dev/null
@@ -1,105 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.change;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.AccountApi;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ChangeScreen;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.event.dom.client.ClickEvent;
-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.Cookies;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.UIObject;
-
-import java.util.Date;
-
-/** Displays a welcome to the new change screen bar. */
-class NewChangeScreenBar extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, NewChangeScreenBar> {}
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  static boolean show() {
-    if (Gerrit.isSignedIn()) {
-      return Gerrit.getUserAccount()
-          .getGeneralPreferences()
-          .getChangeScreen() == null;
-    }
-    return Cookies.getCookie(Dispatcher.COOKIE_CS2) == null;
-  }
-
-  private final Change.Id id;
-
-  @UiField Element docs;
-  @UiField Element settings;
-  @UiField Anchor keepNew;
-  @UiField Anchor keepOld;
-
-  NewChangeScreenBar(Change.Id id) {
-    this.id = id;
-    initWidget(uiBinder.createAndBindUi(this));
-    UIObject.setVisible(docs, Gerrit.getConfig().isDocumentationAvailable());
-    UIObject.setVisible(settings, Gerrit.isSignedIn());
-  }
-
-  @UiHandler("keepOld")
-  void onKeepOld(ClickEvent e) {
-    save(ChangeScreen.OLD_UI);
-    Gerrit.display(PageLinks.toChange(id));
-  }
-
-  @UiHandler("keepNew")
-  void onKeepNew(ClickEvent e) {
-    save(ChangeScreen.CHANGE_SCREEN2);
-  }
-
-  private void save(ChangeScreen sel) {
-    removeFromParent();
-    Dispatcher.changeScreen2 = sel == ChangeScreen.CHANGE_SCREEN2;
-
-    if (Gerrit.isSignedIn()) {
-      Gerrit.getUserAccount().getGeneralPreferences().setChangeScreen(sel);
-
-      Prefs in = Prefs.createObject().cast();
-      in.change_screen(sel.name());
-      AccountApi.self().view("preferences").background().post(in,
-        new AsyncCallback<JavaScriptObject>() {
-          @Override public void onFailure(Throwable caught) {}
-          @Override public void onSuccess(JavaScriptObject result) {}
-        });
-    } else {
-      Cookies.setCookie(
-        Dispatcher.COOKIE_CS2,
-        Dispatcher.changeScreen2 ? "1" : "0",
-        new Date(System.currentTimeMillis() + 7 * 24 * 3600 * 1000));
-    }
-  }
-
-  private static class Prefs extends JavaScriptObject {
-    final native void change_screen(String n) /*-{ this.change_screen=n }-*/;
-    protected Prefs() {
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/NewChangeScreenBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/NewChangeScreenBar.ui.xml
deleted file mode 100644
index 92eff8d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/NewChangeScreenBar.ui.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-<?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:style>
-    .popup {
-      position: fixed;
-      top: 5px;
-      left: 50%;
-      margin-left: -200px;
-      z-index: 201;
-      padding-top: 5px;
-      padding-bottom: 5px;
-      padding-left: 12px;
-      padding-right: 12px;
-      text-align: center;
-      background: #FFF1A8;
-      border-radius: 10px;
-    }
-
-    @if user.agent safari {
-      .popup {
-        \-webkit-border-radius: 10px;
-      }
-    }
-    @if user.agent gecko1_8 {
-      .popup {
-        \-moz-border-radius: 10px;
-      }
-    }
-
-    a.action {
-      color: #222;
-      text-decoration: underline;
-      display: inline-block;
-      margin-left: 0.5em;
-    }
-    .welcome { font-weight: bold; }
-  </ui:style>
-  <g:HTMLPanel styleName='{style.popup}'>
-    <div><ui:msg><span class='{style.welcome}'>Welcome to the new change screen!</span>
-      <a ui:field='docs'
-         class='{style.action}'
-         href='Documentation/user-review-ui.html'
-         target='_blank'>Learn more</a></ui:msg>
-    </div>
-    <div>
-      <ui:msg>You can<g:Anchor ui:field='keepOld'
-          styleName='{style.action}'
-          href='javascript:;'
-          title='Switch back to the old screen'><ui:attribute name='title'/>revert
-            to the old screen</g:Anchor><span ui:field='settings'>&#160;in Settings &gt; Preferences</span>.
-      <g:Anchor ui:field='keepNew'
-          styleName='{style.action}'
-          href='javascript:;'
-          title='Keep the new change screen'>
-        <ui:attribute name='title'/>
-        Got it!
-      </g:Anchor></ui:msg>
-    </div>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsAction.java
index b29ca11..49cbd04 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsAction.java
@@ -24,13 +24,14 @@
   PatchSetsAction(
       Change.Id changeId,
       String revision,
-      ChangeScreen2.Style style,
+      ChangeScreen.Style style,
       UIObject relativeTo,
       Widget downloadButton) {
     super(style, relativeTo, downloadButton);
     this.revisionBox = new PatchSetsBox(changeId, revision);
   }
 
+  @Override
   Widget getWidget() {
     return revisionBox;
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java
index 2fa721e..92f6956 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java
@@ -19,14 +19,17 @@
 import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.changes.ChangeInfo.EditInfo;
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.changes.ChangeList;
+import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.ui.FancyFlexTableImpl;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JsArray;
@@ -57,6 +60,7 @@
 
   private static final String OPEN;
   private static final HyperlinkImpl link = GWT.create(HyperlinkImpl.class);
+  private EditInfo edit;
 
   static {
     OPEN = DOM.createUniqueId().replace('-', '_');
@@ -116,14 +120,31 @@
   @Override
   protected void onLoad() {
     if (!loaded) {
+      CallbackGroup group = new CallbackGroup();
+      if (Gerrit.isSignedIn()) {
+        // TODO(davido): It shouldn't be necessary to make this call.
+        // PatchSetsBox is constructed via PatchSetsAction which is
+        // only initialized by CS2 after loading the EditInfo in that path.
+        ChangeApi.edit(changeId.get(), group.add(
+            new GerritCallback<EditInfo>() {
+              @Override
+              public void onSuccess(EditInfo result) {
+                edit = result;
+              }
+            }));
+      }
       RestApi call = ChangeApi.detail(changeId.get());
       ChangeList.addOptions(call, EnumSet.of(
           ListChangesOption.ALL_COMMITS,
           ListChangesOption.ALL_REVISIONS,
           ListChangesOption.DRAFT_COMMENTS));
-      call.get(new AsyncCallback<ChangeInfo>() {
+      call.get(group.addFinal(new AsyncCallback<ChangeInfo>() {
         @Override
         public void onSuccess(ChangeInfo result) {
+          if (edit != null) {
+            edit.set_name(edit.commit().commit());
+            result.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
+          }
           render(result.revisions());
           loaded = true;
         }
@@ -131,7 +152,7 @@
         @Override
         public void onFailure(Throwable caught) {
         }
-      });
+      }));
     }
   }
 
@@ -189,7 +210,7 @@
         .closeSpan()
         .append(' ');
     }
-    sb.append(r._number());
+    sb.append(r.id());
     sb.closeTd();
 
     sb.openTd()
@@ -218,9 +239,7 @@
   }
 
   private String url(RevisionInfo r) {
-    return PageLinks.toChange(
-        changeId,
-        String.valueOf(r._number()));
+    return PageLinks.toChange(changeId, r.id());
   }
 
   private void closeParent() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PathSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PathSuggestOracle.java
new file mode 100644
index 0000000..7667559
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PathSuggestOracle.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+class PathSuggestOracle extends HighlightSuggestOracle {
+
+  private final Change.Id changeId;
+  private final RevisionInfo revision;
+
+  PathSuggestOracle(Change.Id changeId, RevisionInfo revision) {
+    this.changeId = changeId;
+    this.revision = revision;
+  }
+
+  @Override
+  protected void onRequestSuggestions(final Request req, final Callback cb) {
+    ChangeApi.revision(changeId.get(), revision.name())
+      .view("files")
+      .addParameter("q", req.getQuery())
+      .background()
+      .get(new AsyncCallback<JsArrayString>() {
+          @Override
+          public void onSuccess(JsArrayString result) {
+            List<Suggestion> r = new ArrayList<>();
+            for (String path : Natives.asList(result)) {
+              r.add(new PathSuggestion(path));
+            }
+            cb.onSuggestionsReady(req, new Response(r));
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            List<Suggestion> none = Collections.emptyList();
+            cb.onSuggestionsReady(req, new Response(none));
+          }
+        });
+  }
+
+  private static class PathSuggestion implements Suggestion {
+    private final String path;
+
+    PathSuggestion(String path) {
+      this.path = path;
+    }
+
+    @Override
+    public String getDisplayString() {
+      return path;
+    }
+
+    @Override
+    public String getReplacementString() {
+      return path;
+    }
+  }
+}
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 6e86730..6638dbe 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
@@ -46,6 +46,10 @@
       setVisible(false);
       return;
     }
+    if (info.revision(commit).is_edit() || info.revision(commit).draft()) {
+      setVisible(false);
+      return;
+    }
 
     String qName = null;
     String qValueStr = null;
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 2b1c250..790198b 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
@@ -18,16 +18,42 @@
 import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.RebaseDialog;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.PopupPanel;
 
 class RebaseAction {
-  static void call(final Change.Id id, String revision) {
-    ChangeApi.rebase(id.get(), revision,
-      new GerritCallback<ChangeInfo>() {
-        public void onSuccess(ChangeInfo result) {
-          Gerrit.display(PageLinks.toChange(id));
-        }
-      });
+  static void call(final Button b, final String project, final String branch,
+      final Change.Id id, final String revision, final boolean enabled) {
+    b.setEnabled(false);
+
+    new RebaseDialog(project, branch, id, enabled) {
+      @Override
+      public void onSend() {
+        ChangeApi.rebase(id.get(), revision, getBase(), new GerritCallback<ChangeInfo>() {
+          @Override
+          public void onSuccess(ChangeInfo result) {
+            sent = true;
+            hide();
+            Gerrit.display(PageLinks.toChange(id));
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            enableButtons(true);
+            super.onFailure(caught);
+          }
+        });
+      }
+
+      @Override
+      public void onClose(CloseEvent<PopupPanel> event) {
+        super.onClose(event);
+        b.setEnabled(true);
+      }
+    }.center();
   }
 }
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 5a0ba15..0c78a67 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
@@ -22,7 +22,7 @@
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.changes.ChangeList;
 import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
@@ -62,7 +62,7 @@
     String tabPanel();
   }
 
-  private enum Tab {
+  enum Tab {
     RELATED_CHANGES(Resources.C.relatedChanges(),
         Resources.C.relatedChangesTooltip()) {
       @Override
@@ -127,6 +127,8 @@
     }
   }
 
+  private static Tab savedTab;
+
   private final List<RelatedChangesTab> tabs;
   private int maxHeightWithHeader;
   private int selectedTab;
@@ -155,7 +157,7 @@
     });
 
     for (Tab tabInfo : Tab.values()) {
-      RelatedChangesTab panel = new RelatedChangesTab();
+      RelatedChangesTab panel = new RelatedChangesTab(tabInfo);
       add(panel, tabInfo.defaultTitle);
       tabs.add(panel);
 
@@ -167,6 +169,8 @@
     }
     getTab(Tab.RELATED_CHANGES).setShowIndirectAncestors(true);
     getTab(Tab.CHERRY_PICKS).setShowBranches(true);
+    getTab(Tab.SAME_TOPIC).setShowBranches(true);
+    getTab(Tab.SAME_TOPIC).setShowProjects(true);
   }
 
   void set(final ChangeInfo info, final String revision) {
@@ -194,8 +198,6 @@
     if (info.topic() != null && !"".equals(info.topic())) {
       StringBuilder topicQuery = new StringBuilder();
       topicQuery.append("status:open");
-      topicQuery.append(" ").append(op("project", info.project()));
-      topicQuery.append(" ").append(op("branch", info.branch()));
       topicQuery.append(" ").append(op("topic", info.topic()));
       topicQuery.append(" ").append(op("-change", info.legacy_id().get()));
       ChangeList.query(topicQuery.toString(),
@@ -222,6 +224,10 @@
     R.css().ensureInjected();
   }
 
+  static void setSavedTab(Tab subject) {
+    savedTab = subject;
+  }
+
   private RelatedChangesTab getTab(Tab tabInfo) {
     return tabs.get(tabInfo.ordinal());
   }
@@ -264,19 +270,23 @@
 
     @Override
     public void onSuccess(T result) {
-      JsArray<ChangeAndCommit> changes = convert(result);
-      if (changes.length() > 0) {
-        setTabTitle(tabInfo, tabInfo.getTitle(changes.length()));
-        getTab(tabInfo).setChanges(project, revision, changes);
+      if (isAttached()) {
+        JsArray<ChangeAndCommit> changes = convert(result);
+        if (changes.length() > 0) {
+          setTabTitle(tabInfo, tabInfo.getTitle(changes.length()));
+          getTab(tabInfo).setChanges(project, revision, changes);
+        }
+        onDone(changes.length() > 0);
       }
-      onDone(changes.length() > 0);
     }
 
     @Override
     public void onFailure(Throwable err) {
-      setTabTitle(tabInfo, tabInfo.getTitle(Resources.C.notAvailable()));
-      getTab(tabInfo).setError(err.getMessage());
-      onDone(true);
+      if (isAttached()) {
+        setTabTitle(tabInfo, tabInfo.getTitle(Resources.C.notAvailable()));
+        getTab(tabInfo).setError(err.getMessage());
+        onDone(true);
+      }
     }
 
     private void onDone(boolean enabled) {
@@ -293,6 +303,10 @@
           }
         }
       }
+
+      if (tabInfo == savedTab && enabled) {
+        selectTab(savedTab.ordinal());
+      }
     }
   }
 
@@ -313,6 +327,7 @@
           c.set_change_number(i.legacy_id().get());
           c.set_revision_number(currentRevision._number());
           c.set_branch(i.branch());
+          c.set_project(i.project());
           arr.push(c);
         }
       }
@@ -334,6 +349,7 @@
     public final native String id() /*-{ return this.change_id }-*/;
     public final native CommitInfo commit() /*-{ return this.commit }-*/;
     final native String branch() /*-{ return this.branch }-*/;
+    final native String project() /*-{ return this.project }-*/;
 
     final native void set_id(String i)
     /*-{ if(i)this.change_id=i; }-*/;
@@ -344,6 +360,9 @@
     final native void set_branch(String b)
     /*-{ if(b)this.branch=b; }-*/;
 
+    final native void set_project(String b)
+    /*-{ if(b)this.project=b; }-*/;
+
     public final Change.Id legacy_id() {
       return has_change_number() ? new Change.Id(_change_number()) : null;
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
index e1ce99d..96d0d10 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
@@ -82,8 +82,10 @@
   }
 
   private final SimplePanel panel;
+  private final RelatedChanges.Tab subject;
 
   private boolean showBranches;
+  private boolean showProjects;
   private boolean showIndirectAncestors;
   private boolean registerKeys;
   private int maxHeight;
@@ -91,8 +93,9 @@
   private String project;
   private NavigationList view;
 
-  RelatedChangesTab() {
+  RelatedChangesTab(RelatedChanges.Tab subject) {
     panel = new SimplePanel();
+    this.subject = subject;
   }
 
   @Override
@@ -104,6 +107,10 @@
     this.showBranches = showBranches;
   }
 
+  void setShowProjects(boolean showProjects) {
+    this.showProjects = showProjects;
+  }
+
   void setShowIndirectAncestors(boolean showIndirectAncestors) {
     this.showIndirectAncestors = showIndirectAncestors;
   }
@@ -200,6 +207,7 @@
       return false;
     }
 
+    @Override
     public boolean execute() {
       if (navList != view || !panel.isAttached()) {
         // If the user navigated away, we aren't in the DOM anymore.
@@ -273,6 +281,9 @@
         if (url.startsWith("#")) {
           sb.setAttribute("onclick", OPEN);
         }
+        if (showProjects) {
+          sb.append(info.project()).append(": ");
+        }
         if (showBranches) {
           sb.append(info.branch()).append(": ");
         }
@@ -311,7 +322,7 @@
         PatchSet.Id id = info.patch_set_id();
         return "#" + PageLinks.toChange(
             id.getParentKey(),
-            String.valueOf(id.get()));
+            id.getId());
       }
 
       GitwebLink gw = Gerrit.getGitwebLink();
@@ -486,6 +497,7 @@
         movePointerTo(startRow + DOM.getChildIndex(body, row), false);
         event.stopPropagation();
       }
+      saveSelectedTab();
     }
 
     @Override
@@ -536,6 +548,12 @@
           }
         }
       }
+
+      saveSelectedTab();
+    }
+
+    private void saveSelectedTab() {
+      RelatedChanges.setSavedTab(subject);
     }
 
     @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileAction.java
new file mode 100644
index 0000000..1f11e65
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileAction.java
@@ -0,0 +1,71 @@
+//Copyright (C) 2015 The Android Open Source Project
+//
+//Licensed under the Apache License, Version 2.0 (the "License");
+//you may not use this file except in compliance with the License.
+//You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing, software
+//distributed under the License is distributed on an "AS IS" BASIS,
+//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//See the License for the specific language governing permissions and
+//limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+class RenameFileAction {
+  private final Change.Id changeId;
+  private final RevisionInfo revision;
+  private final ChangeScreen.Style style;
+  private final Widget renameButton;
+
+  private RenameFileBox renameBox;
+  private PopupPanel popup;
+
+  RenameFileAction(Change.Id changeId, RevisionInfo revision,
+      ChangeScreen.Style style, Widget renameButton) {
+    this.changeId = changeId;
+    this.revision = revision;
+    this.style = style;
+    this.renameButton = renameButton;
+  }
+
+  void onRename() {
+    if (popup != null) {
+      popup.hide();
+      return;
+    }
+
+    if (renameBox == null) {
+      renameBox = new RenameFileBox(changeId, revision);
+    }
+    renameBox.clearPath();
+
+    final PluginSafePopupPanel p = new PluginSafePopupPanel(true);
+    p.setStyleName(style.replyBox());
+    p.addAutoHidePartner(renameButton.getElement());
+    p.addCloseHandler(new CloseHandler<PopupPanel>() {
+      @Override
+      public void onClose(CloseEvent<PopupPanel> event) {
+        if (popup == p) {
+          popup = null;
+        }
+      }
+    });
+    p.add(renameBox);
+    p.showRelativeTo(renameButton);
+    GlobalKey.dialog(p);
+    renameBox.setFocus(true);
+    popup = p;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.java
new file mode 100644
index 0000000..77348f7
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.java
@@ -0,0 +1,107 @@
+//Copyright (C) 2015 The Android Open Source Project
+//
+//Licensed under the Apache License, Version 2.0 (the "License");
+//you may not use this file except in compliance with the License.
+//You may obtain a copy of the License at
+//
+//http://www.apache.org/licenses/LICENSE-2.0
+//
+//Unless required by applicable law or agreed to in writing, software
+//distributed under the License is distributed on an "AS IS" BASIS,
+//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//See the License for the specific language governing permissions and
+//limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.changes.ChangeEditApi;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.ui.RemoteSuggestBox;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+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.AsyncCallback;
+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.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+
+class RenameFileBox extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, RenameFileBox> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  private final Change.Id changeId;
+
+  @UiField Button rename;
+  @UiField Button cancel;
+
+  @UiField(provided = true)
+  RemoteSuggestBox path;
+  @UiField NpTextBox newPath;
+
+  RenameFileBox(Change.Id changeId, RevisionInfo revision) {
+    this.changeId = changeId;
+
+    path = new RemoteSuggestBox(new PathSuggestOracle(changeId, revision));
+    path.addCloseHandler(new CloseHandler<RemoteSuggestBox>() {
+      @Override
+      public void onClose(CloseEvent<RemoteSuggestBox> event) {
+        hide();
+      }
+    });
+
+    initWidget(uiBinder.createAndBindUi(this));
+  }
+
+  void setFocus(boolean focus) {
+    path.setFocus(focus);
+  }
+
+  void clearPath() {
+    path.setText("");
+  }
+
+  @UiHandler("rename")
+  void onRename(@SuppressWarnings("unused") ClickEvent e) {
+    rename(path.getText(), newPath.getText());
+  }
+
+  private void rename(String path, String newPath) {
+    hide();
+    ChangeEditApi.rename(changeId.get(), path, newPath,
+        new AsyncCallback<VoidResult>() {
+          @Override
+          public void onSuccess(VoidResult result) {
+            Gerrit.display(PageLinks.toChangeInEditMode(changeId));
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+          }
+        });
+  }
+
+  @UiHandler("cancel")
+  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
+    hide();
+  }
+
+  private void hide() {
+    for (Widget w = getParent(); w != null; w = w.getParent()) {
+      if (w instanceof PopupPanel) {
+        ((PopupPanel) w).hide();
+        break;
+      }
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.ui.xml
similarity index 70%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.ui.xml
copy to gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.ui.xml
index 9df4bbc..27849ee 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.ui.xml
@@ -16,30 +16,26 @@
 -->
 <ui:UiBinder
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'
-    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'>
+    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
+    xmlns:u='urn:import:com.google.gerrit.client.ui'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
   <ui:style>
-    .commitMessage {
-      background-color: white;
-      font-family: monospace;
-    }
     .cancel { float: right; }
   </ui:style>
   <g:HTMLPanel>
     <div class='{res.style.section}'>
-      <c:NpTextArea
-         visibleLines='30'
-         characterWidth='78'
-         styleName='{style.commitMessage}'
-         ui:field='message'/>
+      <ui:msg>Old: <u:RemoteSuggestBox ui:field='path' visibleLength='86'/></ui:msg>
     </div>
     <div class='{res.style.section}'>
-      <g:Button ui:field='save'
-          title='Create new patch set with updated commit message'
+      <ui:msg>New: <c:NpTextBox ui:field='newPath' visibleLength='86'/></ui:msg>
+    </div>
+    <div class='{res.style.section}'>
+      <g:Button ui:field='rename'
+          title='Rename file in the repository'
           styleName='{res.style.button}'>
         <ui:attribute name='title'/>
-        <div><ui:msg>Save</ui:msg></div>
+        <div><ui:msg>Rename</ui:msg></div>
       </g:Button>
       <g:Button ui:field='cancel'
           styleName='{res.style.button}'
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java
index b234fe3..6e40979 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java
@@ -33,7 +33,7 @@
 class ReplyAction {
   private final PatchSet.Id psId;
   private final String revision;
-  private final ChangeScreen2.Style style;
+  private final ChangeScreen.Style style;
   private final CommentLinkProcessor clp;
   private final Widget replyButton;
   private final Widget quickApproveButton;
@@ -47,7 +47,7 @@
   ReplyAction(
       ChangeInfo info,
       String revision,
-      ChangeScreen2.Style style,
+      ChangeScreen.Style style,
       CommentLinkProcessor clp,
       Widget replyButton,
       Widget quickApproveButton) {
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 ddd650e..f9054fe 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
@@ -76,8 +76,6 @@
 import java.util.TreeSet;
 
 class ReplyBox extends Composite {
-  private static final String CODE_REVIEW = "Code-Review";
-
   interface Binder extends UiBinder<HTMLPanel, ReplyBox> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
@@ -92,7 +90,6 @@
   private final String revision;
   private ReviewInput in = ReviewInput.create();
   private int labelHelpColumn;
-  private Runnable lgtm;
 
   @UiField Styles style;
   @UiField TextArea message;
@@ -173,22 +170,8 @@
       }}, 0);
   }
 
-  @UiHandler("message")
-  void onMessageKey(KeyPressEvent event) {
-    if (lgtm != null
-        && event.getCharCode() == 'M'
-        && message.getValue().equals("LGT")) {
-      Scheduler.get().scheduleDeferred(new ScheduledCommand() {
-        @Override
-        public void execute() {
-          lgtm.run();
-        }
-      });
-    }
-  }
-
   @UiHandler("post")
-  void onPost(ClickEvent e) {
+  void onPost(@SuppressWarnings("unused") ClickEvent e) {
     postReview();
   }
 
@@ -214,7 +197,7 @@
   }
 
   @UiHandler("cancel")
-  void onCancel(ClickEvent e) {
+  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
     message.setText("");
     hide();
   }
@@ -358,15 +341,6 @@
         labelsTable.setWidget(row, 1 + i, b);
       }
     }
-
-    if (CODE_REVIEW.equalsIgnoreCase(id) && !group.buttons.isEmpty()) {
-      lgtm = new Runnable() {
-        @Override
-        public void run() {
-          group.selectMax();
-        }
-      };
-    }
   }
 
   private void renderCheckBox(int row, LabelAndValues lv) {
@@ -377,7 +351,6 @@
     final String id = lv.info.name();
     final CheckBox b = new CheckBox();
     b.setText(id);
-    b.setTitle(lv.info.value_text("+1"));
     b.setEnabled(lv.permitted.contains((short) 1));
     if (self != null && self.value() == 1) {
       b.setValue(true);
@@ -391,14 +364,9 @@
     b.setStyleName(style.label_name());
     labelsTable.setWidget(row, 0, b);
 
-    if (CODE_REVIEW.equalsIgnoreCase(id)) {
-      lgtm = new Runnable() {
-        @Override
-        public void run() {
-          b.setValue(true, true);
-        }
-      };
-    }
+    CellFormatter fmt = labelsTable.getCellFormatter();
+    fmt.setStyleName(row, labelHelpColumn, style.label_help());
+    labelsTable.setText(row, labelHelpColumn, lv.info.value_text("+1"));
   }
 
   private static boolean isCheckBox(Set<Short> values) {
@@ -467,16 +435,6 @@
       selected = b;
       labelsTable.setText(row, labelHelpColumn, b.text);
     }
-
-    void selectMax() {
-      for (int i = 0; i < buttons.size() - 1; i++) {
-        buttons.get(i).setValue(false, false);
-      }
-
-      LabelRadioButton max = buttons.get(buttons.size() - 1);
-      max.setValue(true, true);
-      max.select();
-    }
   }
 
   private class LabelRadioButton extends RadioButton implements
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
index 3c927fc..a17d648 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
@@ -63,12 +63,12 @@
       <g:FlowPanel ui:field='comments'/>
     </g:ScrollPanel>
     <div class='{res.style.section}' style='position: relative'>
-      <ui:msg><g:Button ui:field='post'
+      <g:Button ui:field='post'
           title='Post reply (Shortcut: Ctrl-Enter)'
           styleName='{res.style.button}'>
         <ui:attribute name='title'/>
-        <div>Post</div>
-      </g:Button></ui:msg>
+        <div><ui:msg>Post</ui:msg></div>
+      </g:Button>
 
       <g:Button ui:field='cancel'
           title='Close reply form (Shortcut: Esc)'
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java
index 7ca3c53..4fc76c1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java
@@ -17,16 +17,12 @@
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.resources.client.ClientBundle;
 import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.resources.client.ImageResource;
 
 public interface Resources extends ClientBundle {
   public static final Resources I = GWT.create(Resources.class);
   static final ChangeConstants C = GWT.create(ChangeConstants.class);
   static final ChangeMessages M = GWT.create(ChangeMessages.class);
 
-  @Source("star_open.png") ImageResource star_open();
-  @Source("star_filled.png") ImageResource star_filled();
-  @Source("remove_reviewer.png") ImageResource remove_reviewer();
   @Source("common.css") Style style();
 
   public interface Style extends CssResource {
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 215f39d..5ec292b 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
@@ -30,6 +30,7 @@
     this.id = id;
   }
 
+  @Override
   void send(String message) {
     ChangeApi.restore(id.get(), message, new GerritCallback<ChangeInfo>() {
       @Override
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 5f9f076..3af97f5 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
@@ -19,17 +19,17 @@
 import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.ActionDialog;
+import com.google.gerrit.client.ui.TextAreaActionDialog;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.user.client.ui.Button;
 
 class RevertAction {
   static void call(Button b, final Change.Id id, final String revision,
-      String project, final String commitSubject) {
+      final String commitSubject) {
     // TODO Replace ActionDialog with a nicer looking display.
     b.setEnabled(false);
-    new ActionDialog(b, false,
+    new TextAreaActionDialog(
         Util.C.revertChangeTitle(),
         Util.C.headingRevertMessage()) {
       {
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/ReviewerSuggestOracle.java
similarity index 79%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestReviewerSuggestOracle.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java
index b5a3023..7af247a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestReviewerSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java
@@ -25,28 +25,32 @@
 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.Collections;
 import java.util.List;
 
 /** REST API based suggestion Oracle for reviewers. */
-public class RestReviewerSuggestOracle extends SuggestAfterTypingNCharsOracle {
-
+public class ReviewerSuggestOracle 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>>() {
+  protected void _onRequestSuggestions(final Request req, final Callback cb) {
+    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<>(result.length());
-            for (final SuggestReviewerInfo reviewer : Natives.asList(result)) {
+            List<RestReviewerSuggestion> r = new ArrayList<>(result.length());
+            for (SuggestReviewerInfo reviewer : Natives.asList(result)) {
               r.add(new RestReviewerSuggestion(reviewer));
             }
-            callback.onSuggestionsReady(req, new Response(r));
+            cb.onSuggestionsReady(req, new Response(r));
+          }
+
+          @Override
+          public void onFailure(Throwable err) {
+            List<Suggestion> r = Collections.emptyList();
+            cb.onSuggestionsReady(req, new Response(r));
           }
         });
   }
@@ -55,13 +59,14 @@
     this.changeId = changeId;
   }
 
-  private static class RestReviewerSuggestion implements SuggestOracle.Suggestion {
+  private static class RestReviewerSuggestion implements Suggestion {
     private final SuggestReviewerInfo reviewer;
 
     RestReviewerSuggestion(final SuggestReviewerInfo reviewer) {
       this.reviewer = reviewer;
     }
 
+    @Override
     public String getDisplayString() {
       if (reviewer.account() != null) {
         return FormatUtil.nameEmail(reviewer.account());
@@ -72,6 +77,7 @@
           + ")";
     }
 
+    @Override
     public String getReplacementString() {
       if (reviewer.account() != null) {
         return FormatUtil.nameEmail(reviewer.account());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
index 87320b8..18e5e87 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
@@ -27,16 +27,15 @@
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.HintTextBox;
+import com.google.gerrit.client.ui.RemoteSuggestBox;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyDownEvent;
-import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
 import com.google.gwt.event.logical.shared.SelectionEvent;
 import com.google.gwt.event.logical.shared.SelectionHandler;
 import com.google.gwt.uibinder.client.UiBinder;
@@ -46,9 +45,6 @@
 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;
@@ -69,53 +65,36 @@
   @UiField Element form;
   @UiField Element error;
   @UiField(provided = true)
-  SuggestBox suggestBox;
+  RemoteSuggestBox suggestBox;
 
-  private ChangeScreen2.Style style;
+  private ChangeScreen.Style style;
   private Element ccText;
 
-  private RestReviewerSuggestOracle reviewerSuggestOracle;
-  private HintTextBox nameTxtBox;
+  private ReviewerSuggestOracle reviewerSuggestOracle;
   private Change.Id changeId;
-  private boolean submitOnSelection;
 
   Reviewers() {
-    reviewerSuggestOracle = new RestReviewerSuggestOracle();
-    nameTxtBox = new HintTextBox();
-    suggestBox = new SuggestBox(reviewerSuggestOracle, nameTxtBox);
+    reviewerSuggestOracle = new ReviewerSuggestOracle();
+    suggestBox = new RemoteSuggestBox(reviewerSuggestOracle);
+    suggestBox.setVisibleLength(55);
+    suggestBox.setHintText(Util.C.approvalTableAddReviewerHint());
+    suggestBox.addCloseHandler(new CloseHandler<RemoteSuggestBox>() {
+      @Override
+      public void onClose(CloseEvent<RemoteSuggestBox> event) {
+        Reviewers.this.onCancel(null);
+      }
+    });
+    suggestBox.addSelectionHandler(new SelectionHandler<String>() {
+      @Override
+      public void onSelection(SelectionEvent<String> event) {
+        addReviewer(event.getSelectedItem(), false);
+      }
+    });
+
     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 init(ChangeScreen2.Style style, Element ccText) {
+  void init(ChangeScreen.Style style, Element ccText) {
     this.style = style;
     this.ccText = ccText;
   }
@@ -128,7 +107,7 @@
   }
 
   @UiHandler("openForm")
-  void onOpenForm(ClickEvent e) {
+  void onOpenForm(@SuppressWarnings("unused") ClickEvent e) {
     onOpenForm();
   }
 
@@ -140,33 +119,33 @@
   }
 
   @UiHandler("add")
-  void onAdd(ClickEvent e) {
-    String reviewer = suggestBox.getText();
-    if (!reviewer.isEmpty()) {
-      addReviewer(reviewer, false);
-    }
+  void onAdd(@SuppressWarnings("unused") ClickEvent e) {
+    addReviewer(suggestBox.getText(), false);
   }
 
   @UiHandler("addMe")
-  void onAddMe(ClickEvent e) {
+  void onAddMe(@SuppressWarnings("unused") ClickEvent e) {
     String accountId = String.valueOf(Gerrit.getUserAccountInfo()._account_id());
     addReviewer(accountId, false);
   }
 
   @UiHandler("cancel")
-  void onCancel(ClickEvent e) {
+  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
     openForm.setVisible(true);
     UIObject.setVisible(form, false);
     suggestBox.setFocus(false);
   }
 
   private void addReviewer(final String reviewer, boolean confirmed) {
+    if (reviewer.isEmpty()) {
+      return;
+    }
+
     ChangeApi.reviewers(changeId.get()).post(
         PostInput.create(reviewer, confirmed),
         new GerritCallback<PostResult>() {
+          @Override
           public void onSuccess(PostResult result) {
-            nameTxtBox.setEnabled(true);
-
             if (result.confirm()) {
               askForConfirmation(result.error());
             } else if (result.error() != null) {
@@ -175,7 +154,7 @@
             } else {
               UIObject.setVisible(error, false);
               error.setInnerText("");
-              nameTxtBox.setText("");
+              suggestBox.setText("");
 
               if (result.reviewers() != null
                   && result.reviewers().length() > 0) {
@@ -202,7 +181,6 @@
             error.setInnerText(err instanceof StatusCodeException
                 ? ((StatusCodeException) err).getEncodedResponse()
                 : err.getMessage());
-            nameTxtBox.setEnabled(true);
           }
         });
   }
@@ -230,7 +208,6 @@
     for (Integer i : r.keySet()) {
       cc.remove(i);
     }
-    r.remove(info.owner()._account_id());
     cc.remove(info.owner()._account_id());
 
     Set<Integer> removable = new HashSet<>();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
index 024197d..9924c1d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
@@ -17,15 +17,16 @@
 <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'>
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'
+    xmlns:u='urn:import:com.google.gerrit.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
   <ui:style>
     button.openAdd {
       margin: 3px 3px 0 0;
       float: right;
-      color: #444;
+      color: rgba(0, 0, 0, 0.15);
       background-color: #f5f5f5;
-      background-image: -webkit-linear-gradient(top, #f5f5f5, #f1f1f1);
+      background-image: none;
       -webkit-border-radius: 2px;
       -moz-border-radius: 2px;
       border-radius: 2px;
@@ -64,7 +65,7 @@
       </g:Button>
     </div>
     <div ui:field='form' style='display: none' aria-hidden='true'>
-      <g:SuggestBox ui:field='suggestBox' styleName='{style.suggestBox}'/>
+      <u:RemoteSuggestBox ui:field='suggestBox' styleName='{style.suggestBox}'/>
       <div ui:field='error'
            class='{style.error}'
            style='display: none' aria-hidden='true'/>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RightSidePopdownAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RightSidePopdownAction.java
index 3424064..1bf6f6c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RightSidePopdownAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RightSidePopdownAction.java
@@ -26,13 +26,13 @@
 import com.google.gwtexpui.user.client.PluginSafePopupPanel;
 
 abstract class RightSidePopdownAction {
-  private final ChangeScreen2.Style style;
+  private final ChangeScreen.Style style;
   private final Widget button;
   private final UIObject relativeTo;
   private PopupPanel popup;
 
   RightSidePopdownAction(
-      ChangeScreen2.Style style,
+      ChangeScreen.Style style,
       UIObject relativeTo,
       Widget button) {
     this.style = style;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/StarIcon.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/StarIcon.java
index fccca27..0f19406 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/StarIcon.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/StarIcon.java
@@ -14,13 +14,14 @@
 
 package com.google.gerrit.client.change;
 
+import com.google.gerrit.client.Gerrit;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.ToggleButton;
 
 class StarIcon extends ToggleButton {
   StarIcon() {
     super(
-      new Image(Resources.I.star_open()),
-      new Image(Resources.I.star_filled()));
+      new Image(Gerrit.RESOURCES.starOpen()),
+      new Image(Gerrit.RESOURCES.starFilled()));
   }
 }
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 4ca992b..09d3476 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
@@ -19,7 +19,6 @@
 import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.changes.SubmitFailureDialog;
 import com.google.gerrit.client.changes.SubmitInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.common.PageLinks;
@@ -32,10 +31,12 @@
       ChangeApi.submit(
         changeId.get(), revisionInfo.name(),
         new GerritCallback<SubmitInfo>() {
+          @Override
           public void onSuccess(SubmitInfo result) {
             redisplay();
           }
 
+          @Override
           public void onFailure(Throwable err) {
             if (SubmitFailureDialog.isConflict(err)) {
               new SubmitFailureDialog(err.getMessage()).center();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitFailureDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitFailureDialog.java
similarity index 81%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitFailureDialog.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitFailureDialog.java
index bd97790..c9ac2cb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitFailureDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitFailureDialog.java
@@ -12,19 +12,20 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.changes;
+package com.google.gerrit.client.change;
 
 import com.google.gerrit.client.ErrorDialog;
+import com.google.gerrit.client.changes.Util;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 import com.google.gwtjsonrpc.client.RemoteJsonException;
 
-public class SubmitFailureDialog extends ErrorDialog {
-  public static boolean isConflict(Throwable err) {
+class SubmitFailureDialog extends ErrorDialog {
+  static boolean isConflict(Throwable err) {
     return err instanceof RemoteJsonException
         && 409 == ((RemoteJsonException) err).getCode();
   }
 
-  public SubmitFailureDialog(String msg) {
+  SubmitFailureDialog(String msg) {
     super(new SafeHtmlBuilder().append(msg.trim()).wikify());
     setText(Util.C.submitFailed());
   }
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 512203a..9f45678 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
@@ -27,6 +27,7 @@
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.uibinder.client.UiHandler;
@@ -97,6 +98,7 @@
 
   void onEdit() {
     if (canEdit) {
+      UIObject.setVisible(show, false);
       UIObject.setVisible(form, true);
 
       input.setText(text.getText());
@@ -105,9 +107,10 @@
   }
 
   @UiHandler("cancel")
-  void onCancel(ClickEvent e) {
+  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
     input.setFocus(false);
     UIObject.setVisible(form, false);
+    UIObject.setVisible(show, true);
   }
 
   @UiHandler("input")
@@ -116,12 +119,13 @@
       onCancel(null);
     } else if (e.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
       e.stopPropagation();
+      e.preventDefault();
       onSave(null);
     }
   }
 
   @UiHandler("save")
-  void onSave(ClickEvent e) {
+  void onSave(@SuppressWarnings("unused") ClickEvent e) {
     ChangeApi.topic(
         psId.getParentKey().get(),
         input.getValue().trim(),
@@ -135,4 +139,11 @@
         });
     onCancel(null);
   }
+
+  @UiHandler("save")
+  void onSaveKeyPress(KeyPressEvent e) {
+    if (e.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
+      e.stopPropagation();
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java
index 2b6f418..cdbc693 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java
@@ -61,12 +61,12 @@
   }
 
   @UiHandler("show")
-  void onShow(ClickEvent e) {
+  void onShow(@SuppressWarnings("unused") ClickEvent e) {
     onShow();
   }
 
   @UiHandler("ignore")
-  void onIgnore(ClickEvent e) {
+  void onIgnore(@SuppressWarnings("unused") ClickEvent e) {
     onIgnore(updated);
     removeFromParent();
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateCheckTimer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateCheckTimer.java
index 9d2a467..5820a50 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateCheckTimer.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateCheckTimer.java
@@ -28,11 +28,11 @@
   private static final int POLL_PERIOD =
       Gerrit.getConfig().getChangeUpdateDelay() * 1000;
 
-  private final ChangeScreen2 screen;
+  private final ChangeScreen screen;
   private int delay;
   private boolean running;
 
-  UpdateCheckTimer(ChangeScreen2 screen) {
+  UpdateCheckTimer(ChangeScreen screen) {
     this.screen = screen;
     this.delay = POLL_PERIOD;
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/common.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/common.css
index ae00dc7..bb7cb27 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/common.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/common.css
@@ -29,11 +29,11 @@
 .popup button,
 .popup input[type='button'] {
   margin: 0 3px 0 0;
-  border-color: rgba(0, 0, 0, 0.1);
+  border-color: rgba(0, 0, 0, 0.15) !important;
   text-align: center;
   font-size: 11px;
   font-weight: bold;
-  border: 1px solid;
+  border: 2px solid;
   cursor: pointer;
   color: #fff;
   background-color: #4d90fe;
@@ -53,8 +53,8 @@
   width: 54px;
   white-space: nowrap;
   color: #fff;
-  height: 10px;
-  line-height: 10px;
+  height: 14px;
+  line-height: 14px;
 }
 
 .section {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
index 4b695d2..2803db3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
@@ -13,12 +13,22 @@
  * limitations under the License.
  */
 
-.pointer, .reviewed {
-  width: 12px;
+.pointer, .reviewed, .removeButton {
   padding: 0px;
   vertical-align: top;
 }
+.pointer {
+  width: 12px;
+}
+.reviewed, .removeButton {
+  height: 19px;
+  width: 20px;
+}
 
+.table th {
+  vertical-align: top;
+  text-align: left;
+}
 .table tr {
   vertical-align: top;
 }
@@ -40,6 +50,7 @@
 }
 .pathColumn a {
   color: #000;
+  cursor: pointer;
 }
 .commonPrefix {
   color: #888;
@@ -85,3 +96,12 @@
   background-color: #d44;
 }
 
+.removeButton button {
+  cursor: pointer;
+  padding: 0;
+  margin: 0 0 0 5px;
+  border: 0;
+  background-color: transparent;
+  white-space: nowrap;
+}
+
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/more_less.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/moreLess.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/change/more_less.png
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/change/moreLess.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/remove_reviewer.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/remove_reviewer.png
deleted file mode 100644
index 5a3e6f0..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/remove_reviewer.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
index f312d11..d9e9878 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
@@ -19,7 +19,7 @@
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.event.dom.client.KeyPressEvent;
@@ -32,10 +32,10 @@
 public class AccountDashboardScreen extends Screen implements ChangeListScreen {
   private final Account.Id ownerId;
   private final boolean mine;
-  private ChangeTable2 table;
-  private ChangeTable2.Section outgoing;
-  private ChangeTable2.Section incoming;
-  private ChangeTable2.Section closed;
+  private ChangeTable table;
+  private ChangeTable.Section outgoing;
+  private ChangeTable.Section incoming;
+  private ChangeTable.Section closed;
 
   public AccountDashboardScreen(final Account.Id id) {
     ownerId = id;
@@ -45,7 +45,7 @@
   @Override
   protected void onInitUI() {
     super.onInitUI();
-    table = new ChangeTable2() {
+    table = new ChangeTable() {
       {
         keysNavigation.add(new KeyCommand(0, 'R', Util.C.keyReloadSearch()) {
           @Override
@@ -57,9 +57,9 @@
     };
     table.addStyleName(Gerrit.RESOURCES.css().accountDashboard());
 
-    outgoing = new ChangeTable2.Section();
-    incoming = new ChangeTable2.Section();
-    closed = new ChangeTable2.Section();
+    outgoing = new ChangeTable.Section();
+    incoming = new ChangeTable.Section();
+    closed = new ChangeTable.Section();
 
     outgoing.setTitleText(Util.C.outgoingReviews());
     incoming.setTitleText(Util.C.incomingReviews());
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
deleted file mode 100644
index b41a082..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
+++ /dev/null
@@ -1,394 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import static com.google.gerrit.common.data.LabelValue.formatValue;
-
-import com.google.gerrit.client.ConfirmationCallback;
-import com.google.gerrit.client.ConfirmationDialog;
-import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.AccountInfo;
-import com.google.gerrit.client.change.Reviewers.PostInput;
-import com.google.gerrit.client.change.Reviewers.PostResult;
-import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
-import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.ui.AccountLinkPanel;
-import com.google.gerrit.client.ui.AddMemberBox;
-import com.google.gerrit.client.ui.ReviewerSuggestOracle;
-import com.google.gerrit.common.data.ApprovalDetail;
-import com.google.gerrit.common.data.SubmitRecord;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.Panel;
-import com.google.gwt.user.client.ui.PushButton;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/** Displays a table of {@link ApprovalDetail} objects for a change record. */
-public class ApprovalTable extends Composite {
-  private final Grid table;
-  private final Widget missing;
-  private final Panel addReviewer;
-  private final ReviewerSuggestOracle reviewerSuggestOracle;
-  private final AddMemberBox addMemberBox;
-  private ChangeInfo lastChange;
-  private Map<Integer, Integer> rows;
-
-  public ApprovalTable() {
-    rows = new HashMap<>();
-    table = new Grid(1, 3);
-    table.addStyleName(Gerrit.RESOURCES.css().infoTable());
-
-    missing = new Widget() {
-      {
-        setElement((Element)(DOM.createElement("ul")));
-      }
-    };
-    missing.setStyleName(Gerrit.RESOURCES.css().missingApprovalList());
-
-    addReviewer = new FlowPanel();
-    addReviewer.setStyleName(Gerrit.RESOURCES.css().addReviewer());
-    reviewerSuggestOracle = new ReviewerSuggestOracle();
-    addMemberBox =
-        new AddMemberBox(Util.C.approvalTableAddReviewer(),
-            Util.C.approvalTableAddReviewerHint(), reviewerSuggestOracle);
-    addMemberBox.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        doAddReviewer();
-      }
-    });
-    addReviewer.add(addMemberBox);
-    addReviewer.setVisible(false);
-
-    final FlowPanel fp = new FlowPanel();
-    fp.add(table);
-    fp.add(missing);
-    fp.add(addReviewer);
-    initWidget(fp);
-
-    setStyleName(Gerrit.RESOURCES.css().approvalTable());
-  }
-
-  /**
-   * Sets the header row
-   *
-   * @param labels The list of labels to display in the header. This list does
-   *    not get resorted, so be sure that the list's elements are in the same
-   *    order as the list of labels passed to the {@code displayRow} method.
-   */
-  private void displayHeader(Collection<String> labels) {
-    table.resizeColumns(2 + labels.size());
-
-    final CellFormatter fmt = table.getCellFormatter();
-    int col = 0;
-
-    table.setText(0, col, Util.C.approvalTableReviewer());
-    fmt.setStyleName(0, col, Gerrit.RESOURCES.css().header());
-    col++;
-
-    table.clearCell(0, col);
-    fmt.setStyleName(0, col, Gerrit.RESOURCES.css().header());
-    col++;
-
-    for (String name : labels) {
-      table.setText(0, col, name);
-      fmt.setStyleName(0, col, Gerrit.RESOURCES.css().header());
-      col++;
-    }
-    fmt.addStyleName(0, col - 1, Gerrit.RESOURCES.css().rightmost());
-  }
-
-  void display(ChangeInfo change) {
-    lastChange = change;
-    reviewerSuggestOracle.setChange(change.legacy_id());
-    Map<Integer, ApprovalDetail> byUser = new LinkedHashMap<>();
-    Map<Integer, AccountInfo> accounts = new LinkedHashMap<>();
-    List<String> missingLabels = initLabels(change, accounts, byUser);
-
-    removeAllChildren(missing.getElement());
-    for (String label : missingLabels) {
-      addMissingLabel(Util.M.needApproval(label));
-    }
-
-    if (byUser.isEmpty()) {
-      table.setVisible(false);
-    } else {
-      List<String> labels = new ArrayList<>(change.labels());
-      Collections.sort(labels);
-      displayHeader(labels);
-      table.resizeRows(1 + byUser.size());
-      int i = 1;
-      for (ApprovalDetail ad : ApprovalDetail.sort(
-          byUser.values(), change.owner()._account_id())) {
-        displayRow(i++, ad, labels, accounts.get(ad.getAccount().get()));
-      }
-      table.setVisible(true);
-    }
-
-    if (change.status() != Change.Status.MERGED
-        && !change.mergeable()) {
-      addMissingLabel(Util.C.messageNeedsRebaseOrHasDependency());
-    }
-    missing.setVisible(missing.getElement().getChildCount() > 0);
-    addReviewer.setVisible(Gerrit.isSignedIn());
-  }
-
-  private void removeAllChildren(Element el) {
-    for (int i = DOM.getChildCount(el) - 1; i >= 0; i--) {
-      el.removeChild(DOM.getChild(el, i));
-    }
-  }
-
-  private void addMissingLabel(String text) {
-    Element li = DOM.createElement("li");
-    li.setClassName(Gerrit.RESOURCES.css().missingApproval());
-    li.setInnerText(text);
-    DOM.appendChild(missing.getElement(), li);
-  }
-
-  private Set<Integer> removableReviewers(ChangeInfo change) {
-    Set<Integer> result =
-        new HashSet<>(change.removable_reviewers().length());
-    for (int i = 0; i < change.removable_reviewers().length(); i++) {
-      result.add(change.removable_reviewers().get(i)._account_id());
-    }
-    return result;
-  }
-
-  private List<String> initLabels(ChangeInfo change,
-      Map<Integer, AccountInfo> accounts,
-      Map<Integer, ApprovalDetail> byUser) {
-    Set<Integer> removableReviewers = removableReviewers(change);
-    List<String> missing = new ArrayList<>();
-    for (String name : change.labels()) {
-      LabelInfo label = change.label(name);
-
-      String min = null;
-      String max = null;
-      for (String v : label.values()) {
-        if (min == null) {
-          min = v;
-        }
-        if (v.startsWith("+")) {
-          max = v;
-        }
-      }
-
-      if (label.status() == SubmitRecord.Label.Status.NEED) {
-        missing.add(name);
-      }
-
-      if (label.all() != null) {
-        for (ApprovalInfo ai : Natives.asList(label.all())) {
-          if (!accounts.containsKey(ai._account_id())) {
-            accounts.put(ai._account_id(), ai);
-          }
-          int id = ai._account_id();
-          ApprovalDetail ad = byUser.get(id);
-          if (ad == null) {
-            ad = new ApprovalDetail(new Account.Id(id));
-            ad.setCanRemove(removableReviewers.contains(id));
-            byUser.put(id, ad);
-          }
-          if (ai.has_value()) {
-            ad.votable(name);
-            ad.value(name, ai.value());
-            String fv = formatValue(ai.value());
-            if (fv.equals(max)) {
-              ad.approved(name);
-            } else if (ai.value() < 0 && fv.equals(min)) {
-              ad.rejected(name);
-            }
-          }
-        }
-      }
-    }
-    return missing;
-  }
-
-  private void doAddReviewer() {
-    String reviewer = addMemberBox.getText();
-    if (!reviewer.isEmpty()) {
-      addMemberBox.setEnabled(false);
-      addReviewer(reviewer, false);
-    }
-  }
-
-  private void addReviewer(final String reviewer, boolean confirmed) {
-    ChangeApi.reviewers(lastChange.legacy_id().get()).post(
-        PostInput.create(reviewer, confirmed),
-        new GerritCallback<PostResult>() {
-          public void onSuccess(PostResult result) {
-            addMemberBox.setEnabled(true);
-            addMemberBox.setText("");
-            if (result.error() == null) {
-              reload();
-            } else if (result.confirm()) {
-              askForConfirmation(result.error());
-            } else {
-              new ErrorDialog(new SafeHtmlBuilder().append(result.error()));
-            }
-          }
-
-          private void askForConfirmation(String text) {
-            String title = Util.C
-                .approvalTableAddManyReviewersConfirmationDialogTitle();
-            ConfirmationDialog confirmationDialog = new ConfirmationDialog(
-                title, new SafeHtmlBuilder().append(text),
-                new ConfirmationCallback() {
-                  @Override
-                  public void onOk() {
-                    addReviewer(reviewer, true);
-                  }
-                });
-            confirmationDialog.center();
-          }
-
-          @Override
-          public void onFailure(final Throwable caught) {
-            addMemberBox.setEnabled(true);
-            if (isNoSuchEntity(caught)) {
-              new ErrorDialog(Util.M.reviewerNotFound(reviewer)).center();
-            } else {
-              super.onFailure(caught);
-            }
-          }
-        });
-  }
-
-  /**
-   * Sets the reviewer data for a row.
-   *
-   * @param row The number of the row on which to set the reviewer.
-   * @param ad The details for this reviewer's approval.
-   * @param labels The list of labels to show. This list does not get resorted,
-   *    so be sure that the list's elements are in the same order as the list
-   *    of labels passed to the {@code displayHeader} method.
-   * @param account The account information for the approval.
-   */
-  private void displayRow(int row, final ApprovalDetail ad,
-      List<String> labels, AccountInfo account) {
-    final CellFormatter fmt = table.getCellFormatter();
-    int col = 0;
-
-    table.setWidget(row, col++, new AccountLinkPanel(account));
-    rows.put(account._account_id(), row);
-
-    if (ad.canRemove()) {
-      final PushButton remove = new PushButton( //
-          new Image(Util.R.removeReviewerNormal()), //
-          new Image(Util.R.removeReviewerPressed()));
-      remove.setTitle(Util.M.removeReviewer(account.name()));
-      remove.setStyleName(Gerrit.RESOURCES.css().removeReviewer());
-      remove.addStyleName(Gerrit.RESOURCES.css().link());
-      remove.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(ClickEvent event) {
-          doRemove(ad, remove);
-        }
-      });
-      table.setWidget(row, col, remove);
-    } else {
-      table.clearCell(row, col);
-    }
-    fmt.setStyleName(row, col++, Gerrit.RESOURCES.css().removeReviewerCell());
-
-    for (String labelName : labels) {
-      fmt.setStyleName(row, col, Gerrit.RESOURCES.css().approvalscore());
-      if (!ad.canVote(labelName)) {
-        fmt.addStyleName(row, col, Gerrit.RESOURCES.css().notVotable());
-        fmt.getElement(row, col).setTitle(Gerrit.C.userCannotVoteToolTip());
-      }
-
-      if (ad.isRejected(labelName)) {
-        table.setWidget(row, col, new Image(Gerrit.RESOURCES.redNot()));
-
-      } else if (ad.isApproved(labelName)) {
-        table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck()));
-
-      } else {
-        int v = ad.getValue(labelName);
-        if (v == 0) {
-          table.clearCell(row, col);
-          col++;
-          continue;
-        }
-        String vstr = String.valueOf(ad.getValue(labelName));
-        if (v > 0) {
-          vstr = "+" + vstr;
-          fmt.addStyleName(row, col, Gerrit.RESOURCES.css().posscore());
-        } else {
-          fmt.addStyleName(row, col, Gerrit.RESOURCES.css().negscore());
-        }
-        table.setText(row, col, vstr);
-      }
-
-      col++;
-    }
-
-    fmt.addStyleName(row, col - 1, Gerrit.RESOURCES.css().rightmost());
-  }
-
-  private void reload() {
-    ChangeApi.detail(lastChange.legacy_id().get(),
-        new GerritCallback<ChangeInfo>() {
-          @Override
-          public void onSuccess(ChangeInfo result) {
-            display(result);
-          }
-        });
-  }
-
-  private void doRemove(ApprovalDetail ad, final PushButton remove) {
-    remove.setEnabled(false);
-    ChangeApi.reviewer(lastChange.legacy_id().get(), ad.getAccount().get())
-      .delete(new GerritCallback<JavaScriptObject>() {
-          @Override
-          public void onSuccess(JavaScriptObject result) {
-            reload();
-          }
-
-          @Override
-          public void onFailure(final Throwable caught) {
-            remove.setEnabled(true);
-            super.onFailure(caught);
-          }
-        });
-  }
-}
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 406ffd9..98595e7 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
@@ -14,9 +14,14 @@
 
 package com.google.gerrit.client.changes;
 
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.changes.ChangeInfo.EditInfo;
 import com.google.gerrit.client.changes.ChangeInfo.IncludedInInfo;
+import com.google.gerrit.client.rpc.CallbackGroup.Callback;
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.user.client.rpc.AsyncCallback;
@@ -33,6 +38,28 @@
     call(id, "abandon").post(input, cb);
   }
 
+  /** Create a new change.
+   *
+   * The new change is created as DRAFT unless the draft workflow is disabled
+   * by `change.allowDrafts = false` in the configuration, in which case the
+   * new change is created as NEW.
+   *
+   */
+  public static void createChange(String project, String branch,
+      String subject, String base, AsyncCallback<ChangeInfo> cb) {
+    CreateChangeInput input = CreateChangeInput.create();
+    input.project(emptyToNull(project));
+    input.branch(emptyToNull(branch));
+    input.subject(emptyToNull(subject));
+    input.base_change(emptyToNull(base));
+
+    if (Gerrit.getConfig().isAllowDraftChanges()) {
+      input.status(Change.Status.DRAFT.toString());
+    }
+
+    new RestApi("/changes/").post(input, cb);
+  }
+
   /** Restore a previously abandoned change to be open again. */
   public static void restore(int id, String msg, AsyncCallback<ChangeInfo> cb) {
     Input input = Input.create();
@@ -68,6 +95,22 @@
     return call(id, "detail");
   }
 
+  public static void edit(int id, AsyncCallback<EditInfo> cb) {
+    edit(id).get(cb);
+  }
+
+  public static void editWithFiles(int id, AsyncCallback<EditInfo> cb) {
+    edit(id).addParameterTrue("list").get(cb);
+  }
+
+  public static RestApi edit(int id) {
+    return change(id).view("edit");
+  }
+
+  public static RestApi editWithCommands(int id) {
+    return edit(id).addParameterTrue("download-commands");
+  }
+
   public static void includedIn(int id, AsyncCallback<IncludedInInfo> cb) {
     call(id, "in").get(cb);
   }
@@ -103,6 +146,13 @@
     return change(id).view("reviewers").id(reviewer);
   }
 
+  public static RestApi hashtags(int changeId) {
+    return change(changeId).view("hashtags");
+  }
+  public static RestApi hashtag(int changeId, String hashtag){
+    return change(changeId).view("hashtags").id(hashtag);
+  }
+
   /** Submit a specific revision of a change. */
   public static void cherrypick(int id, String commit, String destination, String message, AsyncCallback<ChangeInfo> cb) {
     CherryPickInput cherryPickInput = CherryPickInput.create();
@@ -142,10 +192,28 @@
     revision(id, commit).delete(cb);
   }
 
-  /** Rebase a revision onto the branch tip. */
-  public static void rebase(int id, String commit, AsyncCallback<ChangeInfo> cb) {
+  /** Delete change edit. */
+  public static void deleteEdit(int id, AsyncCallback<JavaScriptObject> cb) {
+    edit(id).delete(cb);
+  }
+
+  /** Publish change edit. */
+  public static void publishEdit(int id, AsyncCallback<JavaScriptObject> cb) {
     JavaScriptObject in = JavaScriptObject.createObject();
-    call(id, commit, "rebase").post(in, cb);
+    change(id).view("edit:publish").post(in, cb);
+  }
+
+  /** Rebase change edit on latest patch set. */
+  public static void rebaseEdit(int id, AsyncCallback<JavaScriptObject> cb) {
+    JavaScriptObject in = JavaScriptObject.createObject();
+    change(id).view("edit:rebase").post(in, cb);
+  }
+
+  /** Rebase a revision onto the branch tip or another change. */
+  public static void rebase(int id, String commit, String base, AsyncCallback<ChangeInfo> cb) {
+    RebaseInput rebaseInput = RebaseInput.create();
+    rebaseInput.setBase(base);
+    call(id, commit, "rebase").post(rebaseInput, cb);
   }
 
   private static class Input extends JavaScriptObject {
@@ -160,6 +228,21 @@
     }
   }
 
+  private static class CreateChangeInput extends JavaScriptObject {
+    static CreateChangeInput create() {
+      return (CreateChangeInput) createObject();
+    }
+
+    public final native void branch(String b) /*-{ if(b)this.branch=b; }-*/;
+    public final native void project(String p) /*-{ if(p)this.project=p; }-*/;
+    public final native void subject(String s) /*-{ if(s)this.subject=s; }-*/;
+    public final native void base_change(String b) /*-{ if(b)this.base_change=b; }-*/;
+    public final native void status(String s)  /*-{ if(s)this.status=s; }-*/;
+
+    protected CreateChangeInput() {
+    }
+  }
+
   private static class CherryPickInput extends JavaScriptObject {
     static CherryPickInput create() {
       return (CherryPickInput) createObject();
@@ -171,6 +254,17 @@
     }
   }
 
+  private static class RebaseInput extends JavaScriptObject {
+    final native void setBase(String b) /*-{ this.base = b; }-*/;
+
+    static RebaseInput create() {
+      return (RebaseInput) createObject();
+    }
+
+    protected RebaseInput() {
+    }
+  }
+
   private static class SubmitInput extends JavaScriptObject {
     final native void wait_for_merge(boolean b) /*-{ this.wait_for_merge=b; }-*/;
 
@@ -198,4 +292,12 @@
   public static String emptyToNull(String str) {
     return str == null || str.isEmpty() ? null : str;
   }
+
+  public static void commitWithLinks(int changeId, String revision,
+      Callback<CommitInfo> callback) {
+    revision(changeId, revision)
+        .view("commit")
+        .addParameterTrue("links")
+        .get(callback);
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java
deleted file mode 100644
index 7fd5290..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import com.google.gerrit.client.ui.ListenableValue;
-import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.reviewdb.client.Change;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/** A Cache to store common client side data by change */
-public class ChangeCache {
-  private static Map<Change.Id, ChangeCache> caches = new HashMap<>();
-
-  public static ChangeCache get(Change.Id chg) {
-    ChangeCache cache = caches.get(chg);
-    if (cache == null) {
-      cache = new ChangeCache(chg);
-      caches.put(chg, cache);
-    }
-    return cache;
-  }
-
-  private Change.Id changeId;
-  private ChangeDetailCache detail;
-  private ListenableValue<ChangeInfo> info;
-
-  protected ChangeCache(Change.Id chg) {
-    changeId = chg;
-  }
-
-  public Change.Id getChangeId() {
-    return changeId;
-  }
-
-  public ChangeDetailCache getChangeDetailCache() {
-    if (detail == null) {
-      detail = new ChangeDetailCache(changeId);
-    }
-    return detail;
-  }
-
-  public ListenableValue<ChangeInfo> getChangeInfoCache() {
-    if (info == null) {
-      info = new ListenableValue<>();
-    }
-    return info;
-  }
-}
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 03c58d2..6531129 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
@@ -25,6 +25,7 @@
   String readyToSubmit();
   String mergeConflict();
   String notCurrent();
+  String changeEdit();
 
   String myDashboardTitle();
   String unknownDashboardTitle();
@@ -163,6 +164,12 @@
   String cherryPickCommitMessage();
   String cherryPickTitle();
 
+  String buttonRebaseChangeSend();
+  String rebaseConfirmMessage();
+  String rebaseNotPossibleMessage();
+  String rebasePlaceholderMessage();
+  String rebaseTitle();
+
   String buttonAbandonChangeBegin();
   String buttonAbandonChangeSend();
   String headingAbandonMessage();
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 c904cac..45415c5 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
@@ -6,6 +6,7 @@
 readyToSubmit = Ready to Submit
 mergeConflict = Merge Conflict
 notCurrent = Not Current
+changeEdit = Change Edit
 
 starredHeading = Starred Changes
 watchedHeading = Open Changes of Watched Projects
@@ -149,6 +150,12 @@
 cherryPickCommitMessage = Cherry Pick Commit Message:
 cherryPickTitle = Code Review - Cherry Pick Change to Another Branch
 
+buttonRebaseChangeSend = Rebase
+rebaseConfirmMessage = Change parent revision
+rebaseNotPossibleMessage = Change is already up to date
+rebasePlaceholderMessage = (subject, change number, or leave empty)
+rebaseTitle = Code Review - Rebase Change
+
 buttonRestoreChangeBegin = Restore Change
 restoreChangeTitle = Code Review - Restore Change
 headingRestoreMessage = Restore Message:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
deleted file mode 100644
index 8f2642c..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.common.data.AccountInfoCache;
-import com.google.gerrit.common.data.ChangeDetail;
-import com.google.gerrit.common.data.SubmitTypeRecord;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwtexpui.globalkey.client.KeyCommandSet;
-
-public class ChangeDescriptionBlock extends Composite {
-  private final ChangeInfoBlock infoBlock;
-  private final CommitMessageBlock messageBlock;
-
-  public ChangeDescriptionBlock(KeyCommandSet keysAction) {
-    infoBlock = new ChangeInfoBlock();
-    messageBlock = new CommitMessageBlock(keysAction);
-
-    final HorizontalPanel hp = new HorizontalPanel();
-    hp.add(infoBlock);
-    hp.add(messageBlock);
-    initWidget(hp);
-  }
-
-  public void display(ChangeDetail changeDetail, Boolean starred, Boolean canEditCommitMessage,
-      PatchSetInfo info, AccountInfoCache acc,
-      SubmitTypeRecord submitTypeRecord,
-      CommentLinkProcessor commentLinkProcessor) {
-    infoBlock.display(changeDetail, acc, submitTypeRecord);
-    messageBlock.display(changeDetail.getChange().currentPatchSetId(), info.getRevId(), starred,
-        canEditCommitMessage, info.getMessage(), commentLinkProcessor);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
deleted file mode 100644
index f2c97c1..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
+++ /dev/null
@@ -1,261 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import com.google.gerrit.client.actions.ActionInfo;
-import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
-import com.google.gerrit.client.changes.ChangeInfo.GitPerson;
-import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
-import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.client.ui.ListenableValue;
-import com.google.gerrit.common.data.AccountInfo;
-import com.google.gerrit.common.data.AccountInfoCache;
-import com.google.gerrit.common.data.ChangeDetail;
-import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.common.data.SubmitRecord;
-import com.google.gerrit.common.data.SubmitTypeRecord;
-import com.google.gerrit.common.data.UiCommandDetail;
-import com.google.gerrit.extensions.common.ListChangesOption;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.reviewdb.client.PatchSetInfo.ParentInfo;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.client.UserIdentity;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-
-import java.util.ArrayList;
-import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-
-public class ChangeDetailCache extends ListenableValue<ChangeDetail> {
-  public static class NewGerritCallback extends
-      com.google.gerrit.client.rpc.GerritCallback<ChangeInfo> {
-    @Override
-    public void onSuccess(ChangeInfo detail) {
-      setChangeDetail(reverse(detail));
-    }
-  }
-
-  public static class IgnoreErrorCallback implements AsyncCallback<ChangeDetail> {
-    @Override
-    public void onSuccess(ChangeDetail info) {
-      setChangeDetail(info);
-    }
-
-    @Override
-    public void onFailure(Throwable caught) {
-    }
-  }
-
-  public static ChangeDetail reverse(ChangeInfo info) {
-    info.revisions().copyKeysIntoChildren("name");
-    RevisionInfo rev = current(info);
-
-    ChangeDetail r = new ChangeDetail();
-    r.setAllowsAnonymous(rev.has_fetch() && rev.fetch().containsKey("http"));
-    r.setCanAbandon(can(info.actions(), "abandon"));
-    r.setCanEditCommitMessage(can(rev.actions(), "message"));
-    r.setCanCherryPick(can(rev.actions(), "cherrypick"));
-    r.setCanPublish(can(rev.actions(), "publish"));
-    r.setCanRebase(can(rev.actions(), "rebase"));
-    r.setCanRestore(can(info.actions(), "restore"));
-    r.setCanRevert(can(info.actions(), "revert"));
-    r.setCanDeleteDraft(can(info.actions(), "/"));
-    r.setCanEditTopicName(can(info.actions(), "topic"));
-    r.setCanSubmit(can(rev.actions(), "submit"));
-    r.setCanEdit(true);
-    r.setChange(toChange(info));
-    r.setStarred(info.starred());
-    r.setPatchSets(toPatchSets(info));
-    r.setMessages(toMessages(info));
-    r.setAccounts(users(info));
-    r.setCurrentPatchSetId(new PatchSet.Id(info.legacy_id(), rev._number()));
-    r.setCurrentPatchSetDetail(toPatchSetDetail(info));
-    r.setSubmitRecords(new ArrayList<SubmitRecord>());
-
-    // Obtained later in ChangeScreen.
-    r.setSubmitTypeRecord(new SubmitTypeRecord());
-    r.getSubmitTypeRecord().status = SubmitTypeRecord.Status.RULE_ERROR;
-    r.setPatchSetsWithDraftComments(new HashSet<PatchSet.Id>());
-    r.setDependsOn(new ArrayList<com.google.gerrit.common.data.ChangeInfo>());
-    r.setNeededBy(new ArrayList<com.google.gerrit.common.data.ChangeInfo>());
-    return r;
-  }
-
-  private static PatchSetDetail toPatchSetDetail(ChangeInfo info) {
-    RevisionInfo rev = current(info);
-    PatchSetDetail p = new PatchSetDetail();
-    p.setPatchSet(toPatchSet(info, rev));
-    p.setProject(info.project_name_key());
-    p.setInfo(new PatchSetInfo(p.getPatchSet().getId()));
-    p.getInfo().setRevId(rev.name());
-    p.getInfo().setParents(new ArrayList<ParentInfo>());
-    p.getInfo().setAuthor(toUser(rev.commit().author()));
-    p.getInfo().setCommitter(toUser(rev.commit().committer()));
-    p.getInfo().setSubject(rev.commit().subject());
-    p.getInfo().setMessage(rev.commit().message());
-    if (rev.commit().parents() != null) {
-      for (CommitInfo c : Natives.asList(rev.commit().parents())) {
-        p.getInfo().getParents().add(new ParentInfo(
-            new RevId(c.commit()),
-            c.subject()));
-      }
-    }
-    p.setPatches(new ArrayList<Patch>());
-    p.setCommands(new ArrayList<UiCommandDetail>());
-
-    rev.files();
-    return p;
-  }
-
-  private static UserIdentity toUser(GitPerson p) {
-    UserIdentity u = new UserIdentity();
-    u.setName(p.name());
-    u.setEmail(p.email());
-    u.setDate(p.date());
-    return u;
-  }
-
-  public static AccountInfoCache users(ChangeInfo info) {
-    Map<Integer, AccountInfo> r = new HashMap<>();
-    add(r, info.owner());
-    if (info.messages() != null) {
-      for (MessageInfo m : Natives.asList(info.messages())) {
-        add(r, m.author());
-      }
-    }
-    return new AccountInfoCache(r.values());
-  }
-
-  private static void add(Map<Integer, AccountInfo> r,
-      com.google.gerrit.client.account.AccountInfo user) {
-    if (user != null && !r.containsKey(user._account_id())) {
-      AccountInfo a = new AccountInfo(new Account.Id(user._account_id()));
-      a.setPreferredEmail(user.email());
-      a.setFullName(user.name());
-      r.put(user._account_id(), a);
-    }
-  }
-
-  private static boolean can(NativeMap<ActionInfo> m, String n) {
-    return m != null && m.containsKey(n) && m.get(n).enabled();
-  }
-
-  private static List<ChangeMessage> toMessages(ChangeInfo info) {
-    List<ChangeMessage> msgs = new ArrayList<>();
-    for (MessageInfo m : Natives.asList(info.messages())) {
-      ChangeMessage o = new ChangeMessage(
-          new ChangeMessage.Key(
-              info.legacy_id(),
-              m.date().toString()),
-          m.author() != null
-            ? new Account.Id(m.author()._account_id())
-            : null,
-          m.date(),
-          m._revisionNumber() > 0
-            ? new PatchSet.Id(info.legacy_id(), m._revisionNumber())
-            : null);
-      o.setMessage(m.message());
-      msgs.add(o);
-    }
-    return msgs;
-  }
-
-  private static List<PatchSet> toPatchSets(ChangeInfo info) {
-    JsArray<RevisionInfo> all = info.revisions().values();
-    RevisionInfo.sortRevisionInfoByNumber(all);
-
-    List<PatchSet> r = new ArrayList<>(all.length());
-    for (RevisionInfo rev : Natives.asList(all)) {
-      r.add(toPatchSet(info, rev));
-    }
-    return r;
-  }
-
-  private static PatchSet toPatchSet(ChangeInfo info, RevisionInfo rev) {
-    PatchSet p = new PatchSet(
-        new PatchSet.Id(info.legacy_id(), rev._number()));
-    p.setCreatedOn(rev.commit().committer().date());
-    p.setDraft(rev.draft());
-    p.setRevision(new RevId(rev.name()));
-    return p;
-  }
-
-  public static Change toChange(ChangeInfo info) {
-    RevisionInfo rev = current(info);
-    PatchSetInfo p = new PatchSetInfo(
-      new PatchSet.Id(
-          info.legacy_id(),
-          rev._number()));
-    p.setSubject(info.subject());
-    Change c = new Change(
-        new Change.Key(info.change_id()),
-        info.legacy_id(),
-        new Account.Id(info.owner()._account_id()),
-        new Branch.NameKey(
-            info.project_name_key(),
-            info.branch()),
-        info.created());
-    c.setTopic(info.topic());
-    c.setStatus(info.status());
-    c.setCurrentPatchSet(p);
-    c.setLastUpdatedOn(info.updated());
-    c.setMergeable(info.mergeable());
-    return c;
-  }
-
-  private static RevisionInfo current(ChangeInfo info) {
-    RevisionInfo rev = info.revision(info.current_revision());
-    if (rev == null) {
-      JsArray<RevisionInfo> all = info.revisions().values();
-      RevisionInfo.sortRevisionInfoByNumber(all);
-      rev = all.get(all.length() - 1);
-    }
-    return rev;
-  }
-
-  public static void setChangeDetail(ChangeDetail detail) {
-    Change.Id chgId = detail.getChange().getId();
-    ChangeCache.get(chgId).getChangeDetailCache().set(detail);
-    StarredChanges.fireChangeStarEvent(chgId, detail.isStarred());
-  }
-
-  private final Change.Id changeId;
-
-  public ChangeDetailCache(final Change.Id chg) {
-    changeId = chg;
-  }
-
-  public void refresh() {
-    RestApi call = ChangeApi.detail(changeId.get());
-    ChangeList.addOptions(call, EnumSet.of(
-      ListChangesOption.CURRENT_ACTIONS,
-      ListChangesOption.ALL_REVISIONS,
-      ListChangesOption.ALL_COMMITS));
-    call.get(new NewGerritCallback());
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java
new file mode 100644
index 0000000..a00e329
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java
@@ -0,0 +1,111 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.editor.EditFileInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.HttpCallback;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+/** REST API helpers to remotely edit a change. */
+public class ChangeEditApi {
+  /** Get file (or commit message) contents. */
+  public static void get(PatchSet.Id id, String path,
+      HttpCallback<NativeString> cb) {
+    RestApi api;
+    if (id.get() != 0) {
+      // Read from a published revision, when change edit doesn't
+      // exist for the caller, or is not currently active.
+      api = ChangeApi.revision(id).view("files").id(path).view("content");
+    } else if (Patch.COMMIT_MSG.equals(path)) {
+      api = editMessage(id.getParentKey().get());
+    } else {
+      api = editFile(id.getParentKey().get(), path);
+    }
+    api.get(cb);
+  }
+
+  /** Get meta info for change edit. */
+  public static void getMeta(PatchSet.Id id, String path,
+      AsyncCallback<EditFileInfo> cb) {
+    if (id.get() != 0) {
+      throw new IllegalStateException("only supported for edits");
+    }
+    editFile(id.getParentKey().get(), path).view("meta").get(cb);
+  }
+
+  /** Put message into a change edit. */
+  public static void putMessage(int id, String m, GerritCallback<VoidResult> cb) {
+    editMessage(id).put(m, cb);
+  }
+
+  /** Put contents into a file or commit message in a change edit. */
+  public static void put(int id, String path, String content,
+      GerritCallback<VoidResult> cb) {
+    if (Patch.COMMIT_MSG.equals(path)) {
+      putMessage(id, content, cb);
+    } else {
+      editFile(id, path).put(content, cb);
+    }
+  }
+
+  /** Delete a file in the pending edit. */
+  public static void delete(int id, String path, AsyncCallback<VoidResult> cb) {
+    editFile(id, path).delete(cb);
+  }
+
+  /** Rename a file in the pending edit. */
+  public static void rename(int id, String path, String newPath,
+      AsyncCallback<VoidResult> cb) {
+    Input in = Input.create();
+    in.old_path(path);
+    in.new_path(newPath);
+    ChangeApi.edit(id).post(in, cb);
+  }
+
+  /** Restore (undo delete/modify) a file in the pending edit. */
+  public static void restore(int id, String path, AsyncCallback<VoidResult> cb) {
+    Input in = Input.create();
+    in.restore_path(path);
+    ChangeApi.edit(id).post(in, cb);
+  }
+
+  private static RestApi editMessage(int id) {
+    return ChangeApi.change(id).view("edit:message");
+  }
+
+  private static RestApi editFile(int id, String path) {
+    return ChangeApi.edit(id).id(path);
+  }
+
+  private static class Input extends JavaScriptObject {
+    static Input create() {
+      return createObject().cast();
+    }
+
+    final native void restore_path(String p) /*-{ this.restore_path=p }-*/;
+    final native void old_path(String p) /*-{ this.old_path=p }-*/;
+    final native void new_path(String p) /*-{ this.new_path=p }-*/;
+
+    protected Input() {
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
index 0c1815f..fff792f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
@@ -61,6 +62,12 @@
     return ts;
   }
 
+  public final boolean hasEditBasedOnCurrentPatchSet() {
+    JsArray<RevisionInfo> revList = revisions().values();
+    RevisionInfo.sortRevisionInfoByNumber(revList);
+    return revList.get(revList.length() - 1).is_edit();
+  }
+
   private final native Timestamp _get_cts() /*-{ return this._cts; }-*/;
   private final native void _set_cts(Timestamp ts) /*-{ this._cts = ts; }-*/;
 
@@ -85,7 +92,7 @@
   public final native String branch() /*-{ return this.branch; }-*/;
   public final native String topic() /*-{ return this.topic; }-*/;
   public final native String change_id() /*-{ return this.change_id; }-*/;
-  public final native boolean mergeable() /*-{ return this.mergeable; }-*/;
+  public final native boolean mergeable() /*-{ return this.mergeable || false; }-*/;
   public final native int insertions() /*-{ return this.insertions; }-*/;
   public final native int deletions() /*-{ return this.deletions; }-*/;
   private final native String statusRaw() /*-{ return this.status; }-*/;
@@ -95,13 +102,17 @@
   private final native String updatedRaw() /*-{ return this.updated; }-*/;
   public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
   public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
-  public final native String _sortkey() /*-{ return this._sortkey; }-*/;
   public final native NativeMap<LabelInfo> all_labels() /*-{ return this.labels; }-*/;
   public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/;
   public final native String current_revision() /*-{ return this.current_revision; }-*/;
+  public final native void set_current_revision(String r) /*-{ this.current_revision = r; }-*/;
   public final native NativeMap<RevisionInfo> revisions() /*-{ return this.revisions; }-*/;
   public final native RevisionInfo revision(String n) /*-{ return this.revisions[n]; }-*/;
   public final native JsArray<MessageInfo> messages() /*-{ return this.messages; }-*/;
+  public final native void set_edit(EditInfo edit) /*-{ this.edit = edit; }-*/;
+  public final native EditInfo edit() /*-{ return this.edit; }-*/;
+  public final native boolean has_edit() /*-{ return this.hasOwnProperty('edit') }-*/;
+  public final native JsArrayString hashtags() /*-{ return this.hashtags; }-*/;
 
   public final native boolean has_permitted_labels()
   /*-{ return this.hasOwnProperty('permitted_labels') }-*/;
@@ -204,13 +215,45 @@
     }
   }
 
+  public static class EditInfo extends JavaScriptObject {
+    public final native String name() /*-{ return this.name; }-*/;
+    public final native String set_name(String n) /*-{ this.name = n; }-*/;
+    public final native String base_revision() /*-{ return this.base_revision; }-*/;
+    public final native CommitInfo commit() /*-{ return this.commit; }-*/;
+
+    public final native boolean has_actions() /*-{ return this.hasOwnProperty('actions') }-*/;
+    public final native NativeMap<ActionInfo> actions() /*-{ return this.actions; }-*/;
+
+    public final native boolean has_fetch() /*-{ return this.hasOwnProperty('fetch') }-*/;
+    public final native NativeMap<FetchInfo> fetch() /*-{ return this.fetch; }-*/;
+
+    public final native boolean has_files() /*-{ return this.hasOwnProperty('files') }-*/;
+    public final native NativeMap<FileInfo> files() /*-{ return this.files; }-*/;
+
+    protected EditInfo() {
+    }
+  }
+
   public static class RevisionInfo extends JavaScriptObject {
+    public static RevisionInfo fromEdit(EditInfo edit) {
+      RevisionInfo revisionInfo = createObject().cast();
+      revisionInfo.takeFromEdit(edit);
+      return revisionInfo;
+    }
+    private final native void takeFromEdit(EditInfo edit) /*-{
+      this._number = 0;
+      this.name = edit.name;
+      this.commit = edit.commit;
+      this.edit_base = edit.base_revision;
+    }-*/;
     public final native int _number() /*-{ return this._number; }-*/;
     public final native String name() /*-{ return this.name; }-*/;
     public final native boolean draft() /*-{ return this.draft || false; }-*/;
     public final native boolean has_draft_comments() /*-{ return this.has_draft_comments || false; }-*/;
+    public final native boolean is_edit() /*-{ return this._number == 0; }-*/;
     public final native CommitInfo commit() /*-{ return this.commit; }-*/;
     public final native void set_commit(CommitInfo c) /*-{ this.commit = c; }-*/;
+    public final native String edit_base() /*-{ return this.edit_base; }-*/;
 
     public final native boolean has_files() /*-{ return this.hasOwnProperty('files') }-*/;
     public final native NativeMap<FileInfo> files() /*-{ return this.files; }-*/;
@@ -220,17 +263,45 @@
 
     public final native boolean has_fetch() /*-{ return this.hasOwnProperty('fetch') }-*/;
     public final native NativeMap<FetchInfo> fetch() /*-{ return this.fetch; }-*/;
-    public final native JsArray<WebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
 
     public static void sortRevisionInfoByNumber(JsArray<RevisionInfo> list) {
+      final int editParent = findEditParent(list);
       Collections.sort(Natives.asList(list), new Comparator<RevisionInfo>() {
         @Override
         public int compare(RevisionInfo a, RevisionInfo b) {
-          return a._number() - b._number();
+          return num(a) - num(b);
+        }
+
+        private int num(RevisionInfo r) {
+          return !r.is_edit() ? 2 * (r._number() - 1) + 1 : 2 * editParent;
         }
       });
     }
 
+    public static int findEditParent(JsArray<RevisionInfo> list) {
+      for (int i = 0; i < list.length(); i++) {
+        // edit under revisions?
+        RevisionInfo editInfo = list.get(i);
+        if (editInfo.is_edit()) {
+          String parentRevision = editInfo.edit_base();
+          // find parent
+          for (int j = 0; j < list.length(); j++) {
+            RevisionInfo parentInfo = list.get(j);
+            String name = parentInfo.name();
+            if (name.equals(parentRevision)) {
+              // found parent pacth set number
+              return parentInfo._number();
+            }
+          }
+        }
+      }
+      return -1;
+    }
+
+    public final String id() {
+      return PatchSet.Id.toId(_number());
+    }
+
     protected RevisionInfo () {
     }
   }
@@ -252,6 +323,7 @@
     public final native GitPerson committer() /*-{ return this.committer; }-*/;
     public final native String subject() /*-{ return this.subject; }-*/;
     public final native String message() /*-{ return this.message; }-*/;
+    public final native JsArray<WebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
 
     protected CommitInfo() {
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
deleted file mode 100644
index 8fe7d56..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
+++ /dev/null
@@ -1,223 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import static com.google.gerrit.client.FormatUtil.mediumFormat;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountLinkPanel;
-import com.google.gerrit.client.ui.BranchLink;
-import com.google.gerrit.client.ui.CommentedActionDialog;
-import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.client.ui.ProjectSearchLink;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.AccountInfoCache;
-import com.google.gerrit.common.data.ChangeDetail;
-import com.google.gerrit.common.data.SubmitTypeRecord;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.TextBox;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.clippy.client.CopyableLabel;
-
-public class ChangeInfoBlock extends Composite {
-  private static final int R_CHANGE_ID = 0;
-  private static final int R_OWNER = 1;
-  private static final int R_PROJECT = 2;
-  private static final int R_BRANCH = 3;
-  private static final int R_TOPIC = 4;
-  private static final int R_UPLOADED = 5;
-  private static final int R_UPDATED = 6;
-  private static final int R_SUBMIT_TYPE = 7;
-  private static final int R_STATUS = 8;
-  private static final int R_MERGE_TEST = 9;
-  private static final int R_CNT = 10;
-
-  private final Grid table;
-
-  public ChangeInfoBlock() {
-    table = new Grid(R_CNT, 2);
-    table.setStyleName(Gerrit.RESOURCES.css().infoBlock());
-    table.addStyleName(Gerrit.RESOURCES.css().changeInfoBlock());
-
-    initRow(R_CHANGE_ID, "Change-Id: ");
-    initRow(R_OWNER, Util.C.changeInfoBlockOwner());
-    initRow(R_PROJECT, Util.C.changeInfoBlockProject());
-    initRow(R_BRANCH, Util.C.changeInfoBlockBranch());
-    initRow(R_TOPIC, Util.C.changeInfoBlockTopic());
-    initRow(R_UPLOADED, Util.C.changeInfoBlockUploaded());
-    initRow(R_UPDATED, Util.C.changeInfoBlockUpdated());
-    initRow(R_STATUS, Util.C.changeInfoBlockStatus());
-    initRow(R_SUBMIT_TYPE, Util.C.changeInfoBlockSubmitType());
-    initRow(R_MERGE_TEST, Util.C.changeInfoBlockCanMerge());
-
-    final CellFormatter fmt = table.getCellFormatter();
-    fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
-    fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
-    fmt.addStyleName(R_CHANGE_ID, 1, Gerrit.RESOURCES.css().changeid());
-    fmt.addStyleName(R_CNT - 2, 0, Gerrit.RESOURCES.css().bottomheader());
-
-    initWidget(table);
-  }
-
-  private void initRow(final int row, final String name) {
-    table.setText(row, 0, name);
-    table.getCellFormatter().addStyleName(row, 0, Gerrit.RESOURCES.css().header());
-  }
-
-  public void display(final ChangeDetail changeDetail,
-      final AccountInfoCache acc, SubmitTypeRecord submitTypeRecord) {
-    final Change chg = changeDetail.getChange();
-    final Branch.NameKey dst = chg.getDest();
-
-    CopyableLabel changeIdLabel =
-        new CopyableLabel("Change-Id: " + chg.getKey().get());
-    changeIdLabel.setPreviewText(chg.getKey().get());
-    table.setWidget(R_CHANGE_ID, 1, changeIdLabel);
-
-    table.setWidget(R_OWNER, 1, AccountLinkPanel.link(acc, chg.getOwner()));
-
-    final FlowPanel p = new FlowPanel();
-    p.add(new ProjectSearchLink(chg.getProject()));
-    p.add(new InlineHyperlink(chg.getProject().get(),
-        PageLinks.toProject(chg.getProject())));
-    table.setWidget(R_PROJECT, 1, p);
-
-    table.setWidget(R_BRANCH, 1, new BranchLink(dst.getShortName(), chg
-        .getProject(), chg.getStatus(), dst.get(), null));
-    table.setWidget(R_TOPIC, 1, topic(changeDetail));
-    table.setText(R_UPLOADED, 1, mediumFormat(chg.getCreatedOn()));
-    table.setText(R_UPDATED, 1, mediumFormat(chg.getLastUpdatedOn()));
-    table.setText(R_STATUS, 1, Util.toLongString(chg.getStatus()));
-    String submitType;
-    if (submitTypeRecord.status == SubmitTypeRecord.Status.OK) {
-      submitType = com.google.gerrit.client.admin.Util
-              .toLongString(submitTypeRecord.type);
-    } else {
-      submitType = submitTypeRecord.status.name();
-    }
-    table.setText(R_SUBMIT_TYPE, 1, submitType);
-    final Change.Status status = chg.getStatus();
-    if (Gerrit.getConfig().getNewFeatures()
-        && (status.equals(Change.Status.NEW) || status.equals(Change.Status.DRAFT))) {
-      table.getRowFormatter().setVisible(R_MERGE_TEST, true);
-      table.setText(R_MERGE_TEST, 1, chg.isMergeable() ? Util.C
-          .changeInfoBlockCanMergeYes() : Util.C.changeInfoBlockCanMergeNo());
-    } else {
-      table.getRowFormatter().setVisible(R_MERGE_TEST, false);
-    }
-
-    if (status.isClosed()) {
-      table.getCellFormatter().addStyleName(R_STATUS, 1, Gerrit.RESOURCES.css().closedstate());
-      table.getRowFormatter().setVisible(R_SUBMIT_TYPE, false);
-    } else {
-      table.getCellFormatter().removeStyleName(R_STATUS, 1, Gerrit.RESOURCES.css().closedstate());
-      table.getRowFormatter().setVisible(R_SUBMIT_TYPE, true);
-    }
-  }
-
-  public Widget topic(final ChangeDetail changeDetail) {
-    final Change chg = changeDetail.getChange();
-    final Branch.NameKey dst = chg.getDest();
-
-    FlowPanel fp = new FlowPanel();
-    fp.addStyleName(Gerrit.RESOURCES.css().changeInfoTopicPanel());
-    fp.add(new BranchLink(chg.getTopic(), chg.getProject(), chg.getStatus(),
-           dst.get(), chg.getTopic()));
-
-    if (changeDetail.canEditTopicName()) {
-      final Image edit = new Image(Gerrit.RESOURCES.edit());
-      edit.addStyleName(Gerrit.RESOURCES.css().link());
-      edit.setTitle(Util.C.changeInfoBlockTopicAlterTopicToolTip());
-      edit.addClickHandler(new  ClickHandler() {
-        @Override
-        public void onClick(final ClickEvent event) {
-          new AlterTopicDialog(chg).center();
-        }
-      });
-      fp.add(edit);
-    }
-
-    return fp;
-  }
-
-  private class AlterTopicDialog extends CommentedActionDialog<ChangeDetail>
-      implements KeyPressHandler {
-    TextBox newTopic;
-    Change change;
-
-    AlterTopicDialog(Change chg) {
-      super(Util.C.alterTopicTitle(), Util.C.headingAlterTopicMessage(),
-          new ChangeDetailCache.IgnoreErrorCallback());
-      change = chg;
-      message.setVisible(false);
-
-      newTopic = new TextBox();
-      newTopic.addKeyPressHandler(this);
-      setFocusOn(newTopic);
-      panel.insert(newTopic, 0);
-      panel.insert(new InlineLabel(Util.C.alterTopicLabel()), 0);
-    }
-
-    @Override
-    protected void onLoad() {
-      super.onLoad();
-      newTopic.setText(change.getTopic());
-    }
-
-    private void doTopicEdit() {
-      String topic = newTopic.getText();
-      ChangeApi.topic(change.getId().get(), topic,
-        new GerritCallback<String>() {
-        @Override
-        public void onSuccess(String result) {
-          sent = true;
-          Gerrit.display(PageLinks.toChange(change.getId()));
-          hide();
-        }
-
-        @Override
-        public void onFailure(final Throwable caught) {
-          enableButtons(true);
-          super.onFailure(caught);
-        }});
-    }
-
-    @Override
-    public void onSend() {
-      doTopicEdit();
-    }
-
-    @Override
-    public void onKeyPress(KeyPressEvent event) {
-      if (event.getSource() == newTopic
-          && event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-        doTopicEdit();
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
index 63f5e29..b1866dd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.client.changes;
 
 import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtorm.client.KeyUtil;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index d435720..76e3211 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
@@ -37,6 +37,7 @@
   String patchTableSize_LongModify(int insertions, int deletions);
   String patchTableSize_Lines(@PluralCount int insertions);
 
+  String removeHashtag(String name);
   String removeReviewer(String fullName);
   String messageWrittenOn(String date);
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
index fa942fd..7069c4a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -20,6 +20,7 @@
 patchTableSize_LongModify = {0} inserted, {1} deleted
 patchTableSize_Lines = {0} lines
 
+removeHashtag = Remove hashtag {0}
 removeReviewer = Remove reviewer {0}
 messageWrittenOn = on {0}
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeResources.java
deleted file mode 100644
index 892143c..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeResources.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import com.google.gwt.resources.client.ClientBundle;
-import com.google.gwt.resources.client.ImageResource;
-
-public interface ChangeResources extends ClientBundle {
-  @Source("removeReviewerNormal.png")
-  public ImageResource removeReviewerNormal();
-
-  @Source("removeReviewerPressed.png")
-  public ImageResource removeReviewerPressed();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
deleted file mode 100644
index 5a6e96c..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
+++ /dev/null
@@ -1,764 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.account.AccountInfo;
-import com.google.gerrit.client.change.RelatedChanges;
-import com.google.gerrit.client.change.RelatedChanges.ChangeAndCommit;
-import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
-import com.google.gerrit.client.diff.DiffApi;
-import com.google.gerrit.client.diff.FileInfo;
-import com.google.gerrit.client.projects.ConfigInfoCache;
-import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.client.ui.CommentPanel;
-import com.google.gerrit.client.ui.ComplexDisclosurePanel;
-import com.google.gerrit.client.ui.ExpandAllCommand;
-import com.google.gerrit.client.ui.LinkMenuBar;
-import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
-import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.data.AccountInfoCache;
-import com.google.gerrit.common.data.ChangeDetail;
-import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.common.data.SubmitTypeRecord;
-import com.google.gerrit.extensions.common.ListChangesOption;
-import com.google.gerrit.extensions.common.SubmitType;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Change.Status;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.Patch.ChangeType;
-import com.google.gerrit.reviewdb.client.Patch.PatchType;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.event.dom.client.ChangeEvent;
-import com.google.gwt.event.dom.client.ChangeHandler;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.i18n.client.LocaleInfo;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.DisclosurePanel;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwt.user.client.ui.Panel;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-import com.google.gwtexpui.globalkey.client.KeyCommandSet;
-
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-
-public class ChangeScreen extends Screen
-    implements ValueChangeHandler<ChangeDetail> {
-  private final Change.Id changeId;
-  private final PatchSet.Id openPatchSetId;
-  private ChangeDetailCache detailCache;
-  private com.google.gerrit.client.changes.ChangeInfo changeInfo;
-
-  private ChangeDescriptionBlock descriptionBlock;
-  private ApprovalTable approvals;
-
-  private IncludedInTable includedInTable;
-  private DisclosurePanel includedInPanel;
-  private ComplexDisclosurePanel dependenciesPanel;
-  private ChangeTable dependencies;
-  private ChangeTable.Section dependsOn;
-  private ChangeTable.Section neededBy;
-
-  private PatchSetsBlock patchSetsBlock;
-
-  private Panel comments;
-  private CommentLinkProcessor commentLinkProcessor;
-
-  private KeyCommandSet keysNavigation;
-  private KeyCommandSet keysAction;
-  private HandlerRegistration regNavigation;
-  private HandlerRegistration regAction;
-  private HandlerRegistration regDetailCache;
-
-  private Grid patchesGrid;
-  private ListBox patchesList;
-
-  /**
-   * The change id for which the old version history is valid.
-   */
-  private static Change.Id currentChangeId;
-
-  /**
-   * Which patch set id is the diff base.
-   */
-  private static PatchSet.Id diffBaseId;
-
-  public ChangeScreen(final Change.Id toShow) {
-    changeId = toShow;
-    openPatchSetId = null;
-  }
-
-  public ChangeScreen(final PatchSet.Id toShow) {
-    changeId = toShow.getParentKey();
-    openPatchSetId = toShow;
-  }
-
-  public ChangeScreen(final ChangeInfo c) {
-    this(c.getId());
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    detailCache.refresh();
-  }
-
-  @Override
-  protected void onUnload() {
-    if (regNavigation != null) {
-      regNavigation.removeHandler();
-      regNavigation = null;
-    }
-    if (regAction != null) {
-      regAction.removeHandler();
-      regAction = null;
-    }
-    if (regDetailCache != null) {
-      regDetailCache.removeHandler();
-      regDetailCache = null;
-    }
-    super.onUnload();
-  }
-
-  @Override
-  public void registerKeys() {
-    super.registerKeys();
-    regNavigation = GlobalKey.add(this, keysNavigation);
-    regAction = GlobalKey.add(this, keysAction);
-    if (openPatchSetId != null) {
-      patchSetsBlock.activate(openPatchSetId);
-    }
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-
-    ChangeCache cache = ChangeCache.get(changeId);
-
-    detailCache = cache.getChangeDetailCache();
-    regDetailCache = detailCache.addValueChangeHandler(this);
-
-    addStyleName(Gerrit.RESOURCES.css().changeScreen());
-    addStyleName(Gerrit.RESOURCES.css().screenNoHeader());
-
-    keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
-    keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
-    keysNavigation.add(new UpToListKeyCommand(0, 'u', Util.C.upToChangeList()));
-    keysNavigation.add(new ExpandCollapseDependencySectionKeyCommand(0, 'd', Util.C.expandCollapseDependencies()));
-
-    if (Gerrit.isSignedIn()) {
-      keysAction.add(new PublishCommentsKeyCommand(0, 'r', Util.C
-          .keyPublishComments()));
-    }
-
-    descriptionBlock = new ChangeDescriptionBlock(keysAction);
-    add(descriptionBlock);
-
-    approvals = new ApprovalTable();
-    add(approvals);
-
-    includedInPanel = new DisclosurePanel(Util.C.changeScreenIncludedIn());
-    includedInTable = new IncludedInTable(changeId);
-
-    includedInPanel.setContent(includedInTable);
-    add(includedInPanel);
-
-    dependencies = new ChangeTable() {
-      {
-        table.setWidth("auto");
-      }
-    };
-    dependsOn = new ChangeTable.Section(Util.C.changeScreenDependsOn());
-    dependsOn.setChangeRowFormatter(new ChangeTable.ChangeRowFormatter() {
-      @Override
-      public String getRowStyle(ChangeInfo c) {
-        if (! c.isLatest() || Change.Status.ABANDONED.equals(c.getStatus())) {
-          return Gerrit.RESOURCES.css().outdated();
-        }
-        return null;
-      }
-
-      @Override
-      public String getDisplayText(final ChangeInfo c, final String displayText) {
-        if (! c.isLatest()) {
-          return displayText + " [OUTDATED]";
-        }
-        return displayText;
-      }
-    });
-    neededBy = new ChangeTable.Section(Util.C.changeScreenNeededBy());
-    dependencies.addSection(dependsOn);
-    dependencies.addSection(neededBy);
-
-    dependenciesPanel = new ComplexDisclosurePanel(
-        Util.C.changeScreenDependencies(), false);
-    dependenciesPanel.setContent(dependencies);
-    add(dependenciesPanel);
-
-    patchesList = new ListBox();
-    patchesList.addChangeHandler(new ChangeHandler() {
-      @Override
-      public void onChange(ChangeEvent event) {
-        final int index = patchesList.getSelectedIndex();
-        final String selectedPatchSet = patchesList.getValue(index);
-        if (index == 0) {
-          diffBaseId = null;
-        } else {
-          diffBaseId = PatchSet.Id.parse(selectedPatchSet);
-        }
-        if (patchSetsBlock != null) {
-          patchSetsBlock.refresh(diffBaseId);
-        }
-      }
-    });
-
-    patchesGrid = new Grid(1, 2);
-    patchesGrid.setStyleName(Gerrit.RESOURCES.css().selectPatchSetOldVersion());
-    patchesGrid.setText(0, 0, Util.C.referenceVersion());
-    patchesGrid.setWidget(0, 1, patchesList);
-    add(patchesGrid);
-
-    patchSetsBlock = new PatchSetsBlock();
-    add(patchSetsBlock);
-
-    comments = new FlowPanel();
-    comments.setStyleName(Gerrit.RESOURCES.css().changeComments());
-    add(comments);
-  }
-
-  private void displayTitle(final Change.Key changeId, final String subject) {
-    final StringBuilder titleBuf = new StringBuilder();
-    if (LocaleInfo.getCurrentLocale().isRTL()) {
-      if (subject != null) {
-        titleBuf.append(subject);
-        titleBuf.append(" :");
-      }
-      titleBuf.append(Util.M.changeScreenTitleId(changeId.abbreviate()));
-    } else {
-      titleBuf.append(Util.M.changeScreenTitleId(changeId.abbreviate()));
-      if (subject != null) {
-        titleBuf.append(": ");
-        titleBuf.append(subject);
-      }
-    }
-    setPageTitle(titleBuf.toString());
-    setHeaderVisible(false);
-  }
-
-  @Override
-  public void onValueChange(final ValueChangeEvent<ChangeDetail> event) {
-    if (isAttached() && isLastValueChangeHandler()) {
-      // Until this screen is fully migrated to the new API, these calls must
-      // happen sequentially after the ChangeDetail lookup, because we can't
-      // start an async get at the source of every call that might trigger a
-      // value change.
-      CallbackGroup cbs1 = new CallbackGroup();
-      final CallbackGroup cbs2 = new CallbackGroup();
-      final PatchSet.Id psId = event.getValue().getCurrentPatchSet().getId();
-      final Map<String, Patch> patches = new HashMap<>();
-      String revId =
-          event.getValue().getCurrentPatchSetDetail().getInfo().getRevId();
-
-      if (event.getValue().getChange().getStatus().isOpen()) {
-        ChangeApi.revision(changeId.get(), "current")
-          .view("submit_type")
-          .get(cbs1.add(new GerritCallback<NativeString>() {
-            @Override
-            public void onSuccess(NativeString result) {
-              event.getValue().setSubmitTypeRecord(SubmitTypeRecord.OK(
-                  SubmitType.valueOf(result.asString())));
-            }
-            public void onFailure(Throwable caught) {}
-          }));
-      }
-      if (Gerrit.isSignedIn()) {
-        ChangeApi.revision(changeId.get(), "" + psId.get())
-          .view("related")
-          .get(cbs1.add(new AsyncCallback<RelatedChanges.RelatedInfo>() {
-              @Override
-              public void onSuccess(RelatedChanges.RelatedInfo info) {
-                if (info.changes() != null) {
-                  dependsOn(info);
-                  neededBy(info);
-                }
-              }
-
-              private void dependsOn(RelatedChanges.RelatedInfo info) {
-                ChangeAndCommit self = null;
-                Map<String, ChangeAndCommit> m = new HashMap<>();
-                for (int i = 0; i < info.changes().length(); i++) {
-                  ChangeAndCommit c = info.changes().get(i);
-                  if (changeId.equals(c.legacy_id())) {
-                    self = c;
-                  }
-                  if (c.commit() != null && c.commit().commit() != null) {
-                    m.put(c.commit().commit(), c);
-                  }
-                }
-                if (self != null && self.commit() != null
-                    && self.commit().parents() != null) {
-                  List<ChangeInfo> d = new ArrayList<>();
-                  for (CommitInfo p : Natives.asList(self.commit().parents())) {
-                    ChangeAndCommit pc = m.get(p.commit());
-                    if (pc != null && pc.has_change_number()) {
-                      ChangeInfo i = new ChangeInfo();
-                      load(pc, i);
-                      d.add(i);
-                    }
-                  }
-                  event.getValue().setDependsOn(d);
-                }
-              }
-
-              private void neededBy(RelatedChanges.RelatedInfo info) {
-                Set<String> mine = new HashSet<>();
-                for (PatchSet ps : event.getValue().getPatchSets()) {
-                  mine.add(ps.getRevision().get());
-                }
-
-                List<ChangeInfo> n = new ArrayList<>();
-                for (int i = 0; i < info.changes().length(); i++) {
-                  ChangeAndCommit c = info.changes().get(i);
-                  if (c.has_change_number()
-                      && c.commit() != null
-                      && c.commit().parents() != null) {
-                    for (int j = 0; j < c.commit().parents().length(); j++) {
-                      CommitInfo p = c.commit().parents().get(j);
-                      if (mine.contains(p.commit())) {
-                        ChangeInfo u = new ChangeInfo();
-                        load(c, u);
-                        n.add(u);
-                        break;
-                      }
-                    }
-                  }
-                }
-                event.getValue().setNeededBy(n);
-              }
-
-              private void load(final ChangeAndCommit pc, final ChangeInfo i) {
-                RestApi call = ChangeApi.change(pc.legacy_id().get());
-                ChangeList.addOptions(call, EnumSet.of(
-                  ListChangesOption.DETAILED_ACCOUNTS,
-                  ListChangesOption.CURRENT_REVISION));
-                call.get(cbs2.add(new AsyncCallback<
-                    com.google.gerrit.client.changes.ChangeInfo>() {
-                  public void onFailure(Throwable caught) {}
-                  public void onSuccess(
-                      com.google.gerrit.client.changes.ChangeInfo result) {
-                    i.set(ChangeDetailCache.toChange(result),
-                        pc.patch_set_id());
-                    i.setStarred(result.starred());
-                    event.getValue().getAccounts()
-                        .merge(ChangeDetailCache.users(result));
-                  }}));
-              }
-              public void onFailure(Throwable caught) {}
-            }));
-        ChangeApi.revision(changeId.get(), revId)
-          .view("files")
-          .addParameterTrue("reviewed")
-          .get(cbs1.add(new AsyncCallback<JsArrayString>() {
-              @Override
-              public void onSuccess(JsArrayString result) {
-                for(int i = 0; i < result.length(); i++) {
-                  String path = result.get(i);
-                  Patch p = patches.get(path);
-                  if (p == null) {
-                    p = new Patch(new Patch.Key(psId, path));
-                    patches.put(path, p);
-                  }
-                  p.setReviewedByCurrentUser(true);
-                }
-              }
-              public void onFailure(Throwable caught) {}
-            }));
-        final Set<PatchSet.Id> withDrafts = new HashSet<>();
-        event.getValue().setPatchSetsWithDraftComments(withDrafts);
-        for (PatchSet ps : event.getValue().getPatchSets()) {
-          if (!ps.getId().equals(psId)) {
-            final PatchSet.Id id = ps.getId();
-            ChangeApi.revision(changeId.get(), "" + id.get())
-              .view("drafts")
-              .get(cbs1.add(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
-                @Override
-                public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
-                  if (!result.isEmpty()) {
-                    withDrafts.add(id);
-                  }
-                }
-                public void onFailure(Throwable caught) {}
-              }));
-          }
-        }
-        ChangeApi.revision(changeId.get(), "" + psId.get())
-          .view("drafts")
-          .get(cbs1.add(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
-            @Override
-            public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
-              for (String path : result.keySet()) {
-                Patch p = patches.get(path);
-                if (p == null) {
-                  p = new Patch(new Patch.Key(psId, path));
-                  patches.put(path, p);
-                }
-                p.setDraftCount(result.get(path).length());
-              }
-              if (!result.isEmpty()) {
-                withDrafts.add(psId);
-              }
-            }
-            public void onFailure(Throwable caught) {}
-          }));
-      }
-      ChangeApi.revision(changeId.get(), revId)
-        .view("comments")
-        .get(cbs1.add(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
-          @Override
-          public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
-            for (String path : result.keySet()) {
-              Patch p = patches.get(path);
-              if (p == null) {
-                p = new Patch(new Patch.Key(psId, path));
-                patches.put(path, p);
-              }
-              p.setCommentCount(result.get(path).length());
-            }
-          }
-          public void onFailure(Throwable caught) {}
-        }));
-      DiffApi.list(changeId.get(), null, revId,
-          new AsyncCallback<NativeMap<FileInfo>>() {
-            @Override
-            public void onSuccess(NativeMap<FileInfo> result) {
-              JsArray<FileInfo> fileInfos = result.values();
-              FileInfo.sortFileInfoByPath(fileInfos);
-              List<Patch> list = new ArrayList<>(fileInfos.length());
-              for (FileInfo f : Natives.asList(fileInfos)) {
-                Patch p = patches.get(f.path());
-                if (p == null) {
-                  p = new Patch(new Patch.Key(psId, f.path()));
-                  patches.put(f.path(), p);
-                }
-                p.setInsertions(f.lines_inserted());
-                p.setDeletions(f.lines_deleted());
-                p.setPatchType(f.binary() ? PatchType.BINARY : PatchType.UNIFIED);
-                if (f.status() == null) {
-                  p.setChangeType(ChangeType.MODIFIED);
-                } else {
-                  p.setChangeType(ChangeType.forCode(f.status().charAt(0)));
-                }
-                list.add(p);
-              }
-              event.getValue().getCurrentPatchSetDetail().setPatches(list);
-            }
-            public void onFailure(Throwable caught) {}
-      });
-      ConfigInfoCache.get(
-          event.getValue().getChange().getProject(),
-          cbs1.add(new GerritCallback<ConfigInfoCache.Entry>() {
-            @Override
-            public void onSuccess(ConfigInfoCache.Entry result) {
-              commentLinkProcessor = result.getCommentLinkProcessor();
-              setTheme(result.getTheme());
-            }
-
-            @Override
-            public void onFailure(Throwable caught) {
-              // Handled by last callback's onFailure.
-            }
-          }));
-      ChangeApi.detail(changeId.get(), cbs1.addFinal(
-          new GerritCallback<com.google.gerrit.client.changes.ChangeInfo>() {
-            @Override
-            public void onSuccess(
-                com.google.gerrit.client.changes.ChangeInfo result) {
-              changeInfo = result;
-              cbs2.addFinal(new AsyncCallback<Void>() {
-                @Override
-                public void onSuccess(Void result) {
-                  display(event.getValue());
-                }
-                public void onFailure(Throwable caught) {}
-              }).onSuccess(null);
-            }
-          }));
-    }
-  }
-
-  // Find the last attached screen.
-  // When DialogBox is used (i. e. CommentedActionDialog) then the original
-  // ChangeScreen is still in attached state.
-  // Use here the fact, that the handlers (ChangeScreen) are sorted.
-  private boolean isLastValueChangeHandler() {
-    int count = detailCache.getHandlerCount();
-    return count > 0 && detailCache.getHandler(count - 1) == this;
-  }
-
-  private void display(final ChangeDetail detail) {
-    displayTitle(detail.getChange().getKey(), detail.getChange().getSubject());
-    discardDiffBaseIfNotApplicable(detail.getChange().getId());
-
-    if (Status.MERGED == detail.getChange().getStatus()) {
-      includedInPanel.setVisible(true);
-      includedInPanel.addOpenHandler(includedInTable);
-    } else {
-      includedInPanel.setVisible(false);
-    }
-
-    dependencies.setAccountInfoCache(detail.getAccounts());
-
-    descriptionBlock.display(detail,
-        detail.isStarred(),
-        detail.canEditCommitMessage(),
-        detail.getCurrentPatchSetDetail().getInfo(),
-        detail.getAccounts(), detail.getSubmitTypeRecord(),
-        commentLinkProcessor);
-    dependsOn.display(detail.getDependsOn());
-    neededBy.display(detail.getNeededBy());
-    approvals.display(changeInfo);
-
-    patchesList.clear();
-    if (detail.getCurrentPatchSetDetail().getInfo().getParents().size() > 1) {
-      patchesList.addItem(Util.C.autoMerge());
-    } else {
-      patchesList.addItem(Util.C.baseDiffItem());
-    }
-    for (PatchSet pId : detail.getPatchSets()) {
-      patchesList.addItem(Util.M.patchSetHeader(pId.getPatchSetId()), pId
-          .getId().toString());
-    }
-
-    if (diffBaseId != null) {
-      patchesList.setSelectedIndex(diffBaseId.get());
-    }
-
-    patchSetsBlock.display(detail, diffBaseId);
-    addComments(detail);
-
-    // If any dependency change is still open, or is outdated,
-    // or the change is needed by a change that is new or submitted,
-    // show our dependency list.
-    //
-    boolean depsOpen = false;
-    int outdated = 0;
-    if (!detail.getChange().getStatus().isClosed()) {
-      final List<ChangeInfo> dependsOn = detail.getDependsOn();
-      if (dependsOn != null) {
-        for (final ChangeInfo ci : dependsOn) {
-          if (!ci.isLatest()) {
-            depsOpen = true;
-            outdated++;
-          } else if (ci.getStatus() != Change.Status.MERGED) {
-            depsOpen = true;
-          }
-        }
-      }
-    }
-    final List<ChangeInfo> neededBy = detail.getNeededBy();
-    if (neededBy != null) {
-      for (final ChangeInfo ci : neededBy) {
-        if ((ci.getStatus() == Change.Status.NEW) ||
-            (ci.getStatus() == Change.Status.SUBMITTED) ||
-            (ci.getStatus() == Change.Status.DRAFT)) {
-          depsOpen = true;
-        }
-      }
-    }
-
-    dependenciesPanel.setOpen(depsOpen);
-
-    dependenciesPanel.getHeader().clear();
-    if (outdated > 0) {
-      dependenciesPanel.getHeader().add(new InlineLabel(
-        Util.M.outdatedHeader(outdated)));
-    }
-
-    if (!isCurrentView()) {
-      display();
-    }
-    patchSetsBlock.setRegisterKeys(true);
-  }
-
-  private static void discardDiffBaseIfNotApplicable(final Change.Id toShow) {
-    if (currentChangeId != null && !currentChangeId.equals(toShow)) {
-      diffBaseId = null;
-    }
-    currentChangeId = toShow;
-  }
-
-  private void addComments(final ChangeDetail detail) {
-    comments.clear();
-
-    final AccountInfoCache accts = detail.getAccounts();
-    final List<ChangeMessage> msgList = detail.getMessages();
-
-    HorizontalPanel title = new HorizontalPanel();
-    title.setWidth("100%");
-    title.add(new Label(Util.C.changeScreenComments()));
-    if (msgList.size() > 1) {
-      title.add(messagesMenuBar());
-    }
-    title.setStyleName(Gerrit.RESOURCES.css().blockHeader());
-    comments.add(title);
-
-    final long AGE = 7 * 24 * 60 * 60 * 1000L;
-    final Timestamp aged = new Timestamp(System.currentTimeMillis() - AGE);
-
-    CommentVisibilityStrategy commentVisibilityStrategy =
-        CommentVisibilityStrategy.EXPAND_RECENT;
-    if (Gerrit.isSignedIn()) {
-      commentVisibilityStrategy = Gerrit.getUserAccount()
-          .getGeneralPreferences().getCommentVisibilityStrategy();
-    }
-
-    for (int i = 0; i < msgList.size(); i++) {
-      final ChangeMessage msg = msgList.get(i);
-
-      AccountInfo author;
-      if (msg.getAuthor() != null) {
-        author = FormatUtil.asInfo(accts.get(msg.getAuthor()));
-      } else {
-        author = AccountInfo.create(0, Util.C.messageNoAuthor(), null, null);
-      }
-
-      boolean isRecent;
-      if (i == msgList.size() - 1) {
-        isRecent = true;
-      } else {
-        // TODO Instead of opening messages by strict age, do it by "unread"?
-        isRecent = msg.getWrittenOn().after(aged);
-      }
-
-      final CommentPanel cp = new CommentPanel(author, msg.getWrittenOn(),
-          msg.getMessage(), commentLinkProcessor);
-      cp.setRecent(isRecent);
-      cp.addStyleName(Gerrit.RESOURCES.css().commentPanelBorder());
-      if (i == msgList.size() - 1) {
-        cp.addStyleName(Gerrit.RESOURCES.css().commentPanelLast());
-      }
-      boolean isOpen = false;
-      switch (commentVisibilityStrategy) {
-        case COLLAPSE_ALL:
-          break;
-        case EXPAND_ALL:
-          isOpen = true;
-          break;
-        case EXPAND_MOST_RECENT:
-          isOpen = i == msgList.size() - 1;
-          break;
-        case EXPAND_RECENT:
-        default:
-          isOpen = isRecent;
-          break;
-      }
-      cp.setOpen(isOpen);
-      comments.add(cp);
-    }
-
-    final Button b = new Button(Util.C.changeScreenAddComment());
-    b.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(final ClickEvent event) {
-            PatchSet.Id currentPatchSetId = patchSetsBlock.getCurrentPatchSet().getId();
-            Gerrit.display(Dispatcher.toPublish(currentPatchSetId));
-        }
-    });
-    comments.add(b);
-    comments.setVisible(msgList.size() > 0);
-  }
-
-  private LinkMenuBar messagesMenuBar() {
-    final Panel c = comments;
-    final LinkMenuBar menuBar = new LinkMenuBar();
-    menuBar.addItem(Util.C.messageExpandRecent(), new ExpandAllCommand(c, true) {
-      @Override
-      protected void expand(final CommentPanel w) {
-        w.setOpen(w.isRecent());
-      }
-    });
-    menuBar.addItem(Util.C.messageExpandAll(), new ExpandAllCommand(c, true));
-    menuBar.addItem(Util.C.messageCollapseAll(), new ExpandAllCommand(c, false));
-    menuBar.addStyleName(Gerrit.RESOURCES.css().commentPanelMenuBar());
-    return menuBar;
-  }
-
-  public class UpToListKeyCommand extends KeyCommand {
-    public UpToListKeyCommand(int mask, char key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-      Gerrit.displayLastChangeList();
-    }
-  }
-
-  public class ExpandCollapseDependencySectionKeyCommand extends KeyCommand {
-    public ExpandCollapseDependencySectionKeyCommand(int mask, char key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(KeyPressEvent event) {
-      dependenciesPanel.setOpen(!dependenciesPanel.isOpen());
-    }
-  }
-
-  public class PublishCommentsKeyCommand extends NeedsSignInKeyCommand {
-    public PublishCommentsKeyCommand(int mask, char key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-      PatchSet.Id currentPatchSetId = patchSetsBlock.getCurrentPatchSet().getId();
-      Gerrit.display(Dispatcher.toPublish(currentPatchSetId));
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index 4999795..8b71448 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -14,9 +14,12 @@
 
 package com.google.gerrit.client.changes;
 
+import static com.google.gerrit.client.FormatUtil.relativeFormat;
 import static com.google.gerrit.client.FormatUtil.shortFormat;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
 import com.google.gerrit.client.ui.AccountLinkPanel;
 import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.ChangeLink;
@@ -24,54 +27,74 @@
 import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
 import com.google.gerrit.client.ui.ProjectLink;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.AccountInfoCache;
-import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.dom.client.Element;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTMLTable.Cell;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.InlineLabel;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
 
 import java.util.ArrayList;
-import java.util.HashSet;
+import java.util.Collections;
 import java.util.List;
-import java.util.Set;
 
 public class ChangeTable extends NavigationTable<ChangeInfo> {
   private static final int C_STAR = 1;
-  private static final int C_SUBJECT = 2;
-  private static final int C_OWNER = 3;
-  private static final int C_PROJECT = 4;
-  private static final int C_BRANCH = 5;
-  private static final int C_LAST_UPDATE = 6;
-  private static final int COLUMNS = 7;
+  private static final int C_ID = 2;
+  private static final int C_SUBJECT = 3;
+  private static final int C_STATUS = 4;
+  private static final int C_OWNER = 5;
+  private static final int C_PROJECT = 6;
+  private static final int C_BRANCH = 7;
+  private static final int C_LAST_UPDATE = 8;
+  private static final int C_SIZE = 9;
+  private static final int BASE_COLUMNS = 10;
 
   private final List<Section> sections;
-  private AccountInfoCache accountCache = AccountInfoCache.empty();
+  private int columns;
+  private boolean showLegacyId;
+  private List<String> labelNames;
 
   public ChangeTable() {
     super(Util.C.changeItemHelp());
+    columns = BASE_COLUMNS;
+    labelNames = Collections.emptyList();
 
     if (Gerrit.isSignedIn()) {
       keysAction.add(new StarKeyCommand(0, 's', Util.C.changeTableStar()));
+      showLegacyId = Gerrit.getUserAccount()
+          .getGeneralPreferences()
+          .isLegacycidInChangeTable();
     }
 
     sections = new ArrayList<>();
     table.setText(0, C_STAR, "");
+    table.setText(0, C_ID, Util.C.changeTableColumnID());
     table.setText(0, C_SUBJECT, Util.C.changeTableColumnSubject());
+    table.setText(0, C_STATUS, Util.C.changeTableColumnStatus());
     table.setText(0, C_OWNER, Util.C.changeTableColumnOwner());
     table.setText(0, C_PROJECT, Util.C.changeTableColumnProject());
     table.setText(0, C_BRANCH, Util.C.changeTableColumnBranch());
     table.setText(0, C_LAST_UPDATE, Util.C.changeTableColumnLastUpdate());
+    table.setText(0, C_SIZE, Util.C.changeTableColumnSize());
 
     final FlexCellFormatter fmt = table.getFlexCellFormatter();
     fmt.addStyleName(0, C_STAR, Gerrit.RESOURCES.css().iconHeader());
-    for (int i = C_SUBJECT; i < COLUMNS; i++) {
+    for (int i = C_ID; i < columns; i++) {
       fmt.addStyleName(0, i, Gerrit.RESOURCES.css().dataHeader());
     }
+    if (!showLegacyId) {
+      fmt.addStyleName(0, C_ID, Gerrit.RESOURCES.css().dataHeaderHidden());
+    }
 
     table.addClickHandler(new ClickHandler() {
       @Override
@@ -82,6 +105,8 @@
         }
         if (cell.getCellIndex() == C_STAR) {
           // Don't do anything (handled by star itself).
+        } else if (cell.getCellIndex() == C_STATUS) {
+          // Don't do anything.
         } else if (cell.getCellIndex() == C_OWNER) {
           // Don't do anything.
         } else if (getRowItem(cell.getRowIndex()) != null) {
@@ -91,29 +116,23 @@
     });
   }
 
-  protected void onStarClick(final int row) {
-    final ChangeInfo c = getRowItem(row);
-    if (c != null && Gerrit.isSignedIn()) {
-      ((StarredChanges.Icon) table.getWidget(row, C_STAR)).toggleStar();
-    }
-  }
-
   @Override
   protected Object getRowItemKey(final ChangeInfo item) {
-    return item.getId();
+    return item.legacy_id();
   }
 
   @Override
   protected void onOpenRow(final int row) {
     final ChangeInfo c = getRowItem(row);
-    Gerrit.display(PageLinks.toChange(c), new ChangeScreen(c));
+    final Change.Id id = c.legacy_id();
+    Gerrit.display(PageLinks.toChange(id));
   }
 
   private void insertNoneRow(final int row) {
     insertRow(row);
     table.setText(row, 0, Util.C.changeTableNone());
     final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.setColSpan(row, 0, COLUMNS);
+    fmt.setColSpan(row, 0, columns);
     fmt.setStyleName(row, 0, Gerrit.RESOURCES.css().emptySection());
   }
 
@@ -127,88 +146,266 @@
     super.applyDataRowStyle(row);
     final CellFormatter fmt = table.getCellFormatter();
     fmt.addStyleName(row, C_STAR, Gerrit.RESOURCES.css().iconCell());
-    for (int i = C_SUBJECT; i < COLUMNS; i++) {
+    for (int i = C_ID; i < columns; i++) {
       fmt.addStyleName(row, i, Gerrit.RESOURCES.css().dataCell());
     }
+    if (!showLegacyId) {
+      fmt.addStyleName(row, C_ID, Gerrit.RESOURCES.css().dataCellHidden());
+    }
     fmt.addStyleName(row, C_SUBJECT, Gerrit.RESOURCES.css().cSUBJECT());
+    fmt.addStyleName(row, C_STATUS, Gerrit.RESOURCES.css().cSTATUS());
     fmt.addStyleName(row, C_OWNER, Gerrit.RESOURCES.css().cOWNER());
     fmt.addStyleName(row, C_LAST_UPDATE, Gerrit.RESOURCES.css().cLastUpdate());
+    fmt.addStyleName(row, C_SIZE, Gerrit.RESOURCES.css().cSIZE());
+
+    for (int i = C_SIZE + 1; i < columns; i++) {
+      fmt.addStyleName(row, i, Gerrit.RESOURCES.css().cAPPROVAL());
+    }
+  }
+
+  public void updateColumnsForLabels(ChangeList... lists) {
+    labelNames = new ArrayList<>();
+    for (ChangeList list : lists) {
+      for (int i = 0; i < list.length(); i++) {
+        for (String name : list.get(i).labels()) {
+          if (!labelNames.contains(name)) {
+            labelNames.add(name);
+          }
+        }
+      }
+    }
+    Collections.sort(labelNames);
+
+    int baseColumns = BASE_COLUMNS;
+    if (baseColumns + labelNames.size() < columns) {
+      int n = columns - (baseColumns + labelNames.size());
+      for (int row = 0; row < table.getRowCount(); row++) {
+        table.removeCells(row, columns, n);
+      }
+    }
+    columns = baseColumns + labelNames.size();
+
+    FlexCellFormatter fmt = table.getFlexCellFormatter();
+    for (int i = 0; i < labelNames.size(); i++) {
+      String name = labelNames.get(i);
+      int col = baseColumns + i;
+
+      String abbrev = getAbbreviation(name, "-");
+      table.setText(0, col, abbrev);
+      table.getCellFormatter().getElement(0, col).setTitle(name);
+      fmt.addStyleName(0, col, Gerrit.RESOURCES.css().dataHeader());
+    }
+
+    for (Section s : sections) {
+      if (s.titleRow >= 0) {
+        fmt.setColSpan(s.titleRow, 0, columns);
+      }
+    }
   }
 
   private void populateChangeRow(final int row, final ChangeInfo c,
-      final ChangeRowFormatter changeRowFormatter) {
-    ChangeCache cache = ChangeCache.get(c.getId());
-    cache.getChangeInfoCache().set(c);
-
-    table.setWidget(row, C_ARROW, null);
+      boolean highlightUnreviewed) {
+    CellFormatter fmt = table.getCellFormatter();
     if (Gerrit.isSignedIn()) {
-      table.setWidget(row, C_STAR, StarredChanges.createIcon(c.getId(), c.isStarred()));
+      table.setWidget(row, C_STAR, StarredChanges.createIcon(
+          c.legacy_id(),
+          c.starred()));
+    }
+    table.setWidget(row, C_ID, new TableChangeLink(String.valueOf(c.legacy_id()), c));
+
+    String subject = Util.cropSubject(c.subject());
+    table.setWidget(row, C_SUBJECT, new TableChangeLink(subject, c));
+
+    Change.Status status = c.status();
+    if (status != Change.Status.NEW) {
+      table.setText(row, C_STATUS, Util.toLongString(status));
+    } else if (!c.mergeable()) {
+      table.setText(row, C_STATUS, Util.C.changeTableNotMergeable());
     }
 
-    String s = Util.cropSubject(c.getSubject());
-    if (c.getStatus() != null && c.getStatus() != Change.Status.NEW) {
-      s += " (" + c.getStatus().name() + ")";
+    if (c.owner() != null) {
+      table.setWidget(row, C_OWNER, new AccountLinkPanel(c.owner(), status));
+    } else {
+      table.setText(row, C_OWNER, "");
     }
-    if (changeRowFormatter != null) {
-      removeChangeStyle(row, changeRowFormatter);
-      final String rowStyle = changeRowFormatter.getRowStyle(c);
-      if (rowStyle != null) {
-        table.getRowFormatter().addStyleName(row, rowStyle);
+
+    table.setWidget(row, C_PROJECT, new ProjectLink(c.project_name_key()));
+    table.setWidget(row, C_BRANCH, new BranchLink(c.project_name_key(), c
+        .status(), c.branch(), c.topic()));
+    if (Gerrit.isSignedIn()
+        && Gerrit.getUserAccount().getGeneralPreferences()
+            .isRelativeDateInChangeTable()) {
+      table.setText(row, C_LAST_UPDATE, relativeFormat(c.updated()));
+    } else {
+      table.setText(row, C_LAST_UPDATE, shortFormat(c.updated()));
+    }
+
+    int col = C_SIZE;
+    if (Gerrit.isSignedIn()
+        && !Gerrit.getUserAccount().getGeneralPreferences()
+            .isSizeBarInChangeTable()) {
+      table.setText(row, col,
+          Util.M.insertionsAndDeletions(c.insertions(), c.deletions()));
+    } else {
+      table.setWidget(row, col, getSizeWidget(c));
+      fmt.getElement(row, col).setTitle(
+          Util.M.insertionsAndDeletions(c.insertions(), c.deletions()));
+    }
+    col++;
+
+    for (int idx = 0; idx < labelNames.size(); idx++, col++) {
+      String name = labelNames.get(idx);
+
+      LabelInfo label = c.label(name);
+      if (label == null) {
+        fmt.getElement(row, col).setTitle(Gerrit.C.labelNotApplicable());
+        fmt.addStyleName(row, col, Gerrit.RESOURCES.css().labelNotApplicable());
+        continue;
       }
-      s = changeRowFormatter.getDisplayText(c, s);
+
+      String user;
+      String info;
+      ReviewCategoryStrategy reviewCategoryStrategy = Gerrit.isSignedIn()
+          ? Gerrit.getUserAccount().getGeneralPreferences()
+                .getReviewCategoryStrategy()
+          : ReviewCategoryStrategy.NONE;
+      if (label.rejected() != null) {
+        user = label.rejected().name();
+        info = getReviewCategoryDisplayInfo(reviewCategoryStrategy,
+            label.rejected());
+        if (info != null) {
+          FlowPanel panel = new FlowPanel();
+          panel.add(new Image(Gerrit.RESOURCES.redNot()));
+          panel.add(new InlineLabel(info));
+          table.setWidget(row, col, panel);
+        } else {
+          table.setWidget(row, col, new Image(Gerrit.RESOURCES.redNot()));
+        }
+      } else if (label.approved() != null) {
+        user = label.approved().name();
+        info = getReviewCategoryDisplayInfo(reviewCategoryStrategy,
+            label.approved());
+        if (info != null) {
+          FlowPanel panel = new FlowPanel();
+          panel.add(new Image(Gerrit.RESOURCES.greenCheck()));
+          panel.add(new InlineLabel(info));
+          table.setWidget(row, col, panel);
+        } else {
+          table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck()));
+        }
+      } else if (label.disliked() != null) {
+        user = label.disliked().name();
+        info = getReviewCategoryDisplayInfo(reviewCategoryStrategy,
+            label.disliked());
+        String vstr = String.valueOf(label._value());
+        if (info != null) {
+          vstr = vstr + " " + info;
+        }
+        fmt.addStyleName(row, col, Gerrit.RESOURCES.css().negscore());
+        table.setText(row, col, vstr);
+      } else if (label.recommended() != null) {
+        user = label.recommended().name();
+        info = getReviewCategoryDisplayInfo(reviewCategoryStrategy,
+            label.recommended());
+        String vstr = "+" + label._value();
+        if (info != null) {
+          vstr = vstr + " " + info;
+        }
+        fmt.addStyleName(row, col, Gerrit.RESOURCES.css().posscore());
+        table.setText(row, col, vstr);
+      } else {
+        table.clearCell(row, col);
+        continue;
+      }
+      fmt.addStyleName(row, col, Gerrit.RESOURCES.css().singleLine());
+
+      if (user != null) {
+        // Some web browsers ignore the embedded newline; some like it;
+        // so we include a space before the newline to accommodate both.
+        fmt.getElement(row, col).setTitle(name + " \nby " + user);
+      }
     }
 
-    table.setWidget(row, C_SUBJECT, new TableChangeLink(s, c));
-    table.setWidget(row, C_OWNER, link(c.getOwner()));
-    table.setWidget(row, C_PROJECT, new ProjectLink(c.getProject().getKey()));
-    table.setWidget(row, C_BRANCH, new BranchLink(c.getProject().getKey(), c
-        .getStatus(), c.getBranch(), c.getTopic()));
-    table.setText(row, C_LAST_UPDATE, shortFormat(c.getLastUpdatedOn()));
+    boolean needHighlight = false;
+    if (highlightUnreviewed && !c.reviewed()) {
+      needHighlight = true;
+    }
+    final Element tr = fmt.getElement(row, 0).getParentElement();
+    UIObject.setStyleName(tr, Gerrit.RESOURCES.css().needsReview(),
+        needHighlight);
 
     setRowItem(row, c);
   }
 
-  private void removeChangeStyle(int row,
-      final ChangeRowFormatter changeRowFormatter) {
-    final ChangeInfo oldChange = getRowItem(row);
-    if (oldChange == null) {
-      return;
-    }
-
-    final String oldRowStyle = changeRowFormatter.getRowStyle(oldChange);
-    if (oldRowStyle != null) {
-      table.getRowFormatter().removeStyleName(row, oldRowStyle);
+  private static String getReviewCategoryDisplayInfo(
+      ReviewCategoryStrategy reviewCategoryStrategy, AccountInfo accountInfo) {
+    switch (reviewCategoryStrategy) {
+      case NAME:
+        return accountInfo.name();
+      case EMAIL:
+        return accountInfo.email();
+      case USERNAME:
+        return accountInfo.username();
+      case ABBREV:
+        return getAbbreviation(accountInfo.name(), " ");
+      default:
+        return null;
     }
   }
 
-  private AccountLinkPanel link(final Account.Id id) {
-    return AccountLinkPanel.link(accountCache, id);
+  private static String getAbbreviation(String name, String token) {
+    StringBuilder abbrev = new StringBuilder();
+    if (name != null) {
+      for (String t : name.split(token)) {
+        abbrev.append(t.substring(0, 1).toUpperCase());
+      }
+    }
+    return abbrev.toString();
+  }
+
+  private static Widget getSizeWidget(ChangeInfo c) {
+    int largeChangeSize = Gerrit.getConfig().getLargeChangeSize();
+    int changedLines = c.insertions() + c.deletions();
+    int p = 100;
+    if (changedLines < largeChangeSize) {
+      p = changedLines * 100 / largeChangeSize;
+    }
+
+    int width = Math.max(2, 70 * p / 100);
+    int red = p >= 50 ? 255 : (int) Math.round((p) * 5.12);
+    int green = p <= 50 ? 255 : (int) Math.round(256 - (p - 50) * 5.12);
+    String bg = "#" + toHex(red) + toHex(green) + "00";
+
+    SimplePanel panel = new SimplePanel();
+    panel.setStyleName(Gerrit.RESOURCES.css().changeSize());
+    panel.setWidth(width + "px");
+    panel.getElement().getStyle().setBackgroundColor(bg);
+    return panel;
+  }
+
+  private static String toHex(int i) {
+    String hex = Integer.toHexString(i);
+    return hex.length() == 1 ? "0" + hex : hex;
   }
 
   public void addSection(final Section s) {
     assert s.parent == null;
 
-    if (s.titleText != null) {
-      s.titleRow = table.getRowCount();
-      table.setText(s.titleRow, 0, s.titleText);
+    s.parent = this;
+    s.titleRow = table.getRowCount();
+    if (s.displayTitle()) {
       final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.setColSpan(s.titleRow, 0, COLUMNS);
+      fmt.setColSpan(s.titleRow, 0, columns);
       fmt.addStyleName(s.titleRow, 0, Gerrit.RESOURCES.css().sectionHeader());
     } else {
       s.titleRow = -1;
     }
 
-    s.parent = this;
     s.dataBegin = table.getRowCount();
     insertNoneRow(s.dataBegin);
     sections.add(s);
   }
 
-  public void setAccountInfoCache(final AccountInfoCache aic) {
-    assert aic != null;
-    accountCache = aic;
-  }
-
   private int insertRow(final int beforeRow) {
     for (final Section s : sections) {
       if (beforeRow <= s.titleRow) {
@@ -240,13 +437,17 @@
 
     @Override
     public void onKeyPress(final KeyPressEvent event) {
-      onStarClick(getCurrentRow());
+      int row = getCurrentRow();
+      ChangeInfo c = getRowItem(row);
+      if (c != null && Gerrit.isSignedIn()) {
+        ((StarredChanges.Icon) table.getWidget(row, C_STAR)).toggleStar();
+      }
     }
   }
 
   private final class TableChangeLink extends ChangeLink {
     private TableChangeLink(final String text, final ChangeInfo c) {
-      super(text, c);
+      super(text, c.legacy_id());
     }
 
     @Override
@@ -257,42 +458,47 @@
   }
 
   public static class Section {
-    String titleText;
-
     ChangeTable parent;
-    final Account.Id ownerId;
+    String titleText;
+    Widget titleWidget;
     int titleRow = -1;
     int dataBegin;
     int rows;
+    private boolean highlightUnreviewed;
 
-    private ChangeRowFormatter changeRowFormatter;
-
-    public Section() {
-      this(null, null);
-    }
-
-    public Section(final String titleText) {
-      this(titleText, null);
-    }
-
-    public Section(final String titleText, final Account.Id owner) {
-      setTitleText(titleText);
-      ownerId = owner;
+    public void setHighlightUnreviewed(boolean value) {
+      this.highlightUnreviewed = value;
     }
 
     public void setTitleText(final String text) {
       titleText = text;
+      titleWidget = null;
       if (titleRow >= 0) {
         parent.table.setText(titleRow, 0, titleText);
       }
     }
 
-    public void setChangeRowFormatter(final ChangeRowFormatter changeRowFormatter) {
-      this.changeRowFormatter = changeRowFormatter;
+    public void setTitleWidget(final Widget title) {
+      titleWidget = title;
+      titleText = null;
+      if (titleRow >= 0) {
+        parent.table.setWidget(titleRow, 0, title);
+      }
     }
 
-    public void display(final List<ChangeInfo> changeList) {
-      final int sz = changeList != null ? changeList.size() : 0;
+    public boolean displayTitle() {
+      if (titleText != null) {
+        setTitleText(titleText);
+        return true;
+      } else if(titleWidget != null) {
+        setTitleWidget(titleWidget);
+        return true;
+      }
+      return false;
+    }
+
+    public void display(ChangeList changeList) {
+      final int sz = changeList != null ? changeList.length() : 0;
       final boolean hadData = rows > 0;
 
       if (hadData) {
@@ -300,51 +506,23 @@
           parent.removeRow(dataBegin);
           rows--;
         }
+      } else {
+        parent.removeRow(dataBegin);
       }
 
       if (sz == 0) {
-        if (hadData) {
-          parent.insertNoneRow(dataBegin);
-        }
-      } else {
-        Set<Change.Id> cids = new HashSet<>();
+        parent.insertNoneRow(dataBegin);
+        return;
+      }
 
-        if (!hadData) {
-          parent.removeRow(dataBegin);
-        }
-
-        while (rows < sz) {
-          parent.insertChangeRow(dataBegin + rows);
-          rows++;
-        }
-
-        for (int i = 0; i < sz; i++) {
-          ChangeInfo c = changeList.get(i);
-          parent.populateChangeRow(dataBegin + i, c, changeRowFormatter);
-          cids.add(c.getId());
-        }
+      while (rows < sz) {
+        parent.insertChangeRow(dataBegin + rows);
+        rows++;
+      }
+      for (int i = 0; i < sz; i++) {
+        parent.populateChangeRow(dataBegin + i, changeList.get(i),
+            highlightUnreviewed);
       }
     }
   }
-
-  public static interface ChangeRowFormatter {
-    /**
-     * Returns the name of the CSS style that should be applied to the change
-     * row.
-     *
-     * @param c the change for which the styling should be returned
-     * @return the name of the CSS style that should be applied to the change
-     *         row
-     */
-    String getRowStyle(ChangeInfo c);
-
-    /**
-     * Returns the text that should be displayed for the change.
-     *
-     * @param c the change for which the display text should be returned
-     * @param displayText the current display text
-     * @return the new display text
-     */
-    String getDisplayText(ChangeInfo c, String displayText);
-  }
 }
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
deleted file mode 100644
index 270b9f5..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
+++ /dev/null
@@ -1,542 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import static com.google.gerrit.client.FormatUtil.relativeFormat;
-import static com.google.gerrit.client.FormatUtil.shortFormat;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
-import com.google.gerrit.client.account.AccountInfo;
-import com.google.gerrit.client.ui.AccountLinkPanel;
-import com.google.gerrit.client.ui.BranchLink;
-import com.google.gerrit.client.ui.ChangeLink;
-import com.google.gerrit.client.ui.NavigationTable;
-import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
-import com.google.gerrit.client.ui.ProjectLink;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTMLTable.Cell;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.SimplePanel;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwt.user.client.ui.Widget;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-public class ChangeTable2 extends NavigationTable<ChangeInfo> {
-  private static final int C_STAR = 1;
-  private static final int C_ID = 2;
-  private static final int C_SUBJECT = 3;
-  private static final int C_STATUS = 4;
-  private static final int C_OWNER = 5;
-  private static final int C_PROJECT = 6;
-  private static final int C_BRANCH = 7;
-  private static final int C_LAST_UPDATE = 8;
-  private static final int C_SIZE = 9;
-  private static final int BASE_COLUMNS = 10;
-
-
-  private final boolean useNewFeatures = Gerrit.getConfig().getNewFeatures();
-  private final List<Section> sections;
-  private int columns;
-  private List<String> labelNames;
-
-  public ChangeTable2() {
-    super(Util.C.changeItemHelp());
-    columns = useNewFeatures ? BASE_COLUMNS : BASE_COLUMNS - 1;
-    labelNames = Collections.emptyList();
-
-    if (Gerrit.isSignedIn()) {
-      keysAction.add(new StarKeyCommand(0, 's', Util.C.changeTableStar()));
-    }
-
-    sections = new ArrayList<>();
-    table.setText(0, C_STAR, "");
-    table.setText(0, C_ID, Util.C.changeTableColumnID());
-    table.setText(0, C_SUBJECT, Util.C.changeTableColumnSubject());
-    table.setText(0, C_STATUS, Util.C.changeTableColumnStatus());
-    table.setText(0, C_OWNER, Util.C.changeTableColumnOwner());
-    table.setText(0, C_PROJECT, Util.C.changeTableColumnProject());
-    table.setText(0, C_BRANCH, Util.C.changeTableColumnBranch());
-    table.setText(0, C_LAST_UPDATE, Util.C.changeTableColumnLastUpdate());
-    if (useNewFeatures) {
-      table.setText(0, C_SIZE, Util.C.changeTableColumnSize());
-    }
-
-    final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.addStyleName(0, C_STAR, Gerrit.RESOURCES.css().iconHeader());
-    for (int i = C_ID; i < columns; i++) {
-      fmt.addStyleName(0, i, Gerrit.RESOURCES.css().dataHeader());
-    }
-
-    if (!Gerrit.isSignedIn() ||
-       (!Gerrit.getUserAccount().getGeneralPreferences()
-         .isLegacycidInChangeTable())) {
-      fmt.addStyleName(0, C_ID, Gerrit.RESOURCES.css().dataHeaderHidden());
-    }
-
-    table.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        final Cell cell = table.getCellForEvent(event);
-        if (cell == null) {
-          return;
-        }
-        if (cell.getCellIndex() == C_STAR) {
-          // Don't do anything (handled by star itself).
-        } else if (cell.getCellIndex() == C_STATUS) {
-          // Don't do anything.
-        } else if (cell.getCellIndex() == C_OWNER) {
-          // Don't do anything.
-        } else if (getRowItem(cell.getRowIndex()) != null) {
-          movePointerTo(cell.getRowIndex());
-        }
-      }
-    });
-  }
-
-  @Override
-  protected Object getRowItemKey(final ChangeInfo item) {
-    return item.legacy_id();
-  }
-
-  @Override
-  protected void onOpenRow(final int row) {
-    final ChangeInfo c = getRowItem(row);
-    final Change.Id id = c.legacy_id();
-    Gerrit.display(PageLinks.toChange(id));
-  }
-
-  private void insertNoneRow(final int row) {
-    insertRow(row);
-    table.setText(row, 0, Util.C.changeTableNone());
-    final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.setColSpan(row, 0, columns);
-    fmt.setStyleName(row, 0, Gerrit.RESOURCES.css().emptySection());
-  }
-
-  private void insertChangeRow(final int row) {
-    insertRow(row);
-    applyDataRowStyle(row);
-  }
-
-  @Override
-  protected void applyDataRowStyle(final int row) {
-    super.applyDataRowStyle(row);
-    final CellFormatter fmt = table.getCellFormatter();
-    fmt.addStyleName(row, C_STAR, Gerrit.RESOURCES.css().iconCell());
-    for (int i = C_ID; i < columns; i++) {
-      fmt.addStyleName(row, i, Gerrit.RESOURCES.css().dataCell());
-    }
-    fmt.addStyleName(row, C_SUBJECT, Gerrit.RESOURCES.css().cSUBJECT());
-    fmt.addStyleName(row, C_STATUS, Gerrit.RESOURCES.css().cSTATUS());
-    fmt.addStyleName(row, C_OWNER, Gerrit.RESOURCES.css().cOWNER());
-    fmt.addStyleName(row, C_LAST_UPDATE, Gerrit.RESOURCES.css().cLastUpdate());
-
-    if (!Gerrit.isSignedIn() ||
-       (!Gerrit.getUserAccount().getGeneralPreferences()
-         .isLegacycidInChangeTable())) {
-      fmt.addStyleName(row, C_ID, Gerrit.RESOURCES.css().dataCellHidden());
-    }
-
-    int i = C_SIZE;
-    if (useNewFeatures) {
-      fmt.addStyleName(row, i++, Gerrit.RESOURCES.css().cSIZE());
-    }
-    for (; i < columns; i++) {
-      fmt.addStyleName(row, i, Gerrit.RESOURCES.css().cAPPROVAL());
-    }
-  }
-
-  public void updateColumnsForLabels(ChangeList... lists) {
-    labelNames = new ArrayList<>();
-    for (ChangeList list : lists) {
-      for (int i = 0; i < list.length(); i++) {
-        for (String name : list.get(i).labels()) {
-          if (!labelNames.contains(name)) {
-            labelNames.add(name);
-          }
-        }
-      }
-    }
-    Collections.sort(labelNames);
-
-    int baseColumns = useNewFeatures ? BASE_COLUMNS : BASE_COLUMNS - 1;
-    if (baseColumns + labelNames.size() < columns) {
-      int n = columns - (baseColumns + labelNames.size());
-      for (int row = 0; row < table.getRowCount(); row++) {
-        table.removeCells(row, columns, n);
-      }
-    }
-    columns = baseColumns + labelNames.size();
-
-    FlexCellFormatter fmt = table.getFlexCellFormatter();
-    for (int i = 0; i < labelNames.size(); i++) {
-      String name = labelNames.get(i);
-      int col = baseColumns + i;
-
-      String abbrev = getAbbreviation(name, "-");
-      table.setText(0, col, abbrev);
-      table.getCellFormatter().getElement(0, col).setTitle(name);
-      fmt.addStyleName(0, col, Gerrit.RESOURCES.css().dataHeader());
-    }
-
-    for (Section s : sections) {
-      if (s.titleRow >= 0) {
-        fmt.setColSpan(s.titleRow, 0, columns);
-      }
-    }
-  }
-
-  private void populateChangeRow(final int row, final ChangeInfo c,
-      boolean highlightUnreviewed) {
-    CellFormatter fmt = table.getCellFormatter();
-    if (Gerrit.isSignedIn()) {
-      table.setWidget(row, C_STAR, StarredChanges.createIcon(
-          c.legacy_id(),
-          c.starred()));
-    }
-    table.setWidget(row, C_ID, new TableChangeLink(String.valueOf(c.legacy_id()), c));
-
-    String subject = Util.cropSubject(c.subject());
-    table.setWidget(row, C_SUBJECT, new TableChangeLink(subject, c));
-
-    Change.Status status = c.status();
-    if (status != Change.Status.NEW) {
-      table.setText(row, C_STATUS, Util.toLongString(status));
-    } else if (!c.mergeable() && useNewFeatures) {
-      table.setText(row, C_STATUS, Util.C.changeTableNotMergeable());
-    }
-
-    if (c.owner() != null) {
-      table.setWidget(row, C_OWNER, new AccountLinkPanel(c.owner(), status));
-    } else {
-      table.setText(row, C_OWNER, "");
-    }
-
-    table.setWidget(row, C_PROJECT, new ProjectLink(c.project_name_key()));
-    table.setWidget(row, C_BRANCH, new BranchLink(c.project_name_key(), c
-        .status(), c.branch(), c.topic()));
-    if (Gerrit.isSignedIn()
-        && Gerrit.getUserAccount().getGeneralPreferences()
-            .isRelativeDateInChangeTable()) {
-      table.setText(row, C_LAST_UPDATE, relativeFormat(c.updated()));
-    } else {
-      table.setText(row, C_LAST_UPDATE, shortFormat(c.updated()));
-    }
-    int col = C_SIZE;
-    if (useNewFeatures) {
-      if (Gerrit.isSignedIn()
-          && !Gerrit.getUserAccount().getGeneralPreferences()
-              .isSizeBarInChangeTable()) {
-        table.setText(row, col,
-            Util.M.insertionsAndDeletions(c.insertions(), c.deletions()));
-      } else {
-        table.setWidget(row, col, getSizeWidget(c));
-        fmt.getElement(row, col).setTitle(
-            Util.M.insertionsAndDeletions(c.insertions(), c.deletions()));
-      }
-      col++;
-    }
-
-    boolean displayInfo = Gerrit.isSignedIn() && Gerrit.getUserAccount()
-        .getGeneralPreferences().isShowInfoInReviewCategory();
-
-    for (int idx = 0; idx < labelNames.size(); idx++, col++) {
-      String name = labelNames.get(idx);
-
-      LabelInfo label = c.label(name);
-      if (label == null) {
-        fmt.getElement(row, col).setTitle(Gerrit.C.labelNotApplicable());
-        fmt.addStyleName(row, col, Gerrit.RESOURCES.css().labelNotApplicable());
-        continue;
-      }
-
-      String user;
-      String info;
-      ReviewCategoryStrategy reviewCategoryStrategy = Gerrit.isSignedIn()
-          ? Gerrit.getUserAccount().getGeneralPreferences()
-                .getReviewCategoryStrategy()
-          : ReviewCategoryStrategy.NONE;
-      if (label.rejected() != null) {
-        user = label.rejected().name();
-        info = getReviewCategoryDisplayInfo(reviewCategoryStrategy,
-            label.rejected());
-        if (displayInfo && info != null) {
-          FlowPanel panel = new FlowPanel();
-          panel.add(new Image(Gerrit.RESOURCES.redNot()));
-          panel.add(new InlineLabel(info));
-          table.setWidget(row, col, panel);
-        } else {
-          table.setWidget(row, col, new Image(Gerrit.RESOURCES.redNot()));
-        }
-      } else if (label.approved() != null) {
-        user = label.approved().name();
-        info = getReviewCategoryDisplayInfo(reviewCategoryStrategy,
-            label.approved());
-        if (displayInfo && info != null) {
-          FlowPanel panel = new FlowPanel();
-          panel.add(new Image(Gerrit.RESOURCES.greenCheck()));
-          panel.add(new InlineLabel(info));
-          table.setWidget(row, col, panel);
-        } else {
-          table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck()));
-        }
-      } else if (label.disliked() != null) {
-        user = label.disliked().name();
-        info = getReviewCategoryDisplayInfo(reviewCategoryStrategy,
-            label.disliked());
-        String vstr = String.valueOf(label._value());
-        if (displayInfo && info != null) {
-          vstr = vstr + " " + info;
-        }
-        fmt.addStyleName(row, col, Gerrit.RESOURCES.css().negscore());
-        table.setText(row, col, vstr);
-      } else if (label.recommended() != null) {
-        user = label.recommended().name();
-        info = getReviewCategoryDisplayInfo(reviewCategoryStrategy,
-            label.recommended());
-        String vstr = "+" + label._value();
-        if (displayInfo && info != null) {
-          vstr = vstr + " " + info;
-        }
-        fmt.addStyleName(row, col, Gerrit.RESOURCES.css().posscore());
-        table.setText(row, col, vstr);
-      } else {
-        table.clearCell(row, col);
-        continue;
-      }
-      fmt.addStyleName(row, col, Gerrit.RESOURCES.css().singleLine());
-
-      if ((!displayInfo || reviewCategoryStrategy == ReviewCategoryStrategy.ABBREV)
-          && user != null) {
-        // Some web browsers ignore the embedded newline; some like it;
-        // so we include a space before the newline to accommodate both.
-        fmt.getElement(row, col).setTitle(name + " \nby " + user);
-      }
-    }
-
-    boolean needHighlight = false;
-    if (highlightUnreviewed && !c.reviewed()) {
-      needHighlight = true;
-    }
-    final Element tr = fmt.getElement(row, 0).getParentElement();
-    UIObject.setStyleName(tr, Gerrit.RESOURCES.css().needsReview(),
-        needHighlight);
-
-    setRowItem(row, c);
-  }
-
-  private static String getReviewCategoryDisplayInfo(
-      ReviewCategoryStrategy reviewCategoryStrategy, AccountInfo accountInfo) {
-    switch (reviewCategoryStrategy) {
-      case NAME:
-        return accountInfo.name();
-      case EMAIL:
-        return accountInfo.email();
-      case USERNAME:
-        return accountInfo.username();
-      case ABBREV:
-        return getAbbreviation(accountInfo.name(), " ");
-      default:
-        return null;
-    }
-  }
-
-  private static String getAbbreviation(String name, String token) {
-    StringBuilder abbrev = new StringBuilder();
-    if (name != null) {
-      for (String t : name.split(token)) {
-        abbrev.append(t.substring(0, 1).toUpperCase());
-      }
-    }
-    return abbrev.toString();
-  }
-
-  private static Widget getSizeWidget(ChangeInfo c) {
-    int largeChangeSize = Gerrit.getConfig().getLargeChangeSize();
-    int changedLines = c.insertions() + c.deletions();
-    int p = 100;
-    if (changedLines < largeChangeSize) {
-      p = changedLines * 100 / largeChangeSize;
-    }
-
-    int width = Math.max(2, 70 * p / 100);
-    int red = p > 50 ? 255 : (int) Math.round((p) * 5.12);
-    int green = p < 50 ? 255 : (int) Math.round(256 - (p - 50) * 5.12);
-    String bg = "#" + toHex(red) + toHex(green) + "00";
-
-    SimplePanel panel = new SimplePanel();
-    panel.setStyleName(Gerrit.RESOURCES.css().changeSize());
-    panel.setWidth(width + "px");
-    panel.getElement().getStyle().setBackgroundColor(bg);
-    return panel;
-  }
-
-  private static String toHex(int i) {
-    String hex = Integer.toHexString(i);
-    return hex.length() == 1 ? "0" + hex : hex;
-  }
-
-  public void addSection(final Section s) {
-    assert s.parent == null;
-
-    s.parent = this;
-    s.titleRow = table.getRowCount();
-    if (s.displayTitle()) {
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.setColSpan(s.titleRow, 0, columns);
-      fmt.addStyleName(s.titleRow, 0, Gerrit.RESOURCES.css().sectionHeader());
-    } else {
-      s.titleRow = -1;
-    }
-
-    s.dataBegin = table.getRowCount();
-    insertNoneRow(s.dataBegin);
-    sections.add(s);
-  }
-
-  private int insertRow(final int beforeRow) {
-    for (final Section s : sections) {
-      if (beforeRow <= s.titleRow) {
-        s.titleRow++;
-      }
-      if (beforeRow < s.dataBegin) {
-        s.dataBegin++;
-      }
-    }
-    return table.insertRow(beforeRow);
-  }
-
-  private void removeRow(final int row) {
-    for (final Section s : sections) {
-      if (row < s.titleRow) {
-        s.titleRow--;
-      }
-      if (row < s.dataBegin) {
-        s.dataBegin--;
-      }
-    }
-    table.removeRow(row);
-  }
-
-  public class StarKeyCommand extends NeedsSignInKeyCommand {
-    public StarKeyCommand(int mask, char key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-      int row = getCurrentRow();
-      ChangeInfo c = getRowItem(row);
-      if (c != null && Gerrit.isSignedIn()) {
-        ((StarredChanges.Icon) table.getWidget(row, C_STAR)).toggleStar();
-      }
-    }
-  }
-
-  private final class TableChangeLink extends ChangeLink {
-    private TableChangeLink(final String text, final ChangeInfo c) {
-      super(text, c.legacy_id());
-    }
-
-    @Override
-    public void go() {
-      movePointerTo(cid);
-      super.go();
-    }
-  }
-
-  public static class Section {
-    ChangeTable2 parent;
-    String titleText;
-    Widget titleWidget;
-    int titleRow = -1;
-    int dataBegin;
-    int rows;
-    private boolean highlightUnreviewed;
-
-    public void setHighlightUnreviewed(boolean value) {
-      this.highlightUnreviewed = value;
-    }
-
-    public void setTitleText(final String text) {
-      titleText = text;
-      titleWidget = null;
-      if (titleRow >= 0) {
-        parent.table.setText(titleRow, 0, titleText);
-      }
-    }
-
-    public void setTitleWidget(final Widget title) {
-      titleWidget = title;
-      titleText = null;
-      if (titleRow >= 0) {
-        parent.table.setWidget(titleRow, 0, title);
-      }
-    }
-
-    public boolean displayTitle() {
-      if (titleText != null) {
-        setTitleText(titleText);
-        return true;
-      } else if(titleWidget != null) {
-        setTitleWidget(titleWidget);
-        return true;
-      }
-      return false;
-    }
-
-    public void display(ChangeList changeList) {
-      final int sz = changeList != null ? changeList.length() : 0;
-      final boolean hadData = rows > 0;
-
-      if (hadData) {
-        while (sz < rows) {
-          parent.removeRow(dataBegin);
-          rows--;
-        }
-      } else {
-        parent.removeRow(dataBegin);
-      }
-
-      if (sz == 0) {
-        parent.insertNoneRow(dataBegin);
-        return;
-      }
-
-      while (rows < sz) {
-        parent.insertChangeRow(dataBegin + rows);
-        rows++;
-      }
-      for (int i = 0; i < sz; i++) {
-        parent.populateChangeRow(dataBegin + i, changeList.get(i),
-            highlightUnreviewed);
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
index e1ec47a..52e5d40 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.client.diff.CommentRange;
-import com.google.gerrit.common.changes.Side;
+import com.google.gerrit.extensions.client.Side;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
deleted file mode 100644
index 1f66b72..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
+++ /dev/null
@@ -1,188 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.ChangeLink;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.client.ui.CommentedActionDialog;
-import com.google.gerrit.client.ui.TextBoxChangeListener;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.dom.client.PreElement;
-import com.google.gwt.dom.client.Style.Display;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.SimplePanel;
-import com.google.gwtexpui.clippy.client.CopyableLabel;
-import com.google.gwtexpui.globalkey.client.KeyCommandSet;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-
-public class CommitMessageBlock extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, CommitMessageBlock> {
-  }
-
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  private KeyCommandSet keysAction;
-
-  @UiField
-  SimplePanel starPanel;
-  @UiField
-  FlowPanel permalinkPanel;
-  @UiField
-  PreElement commitSummaryPre;
-  @UiField
-  PreElement commitBodyPre;
-
-  public CommitMessageBlock() {
-    initWidget(uiBinder.createAndBindUi(this));
-  }
-
-  public CommitMessageBlock(KeyCommandSet keysAction) {
-    this.keysAction = keysAction;
-    initWidget(uiBinder.createAndBindUi(this));
-  }
-
-  public void display(String commitMessage,
-      CommentLinkProcessor commentLinkProcessor) {
-    display(null, null, null, false, commitMessage, commentLinkProcessor);
-  }
-
-  private abstract class CommitMessageEditDialog
-      extends CommentedActionDialog<JavaScriptObject> {
-    private final String originalMessage;
-    public CommitMessageEditDialog(final String title, final String heading,
-        final String commitMessage, AsyncCallback<JavaScriptObject> callback) {
-      super(title, heading, callback);
-      originalMessage = commitMessage.trim();
-      message.setCharacterWidth(72);
-      message.setVisibleLines(20);
-      message.setText(originalMessage);
-      message.addStyleName(Gerrit.RESOURCES.css().changeScreenDescription());
-      sendButton.setEnabled(false);
-
-      new TextBoxChangeListener(message) {
-        public void onTextChanged(String newText) {
-          // Trim the new text so we don't consider trailing
-          // newlines as changes
-          sendButton.setEnabled(!newText.trim().equals(originalMessage));
-        }
-      };
-    }
-
-    public String getMessageText() {
-      // As we rely on commit message lines ending in LF, we convert CRLF to
-      // LF. Additionally, the commit message should be trimmed to remove any
-      // excess newlines at the end, but we need to make sure it still has at
-      // least one trailing newline.
-      return message.getText().replaceAll("\r\n", "\n").trim() + '\n';
-    }
-  }
-
-  public void display(final PatchSet.Id patchSetId, final String revision,
-      Boolean starred, Boolean canEditCommitMessage, final String commitMessage,
-      CommentLinkProcessor commentLinkProcessor) {
-    starPanel.clear();
-    if (patchSetId != null && starred != null && Gerrit.isSignedIn()) {
-      Change.Id changeId = patchSetId.getParentKey();
-      StarredChanges.Icon star = StarredChanges.createIcon(changeId, starred);
-      star.setStyleName(Gerrit.RESOURCES.css().changeScreenStarIcon());
-      starPanel.add(star);
-
-      if (keysAction != null) {
-        keysAction.add(StarredChanges.newKeyCommand(star));
-      }
-    }
-
-    permalinkPanel.clear();
-    if (patchSetId != null && revision != null) {
-      final Change.Id changeId = patchSetId.getParentKey();
-      permalinkPanel.add(new ChangeLink(Util.C.changePermalink(), changeId));
-      permalinkPanel.add(new CopyableLabel(ChangeLink.permalink(changeId),
-          false));
-      if (canEditCommitMessage) {
-        final Image edit = new Image(Gerrit.RESOURCES.edit());
-        edit.setTitle(Util.C.editCommitMessageToolTip());
-        edit.addStyleName(Gerrit.RESOURCES.css().link());
-        edit.addClickHandler(new ClickHandler() {
-          @Override
-          public void onClick(final ClickEvent event) {
-            new CommitMessageEditDialog(Util.C.titleEditCommitMessage(),
-                Util.C.headingEditCommitMessage(),
-                commitMessage,
-                new GerritCallback<JavaScriptObject>() {
-                  @Override
-                  public void onSuccess(JavaScriptObject result) {}
-                }) {
-              @Override
-              public void onSend() {
-                ChangeApi.message(changeId.get(), revision, getMessageText(),
-                    new GerritCallback<JavaScriptObject>() {
-                      @Override
-                      public void onSuccess(JavaScriptObject msg) {
-                        Gerrit.display(PageLinks.toChange(changeId));
-                        hide();
-                      }
-                    });
-              }
-            }.center();
-          }
-        });
-
-        permalinkPanel.add(edit);
-      }
-    }
-
-    String[] splitCommitMessage = commitMessage.split("\n", 2);
-
-    String commitSummary = splitCommitMessage[0];
-    String commitBody = "";
-    if (splitCommitMessage.length > 1) {
-      commitBody = splitCommitMessage[1];
-    }
-
-    // Linkify commit summary
-    SafeHtml commitSummaryLinkified = new SafeHtmlBuilder().append(commitSummary);
-    commitSummaryLinkified = commitSummaryLinkified.linkify();
-    commitSummaryLinkified = commentLinkProcessor.apply(commitSummaryLinkified);
-    commitSummaryPre.setInnerHTML(commitSummaryLinkified.asString());
-
-    // Hide commit body if there is no body
-    if (commitBody.trim().isEmpty()) {
-      commitBodyPre.getStyle().setDisplay(Display.NONE);
-    } else {
-      // Linkify commit body
-      SafeHtml commitBodyLinkified = new SafeHtmlBuilder().append(commitBody);
-      commitBodyLinkified = commitBodyLinkified.linkify();
-      commitBodyLinkified = commentLinkProcessor.apply(commitBodyLinkified);
-      commitBodyLinkified = commitBodyLinkified.replaceAll("\n\n", "<p></p>");
-      commitBodyLinkified = commitBodyLinkified.replaceAll("\n", "<br />");
-      commitBodyPre.setInnerHTML(commitBodyLinkified.asString());
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
index 53c5c6d..9e97c56 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.http.client.URL;
@@ -31,7 +31,7 @@
 import java.util.List;
 import java.util.ListIterator;
 
-public class DashboardTable extends ChangeTable2 {
+public class DashboardTable extends ChangeTable {
   private List<Section> sections;
   private String title;
   private List<String> titles;
@@ -96,6 +96,7 @@
     return unlimitedQuery.toString().trim();
   }
 
+  @Override
   public String getTitle() {
     return title;
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/IncludedInTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/IncludedInTable.java
deleted file mode 100644
index 62fbfb4..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/IncludedInTable.java
+++ /dev/null
@@ -1,103 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeInfo.IncludedInInfo;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.common.data.IncludedInDetail;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.event.logical.shared.OpenEvent;
-import com.google.gwt.event.logical.shared.OpenHandler;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.DisclosurePanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-
-import java.util.ArrayList;
-import java.util.List;
-
-
-/** Displays a table of Branches and Tags containing the change record. */
-public class IncludedInTable extends Composite implements
-    OpenHandler<DisclosurePanel> {
-  private final Grid table;
-  private final Change.Id changeId;
-  private boolean loaded = false;
-
-  public IncludedInTable(final Change.Id chId) {
-    changeId = chId;
-    table = new Grid(1, 1);
-    initWidget(table);
-  }
-
-  public void loadTable(final IncludedInDetail detail) {
-    int row = 0;
-    table.resizeRows(detail.getBranches().size() + 1);
-    table.addStyleName(Gerrit.RESOURCES.css().changeTable());
-    final CellFormatter fmt = table.getCellFormatter();
-    fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().dataHeader());
-    table.setText(row, 0, Util.C.includedInTableBranch());
-
-    for (final String branch : detail.getBranches()) {
-      fmt.addStyleName(++row, 0, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().leftMostCell());
-      table.setText(row, 0, branch);
-    }
-
-    if (!detail.getTags().isEmpty()) {
-      table.resizeRows(table.getRowCount() + 2 + detail.getTags().size());
-      row++;
-      fmt.addStyleName(++row, 0, Gerrit.RESOURCES.css().dataHeader());
-      table.setText(row, 0, Util.C.includedInTableTag());
-
-      for (final String tag : detail.getTags()) {
-        fmt.addStyleName(++row, 0, Gerrit.RESOURCES.css().dataCell());
-        fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().leftMostCell());
-        table.setText(row, 0, tag);
-      }
-    }
-
-    table.setVisible(true);
-    loaded = true;
-  }
-
-  @Override
-  public void onOpen(OpenEvent<DisclosurePanel> event) {
-    if (!loaded) {
-      ChangeApi.includedIn(changeId.get(),
-          new GerritCallback<IncludedInInfo>() {
-        @Override
-        public void onSuccess(IncludedInInfo r) {
-          IncludedInDetail result = new IncludedInDetail();
-          result.setBranches(toList(r.branches()));
-          result.setTags(toList(r.tags()));
-          loadTable(result);
-        }
-
-        private List<String> toList(JsArrayString in) {
-          List<String> r = new ArrayList<>();
-          if (in != null) {
-            for (int i = 0; i < in.length(); i++) {
-              r.add(in.get(i));
-            }
-          }
-          return r;
-        }
-      });
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
index f98ac78..9527c36 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
@@ -31,8 +31,8 @@
   private final String anchorPrefix;
 
   protected ChangeList changes;
-  private ChangeTable2 table;
-  private ChangeTable2.Section section;
+  private ChangeTable table;
+  private ChangeTable.Section section;
   private Hyperlink prev;
   private Hyperlink next;
 
@@ -59,7 +59,7 @@
     next = new Hyperlink(Util.C.pagedChangeListNext(), true, "");
     next.setVisible(false);
 
-    table = new ChangeTable2() {
+    table = new ChangeTable() {
       {
         keysNavigation.add(
             new DoLinkCommand(0, 'p', Util.C.changeTablePagePrev(), prev),
@@ -77,7 +77,7 @@
         });
       }
     };
-    section = new ChangeTable2.Section();
+    section = new ChangeTable.Section();
     table.addSection(section);
     table.setSavePointerId(anchorPrefix);
     add(table);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
deleted file mode 100644
index b7a0ec8..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
+++ /dev/null
@@ -1,730 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.GitwebLink;
-import com.google.gerrit.client.change.DraftActions;
-import com.google.gerrit.client.download.DownloadPanel;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.client.ui.AccountLinkPanel;
-import com.google.gerrit.client.ui.ActionDialog;
-import com.google.gerrit.client.ui.CherryPickDialog;
-import com.google.gerrit.client.ui.ComplexDisclosurePanel;
-import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.ChangeDetail;
-import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.common.data.UiCommandDetail;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.reviewdb.client.UserIdentity;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.logical.shared.OpenEvent;
-import com.google.gwt.event.logical.shared.OpenHandler;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.DisclosurePanel;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.Panel;
-
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
-    implements OpenHandler<DisclosurePanel> {
-  private static final int R_AUTHOR = 0;
-  private static final int R_COMMITTER = 1;
-  private static final int R_PARENTS = 2;
-  private static final int R_DOWNLOAD = 3;
-  private static final int R_CNT = 4;
-
-  private final ChangeDetailCache detailCache;
-  private final ChangeDetail changeDetail;
-  private final PatchSet patchSet;
-  private final FlowPanel body;
-
-  private Grid infoTable;
-  private Panel actionsPanel;
-  private PatchTable patchTable;
-  private final Set<ClickHandler> registeredClickHandler =  new HashSet<>();
-
-  private PatchSet.Id diffBaseId;
-
-  /**
-   * Creates a closed complex disclosure panel for a patch set.
-   * The patch set details are loaded when the complex disclosure panel is opened.
-   */
-  public PatchSetComplexDisclosurePanel(final PatchSet ps, boolean isOpen,
-      boolean hasDraftComments) {
-    super(Util.M.patchSetHeader(ps.getPatchSetId()), isOpen);
-    detailCache = ChangeCache.get(ps.getId().getParentKey()).getChangeDetailCache();
-    changeDetail = detailCache.get();
-    patchSet = ps;
-
-    body = new FlowPanel();
-    setContent(body);
-
-    if (hasDraftComments) {
-      final Image draftComments = new Image(Gerrit.RESOURCES.draftComments());
-      draftComments.setTitle(Util.C.patchSetWithDraftCommentsToolTip());
-      getHeader().add(draftComments);
-    }
-
-    final GitwebLink gw = Gerrit.getGitwebLink();
-    final InlineLabel revtxt = new InlineLabel(ps.getRevision().get() + " ");
-    revtxt.addStyleName(Gerrit.RESOURCES.css().patchSetRevision());
-    getHeader().add(revtxt);
-    if (gw != null && gw.canLink(ps)) {
-      final Anchor revlink =
-          new Anchor(gw.getLinkName(), false, gw.toRevision(changeDetail.getChange()
-              .getProject(), ps));
-      revlink.addStyleName(Gerrit.RESOURCES.css().patchSetLink());
-      getHeader().add(revlink);
-    }
-
-    if (ps.isDraft()) {
-      final InlineLabel draftLabel = new InlineLabel(Util.C.draftPatchSetLabel());
-      draftLabel.addStyleName(Gerrit.RESOURCES.css().patchSetRevision());
-      getHeader().add(draftLabel);
-    }
-
-    if (isOpen) {
-      ensureLoaded(changeDetail.getCurrentPatchSetDetail());
-    } else {
-      addOpenHandler(this);
-    }
-
-  }
-
-  public void setDiffBaseId(PatchSet.Id diffBaseId) {
-    this.diffBaseId = diffBaseId;
-  }
-
-  /**
-   * Display the table showing the Author, Committer and Download links,
-   * followed by the action buttons.
-   */
-  public void ensureLoaded(final PatchSetDetail detail) {
-    loadInfoTable(detail);
-    loadActionPanel(detail);
-    loadPatchTable(detail);
-  }
-
-  public void loadInfoTable(final PatchSetDetail detail) {
-    infoTable = new Grid(R_CNT, 2);
-    infoTable.setStyleName(Gerrit.RESOURCES.css().infoBlock());
-    infoTable.addStyleName(Gerrit.RESOURCES.css().patchSetInfoBlock());
-
-    initRow(R_AUTHOR, Util.C.patchSetInfoAuthor());
-    initRow(R_COMMITTER, Util.C.patchSetInfoCommitter());
-    initRow(R_PARENTS, Util.C.patchSetInfoParents());
-    initRow(R_DOWNLOAD, Util.C.patchSetInfoDownload());
-
-    final CellFormatter itfmt = infoTable.getCellFormatter();
-    itfmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
-    itfmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
-    itfmt.addStyleName(R_CNT - 1, 0, Gerrit.RESOURCES.css().bottomheader());
-    itfmt.addStyleName(R_AUTHOR, 1, Gerrit.RESOURCES.css().useridentity());
-    itfmt.addStyleName(R_COMMITTER, 1, Gerrit.RESOURCES.css().useridentity());
-    itfmt.addStyleName(R_DOWNLOAD, 1, Gerrit.RESOURCES.css()
-        .downloadLinkListCell());
-
-    final PatchSetInfo info = detail.getInfo();
-    displayUserIdentity(R_AUTHOR, info.getAuthor());
-    displayUserIdentity(R_COMMITTER, info.getCommitter());
-    displayParents(info.getParents());
-    displayDownload();
-
-    body.add(infoTable);
-  }
-
-  public void loadActionPanel(final PatchSetDetail detail) {
-    if (!patchSet.getId().equals(diffBaseId)) {
-      actionsPanel = new FlowPanel();
-      actionsPanel.setStyleName(Gerrit.RESOURCES.css().patchSetActions());
-      actionsPanel.setVisible(true);
-      if (Gerrit.isSignedIn()) {
-        if (changeDetail.canEdit()) {
-          populateReviewAction();
-          if (changeDetail.isCurrentPatchSet(detail)) {
-            populateActions(detail);
-          }
-          populateCommands(detail);
-        }
-        if (detail.getPatchSet().isDraft()) {
-          if (changeDetail.canPublish()) {
-            populatePublishAction();
-          }
-          if (changeDetail.canDeleteDraft()
-              && changeDetail.getPatchSets().size() > 1) {
-            populateDeleteDraftPatchSetAction();
-          }
-        }
-      }
-      body.add(actionsPanel);
-    }
-  }
-
-  public void loadPatchTable(final PatchSetDetail detail) {
-    if (!patchSet.getId().equals(diffBaseId)) {
-      patchTable = new PatchTable();
-      patchTable.setSavePointerId("PatchTable " + patchSet.getId());
-      patchTable.display(diffBaseId, detail);
-      for (ClickHandler clickHandler : registeredClickHandler) {
-        patchTable.addClickHandler(clickHandler);
-      }
-      patchTable.setRegisterKeys(true);
-      setActive(true);
-      body.add(patchTable);
-    }
-  }
-
-  public class ChangeDownloadPanel extends DownloadPanel {
-    public ChangeDownloadPanel(String project, String ref, boolean allowAnonymous) {
-      super(project, ref, allowAnonymous);
-    }
-
-    @Override
-    public void populateDownloadCommandLinks() {
-      // This site prefers usage of the 'repo' tool, so suggest
-      // that for easy fetch.
-      //
-      if (allowedSchemes.contains(DownloadScheme.REPO_DOWNLOAD)) {
-        commands.add(cmdLinkfactory.new RepoCommandLink(projectName,
-            changeDetail.getChange().getChangeId() + "/"
-            + patchSet.getPatchSetId()));
-      }
-
-      if (!urls.isEmpty()) {
-        if (allowedCommands.contains(DownloadCommand.CHECKOUT)
-            || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
-          commands.add(cmdLinkfactory.new CheckoutCommandLink());
-        }
-        if (allowedCommands.contains(DownloadCommand.PULL)
-            || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
-          commands.add(cmdLinkfactory.new PullCommandLink());
-        }
-        if (allowedCommands.contains(DownloadCommand.CHERRY_PICK)
-            || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
-          commands.add(cmdLinkfactory.new CherryPickCommandLink());
-        }
-        if (allowedCommands.contains(DownloadCommand.FORMAT_PATCH)
-            || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
-          commands.add(cmdLinkfactory.new FormatPatchCommandLink());
-        }
-      }
-    }
-  }
-
-  private void displayDownload() {
-    ChangeDownloadPanel dp = new ChangeDownloadPanel(
-      changeDetail.getChange().getProject().get(),
-      patchSet.getRefName(),
-      changeDetail.isAllowsAnonymous());
-
-    infoTable.setWidget(R_DOWNLOAD, 1, dp);
-  }
-
-  private void displayUserIdentity(final int row, final UserIdentity who) {
-    if (who == null) {
-      infoTable.clearCell(row, 1);
-      return;
-    }
-
-    final FlowPanel fp = new FlowPanel();
-    fp.setStyleName(Gerrit.RESOURCES.css().patchSetUserIdentity());
-    if (who.getName() != null) {
-      if (who.getAccount() != null) {
-        fp.add(new AccountLinkPanel(who));
-      } else {
-        final InlineLabel lbl = new InlineLabel(who.getName());
-        lbl.setStyleName(Gerrit.RESOURCES.css().accountName());
-        fp.add(lbl);
-      }
-    }
-    if (who.getEmail() != null) {
-      fp.add(new InlineLabel("<" + who.getEmail() + ">"));
-    }
-    if (who.getDate() != null) {
-      fp.add(new InlineLabel(FormatUtil.mediumFormat(who.getDate())));
-    }
-    infoTable.setWidget(row, 1, fp);
-  }
-
-  private void displayParents(final List<PatchSetInfo.ParentInfo> parents) {
-    if (parents.size() == 0) {
-      infoTable.setWidget(R_PARENTS, 1, new InlineLabel(Util.C.initialCommit()));
-      return;
-    }
-    final Grid parentsTable = new Grid(parents.size(), 2);
-
-    parentsTable.setStyleName(Gerrit.RESOURCES.css().parentsTable());
-    parentsTable.addStyleName(Gerrit.RESOURCES.css().noborder());
-    final CellFormatter ptfmt = parentsTable.getCellFormatter();
-    int row = 0;
-    for (PatchSetInfo.ParentInfo parent : parents) {
-      parentsTable.setWidget(row, 0, new InlineLabel(parent.id.get()));
-      ptfmt.addStyleName(row, 0, Gerrit.RESOURCES.css().noborder());
-      ptfmt.addStyleName(row, 0, Gerrit.RESOURCES.css().monospace());
-      parentsTable.setWidget(row, 1,
-          new InlineLabel(Util.cropSubject(parent.shortMessage)));
-      ptfmt.addStyleName(row, 1, Gerrit.RESOURCES.css().noborder());
-      row++;
-    }
-    infoTable.setWidget(R_PARENTS, 1, parentsTable);
-  }
-
-  private void populateActions(final PatchSetDetail detail) {
-    final boolean isOpen = changeDetail.getChange().getStatus().isOpen();
-
-    if (isOpen && changeDetail.canSubmit()) {
-      final Button b =
-          new Button(Util.M
-              .submitPatchSet(detail.getPatchSet().getPatchSetId()));
-      b.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(final ClickEvent event) {
-          b.setEnabled(false);
-          ChangeApi.submit(
-              patchSet.getId().getParentKey().get(),
-              patchSet.getRevision().get(),
-              new GerritCallback<SubmitInfo>() {
-                  public void onSuccess(SubmitInfo result) {
-                    redisplay();
-                  }
-
-                  public void onFailure(Throwable err) {
-                    if (SubmitFailureDialog.isConflict(err)) {
-                      new SubmitFailureDialog(err.getMessage()).center();
-                      redisplay();
-                    } else {
-                      b.setEnabled(true);
-                      super.onFailure(err);
-                    }
-                  }
-
-                  private void redisplay() {
-                    Gerrit.display(
-                        PageLinks.toChange(patchSet.getId().getParentKey()),
-                        new ChangeScreen(patchSet.getId().getParentKey()));
-                  }
-              });
-        }
-      });
-      actionsPanel.add(b);
-    }
-
-    if (changeDetail.canRevert()) {
-      final Button b = new Button(Util.C.buttonRevertChangeBegin());
-      b.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(final ClickEvent event) {
-          b.setEnabled(false);
-          new ActionDialog(b, true, Util.C.revertChangeTitle(),
-              Util.C.headingRevertMessage()) {
-            {
-              sendButton.setText(Util.C.buttonRevertChangeSend());
-              message.setText(Util.M.revertChangeDefaultMessage(
-                  detail.getInfo().getSubject(),
-                  detail.getPatchSet().getRevision().get())
-              );
-            }
-
-            @Override
-            public void onSend() {
-              ChangeApi.revert(changeDetail.getChange().getChangeId(),
-                  getMessageText(), new GerritCallback<ChangeInfo>() {
-                    @Override
-                    public void onSuccess(ChangeInfo result) {
-                      sent = true;
-                      Gerrit.display(PageLinks.toChange(new Change.Id(result
-                          ._number())));
-                      hide();
-                    }
-
-                    @Override
-                    public void onFailure(Throwable caught) {
-                      enableButtons(true);
-                      super.onFailure(caught);
-                    }
-                  });
-            }
-          }.center();
-        }
-      });
-      actionsPanel.add(b);
-    }
-
-    if (changeDetail.canCherryPick()) {
-      final Button b = new Button(Util.C.buttonCherryPickChangeBegin());
-      b.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(final ClickEvent event) {
-          b.setEnabled(false);
-          new CherryPickDialog(b, changeDetail.getChange().getProject()) {
-            {
-              sendButton.setText(Util.C.buttonCherryPickChangeSend());
-              if (changeDetail.getChange().getStatus().isClosed()) {
-                message.setText(Util.M.cherryPickedChangeDefaultMessage(
-                    detail.getInfo().getMessage().trim(),
-                    detail.getPatchSet().getRevision().get()));
-              } else {
-                message.setText(detail.getInfo().getMessage().trim());
-              }
-            }
-
-            @Override
-            public void onSend() {
-              ChangeApi.cherrypick(changeDetail.getChange().getChangeId(),
-                  patchSet.getRevision().get(),
-                  getDestinationBranch(),
-                  getMessageText(),
-                  new GerritCallback<ChangeInfo>() {
-                    @Override
-                    public void onSuccess(ChangeInfo result) {
-                      sent = true;
-                      Gerrit.display(PageLinks.toChange(new Change.Id(result
-                          ._number())));
-                      hide();
-                    }
-
-                    @Override
-                    public void onFailure(Throwable caught) {
-                      enableButtons(true);
-                      super.onFailure(caught);
-                    }
-                  });
-            }
-          }.center();
-        }
-      });
-      actionsPanel.add(b);
-    }
-
-    if (changeDetail.canAbandon()) {
-      final Button b = new Button(Util.C.buttonAbandonChangeBegin());
-      b.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(final ClickEvent event) {
-          b.setEnabled(false);
-          new ActionDialog(b, false, Util.C.abandonChangeTitle(),
-              Util.C.headingAbandonMessage()) {
-            {
-              sendButton.setText(Util.C.buttonAbandonChangeSend());
-            }
-
-            @Override
-            public void onSend() {
-              // TODO: once the other users of ActionDialog have converted to
-              // REST APIs, we can use createCallback() rather than providing
-              // them directly.
-              ChangeApi.abandon(changeDetail.getChange().getChangeId(),
-                  getMessageText(), new GerritCallback<ChangeInfo>() {
-                    @Override
-                    public void onSuccess(ChangeInfo result) {
-                      sent = true;
-                      Gerrit.display(PageLinks.toChange(new Change.Id(result
-                          ._number())));
-                      hide();
-                    }
-
-                    @Override
-                    public void onFailure(Throwable caught) {
-                      enableButtons(true);
-                      super.onFailure(caught);
-                    }
-                  });
-            }
-          }.center();
-        }
-      });
-      actionsPanel.add(b);
-    }
-
-    if (changeDetail.getChange().getStatus() == Change.Status.DRAFT
-        && changeDetail.canDeleteDraft()) {
-      final Button b = new Button(Util.C.buttonDeleteDraftChange());
-      b.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(final ClickEvent event) {
-          b.setEnabled(false);
-          ChangeApi.deleteChange(patchSet.getId().getParentKey().get(),
-              new GerritCallback<JavaScriptObject>() {
-            public void onSuccess(JavaScriptObject result) {
-              Gerrit.display(PageLinks.MINE);
-            }
-
-            public void onFailure(Throwable err) {
-              if (SubmitFailureDialog.isConflict(err)) {
-                new SubmitFailureDialog(err.getMessage()).center();
-                Gerrit.display(PageLinks.MINE);
-              } else {
-                b.setEnabled(true);
-                super.onFailure(err);
-              }
-            }
-          });
-        }
-      });
-      actionsPanel.add(b);
-    }
-
-    if (changeDetail.canRestore()) {
-      final Button b = new Button(Util.C.buttonRestoreChangeBegin());
-      b.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(final ClickEvent event) {
-          b.setEnabled(false);
-          new ActionDialog(b, false, Util.C.restoreChangeTitle(),
-              Util.C.headingRestoreMessage()) {
-            {
-              sendButton.setText(Util.C.buttonRestoreChangeSend());
-            }
-
-            @Override
-            public void onSend() {
-              ChangeApi.restore(changeDetail.getChange().getChangeId(),
-                  getMessageText(), new GerritCallback<ChangeInfo>() {
-                    @Override
-                    public void onSuccess(ChangeInfo result) {
-                      sent = true;
-                      Gerrit.display(PageLinks.toChange(new Change.Id(result
-                          ._number())));
-                      hide();
-                    }
-
-                    @Override
-                    public void onFailure(Throwable caught) {
-                      enableButtons(true);
-                      super.onFailure(caught);
-                    }
-                  });
-            }
-          }.center();
-        }
-      });
-      actionsPanel.add(b);
-    }
-
-    if (changeDetail.canRebase()) {
-      final Button b = new Button(Util.C.buttonRebaseChange());
-      b.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(final ClickEvent event) {
-          b.setEnabled(false);
-          final Change.Id id = patchSet.getId().getParentKey();
-          ChangeApi.rebase(id.get(), patchSet.getRevision().get(),
-              new GerritCallback<ChangeInfo>() {
-                public void onSuccess(ChangeInfo result) {
-                  Gerrit.display(PageLinks.toChange(id));
-                }
-              });
-        }
-      });
-      actionsPanel.add(b);
-    }
-  }
-
-  private void populateCommands(final PatchSetDetail detail) {
-    for (final UiCommandDetail cmd : detail.getCommands()) {
-      final Button b = new Button();
-      b.setText(cmd.label);
-      b.setEnabled(cmd.enabled);
-      b.setTitle(cmd.title);
-      b.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(final ClickEvent event) {
-          b.setEnabled(false);
-          AsyncCallback<NativeString> cb =
-              new AsyncCallback<NativeString>() {
-                @Override
-                public void onFailure(Throwable caught) {
-                  b.setEnabled(true);
-                  new ErrorDialog(caught).center();
-                }
-
-                @Override
-                public void onSuccess(NativeString msg) {
-                  b.setEnabled(true);
-                  if (msg != null && !msg.asString().isEmpty()) {
-                    Window.alert(msg.asString());
-                  }
-                  Gerrit.display(PageLinks.toChange(patchSet.getId()));
-                }
-              };
-          RestApi api = ChangeApi.revision(patchSet.getId()).view(cmd.id);
-          if ("PUT".equalsIgnoreCase(cmd.method)) {
-            api.put(JavaScriptObject.createObject(), cb);
-          } else if ("DELETE".equalsIgnoreCase(cmd.method)) {
-            api.delete(cb);
-          } else {
-            api.post(JavaScriptObject.createObject(), cb);
-          }
-        }
-      });
-      actionsPanel.add(b);
-    }
-  }
-
-  private void populateReviewAction() {
-    final Button b = new Button(Util.C.buttonReview());
-    b.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        Gerrit.display(Dispatcher.toPublish(patchSet.getId()));
-      }
-    });
-    actionsPanel.add(b);
-  }
-
-  private void populatePublishAction() {
-    final Button b = new Button(Util.C.buttonPublishPatchSet());
-    b.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        b.setEnabled(false);
-        final Change.Id id = patchSet.getId().getParentKey();
-        ChangeApi.publish(id.get(),
-            patchSet.getRevision().get(),
-            DraftActions.cs(id));
-      }
-    });
-    actionsPanel.add(b);
-  }
-
-  private void populateDeleteDraftPatchSetAction() {
-    final Button b = new Button(Util.C.buttonDeleteDraftPatchSet());
-    b.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        b.setEnabled(false);
-        final Change.Id id = patchSet.getId().getParentKey();
-        ChangeApi.deleteRevision(id.get(),
-            patchSet.getRevision().get(),
-            DraftActions.cs(id));
-      }
-    });
-    actionsPanel.add(b);
-  }
-
-  public void refresh() {
-    if (patchSet.getId().equals(diffBaseId)) {
-      if (patchTable != null) {
-        patchTable.setVisible(false);
-      }
-      if (actionsPanel != null) {
-        actionsPanel.setVisible(false);
-      }
-    } else {
-      if (patchTable != null) {
-        if (patchTable.getBase() == null && diffBaseId == null
-            || patchTable.getBase() != null
-            && patchTable.getBase().equals(diffBaseId)) {
-          actionsPanel.setVisible(true);
-          patchTable.setVisible(true);
-          return;
-        }
-      }
-
-      AccountDiffPreference diffPrefs;
-      if (patchTable == null) {
-        diffPrefs = new ListenableAccountDiffPreference().get();
-      } else {
-        diffPrefs = patchTable.getPreferences().get();
-        patchTable.setVisible(false);
-      }
-
-      Util.DETAIL_SVC.patchSetDetail2(diffBaseId, patchSet.getId(), diffPrefs,
-          new GerritCallback<PatchSetDetail>() {
-            @Override
-            public void onSuccess(PatchSetDetail result) {
-              if (actionsPanel != null) {
-                actionsPanel.setVisible(true);
-              } else {
-                loadActionPanel(result);
-              }
-              loadPatchTable(result);
-            }
-          });
-    }
-  }
-
-  @Override
-  public void onOpen(final OpenEvent<DisclosurePanel> event) {
-    if (infoTable == null) {
-      AccountDiffPreference diffPrefs;
-      if (diffBaseId == null) {
-        diffPrefs = null;
-      } else {
-        diffPrefs = new ListenableAccountDiffPreference().get();
-      }
-
-      Util.DETAIL_SVC.patchSetDetail2(diffBaseId, patchSet.getId(), diffPrefs,
-          new GerritCallback<PatchSetDetail>() {
-            public void onSuccess(final PatchSetDetail result) {
-              loadInfoTable(result);
-              loadActionPanel(result);
-            }
-          });
-    }
-  }
-
-  private void initRow(final int row, final String name) {
-    infoTable.setText(row, 0, name);
-    infoTable.getCellFormatter().addStyleName(row, 0,
-        Gerrit.RESOURCES.css().header());
-  }
-
-  public PatchSet getPatchSet() {
-    return patchSet;
-  }
-
-  /**
-   * Adds a click handler to the patch table.
-   * If the patch table is not yet initialized it is guaranteed that the click handler
-   * is added to the patch table after initialization.
-   */
-  public void addClickHandler(final ClickHandler clickHandler) {
-    registeredClickHandler.add(clickHandler);
-    if (patchTable != null) {
-      patchTable.addClickHandler(clickHandler);
-    }
-  }
-
-  /** Activates / Deactivates the key navigation and the highlighting of the current row for the patch table */
-  public void setActive(boolean active) {
-    if (patchTable != null) {
-      patchTable.setActive(active);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
deleted file mode 100644
index 01c8fdb..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
+++ /dev/null
@@ -1,272 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.ChangeDetail;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.logical.shared.OpenEvent;
-import com.google.gwt.event.logical.shared.OpenHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.DisclosurePanel;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.globalkey.client.KeyCommand;
-import com.google.gwtexpui.globalkey.client.KeyCommandSet;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Composite that displays the patch sets of a change. This composite ensures
- * that keyboard navigation to each changed file in all patch sets is possible.
- */
-public class PatchSetsBlock extends Composite {
-  private final Map<PatchSet.Id, PatchSetComplexDisclosurePanel> patchSetPanels =
-      new HashMap<>();
-
-  private final FlowPanel body;
-  private HandlerRegistration regNavigation;
-
-  private List<PatchSetComplexDisclosurePanel> patchSetPanelsList;
-
-  /**
-   * the patch set id of the patch set for which is the keyboard navigation is
-   * currently enabled
-   */
-  private PatchSet.Id activePatchSetId;
-
-  /** the patch set id of the current (latest) patch set */
-  private PatchSet.Id currentPatchSetId;
-
-  /** Patch sets on this change, in order. */
-  private List<PatchSet> patchSets;
-
-  PatchSetsBlock() {
-    body = new FlowPanel();
-    initWidget(body);
-  }
-
-  /** Adds UI elements for each patch set of the given change to this composite. */
-  public void display(final ChangeDetail detail, final PatchSet.Id diffBaseId) {
-    clear();
-
-    final PatchSet currps = detail.getCurrentPatchSet();
-    currentPatchSetId = currps.getId();
-    patchSets = detail.getPatchSets();
-
-    if (Gerrit.isSignedIn()) {
-      final AccountGeneralPreferences p =
-          Gerrit.getUserAccount().getGeneralPreferences();
-      if (p.isReversePatchSetOrder()) {
-        Collections.reverse(patchSets);
-      }
-    }
-
-    patchSetPanelsList = new ArrayList<>();
-
-    for (final PatchSet ps : patchSets) {
-      final PatchSetComplexDisclosurePanel p =
-          new PatchSetComplexDisclosurePanel(ps, ps == currps,
-              detail.hasDraftComments(ps.getId()));
-      if (diffBaseId != null) {
-        p.setDiffBaseId(diffBaseId);
-        if (ps == currps) {
-          p.refresh();
-        }
-      }
-      add(p);
-      patchSetPanelsList.add(p);
-    }
-  }
-
-  private void clear() {
-    setRegisterKeys(false);
-    body.clear();
-    patchSetPanels.clear();
-  }
-
-  public void refresh(final PatchSet.Id diffBaseId) {
-    if (patchSetPanelsList != null) {
-      for (final PatchSetComplexDisclosurePanel p : patchSetPanelsList) {
-        p.setDiffBaseId(diffBaseId);
-        if (p.isOpen()) {
-          p.refresh();
-        }
-      }
-    }
-  }
-
-  /**
-   * Adds the given patch set panel to this composite and ensures that handler
-   * to activate / deactivate keyboard navigation for the patch set panel are
-   * registered.
-   */
-  private void add(final PatchSetComplexDisclosurePanel patchSetPanel) {
-    body.add(patchSetPanel);
-
-    final PatchSet.Id id = patchSetPanel.getPatchSet().getId();
-    ActivationHandler activationHandler = new ActivationHandler(id);
-    patchSetPanel.addOpenHandler(activationHandler);
-    patchSetPanel.addClickHandler(activationHandler);
-    patchSetPanels.put(id, patchSetPanel);
-  }
-
-  public void setRegisterKeys(final boolean on) {
-    if (on) {
-      KeyCommandSet keysNavigation =
-          new KeyCommandSet(Gerrit.C.sectionNavigation());
-      keysNavigation.add(new PreviousPatchSetKeyCommand(0, 'p', Util.C
-          .previousPatchSet()));
-      keysNavigation.add(new NextPatchSetKeyCommand(0, 'n', Util.C
-          .nextPatchSet()));
-      regNavigation = GlobalKey.add(this, keysNavigation);
-      if (activePatchSetId != null) {
-        activate(activePatchSetId);
-      } else {
-        activate(currentPatchSetId);
-      }
-    } else {
-      if (regNavigation != null) {
-        regNavigation.removeHandler();
-        regNavigation = null;
-      }
-      deactivate();
-    }
-  }
-
-  @Override
-  protected void onUnload() {
-    setRegisterKeys(false);
-    super.onUnload();
-  }
-
-  /**
-   * Activates keyboard navigation for the patch set panel that displays the
-   * patch set with the given patch set id.
-   * The keyboard navigation for the previously active patch set panel is
-   * automatically deactivated.
-   * This method also ensures that the current row is only highlighted in the
-   * table of the active patch set panel.
-   */
-  public void activate(final PatchSet.Id patchSetId) {
-    if (indexOf(patchSetId) != -1) {
-      if (!patchSetId.equals(activePatchSetId)) {
-        deactivate();
-        PatchSetComplexDisclosurePanel patchSetPanel =
-            patchSetPanels.get(patchSetId);
-        patchSetPanel.setActive(true);
-        patchSetPanel.setOpen(true);
-        activePatchSetId = patchSetId;
-      }
-    } else {
-      Gerrit.display(PageLinks.toChange(patchSetId.getParentKey()));
-    }
-  }
-
-  /** Deactivates the keyboard navigation for the currently active patch set panel. */
-  private void deactivate() {
-    if (activePatchSetId != null) {
-      PatchSetComplexDisclosurePanel patchSetPanel =
-          patchSetPanels.get(activePatchSetId);
-      patchSetPanel.setActive(false);
-      activePatchSetId = null;
-    }
-  }
-
-  public PatchSet getCurrentPatchSet() {
-    PatchSetComplexDisclosurePanel patchSetPanel =
-        patchSetPanels.get(currentPatchSetId);
-    if (patchSetPanel != null) {
-      return patchSetPanel.getPatchSet();
-    } else {
-      return null;
-    }
-  }
-
-  private int indexOf(PatchSet.Id id) {
-    for (int i = 0; i < patchSets.size(); i++) {
-      if (patchSets.get(i).getId().equals(id)) {
-        return i;
-      }
-    }
-    return -1;
-  }
-
-  private class ActivationHandler implements OpenHandler<DisclosurePanel>,
-      ClickHandler {
-
-    private final PatchSet.Id patchSetId;
-
-    ActivationHandler(PatchSet.Id patchSetId) {
-      this.patchSetId = patchSetId;
-    }
-
-    @Override
-    public void onOpen(OpenEvent<DisclosurePanel> event) {
-      // when a patch set panel is opened by the user
-      // it should automatically become active
-      PatchSetComplexDisclosurePanel patchSetPanel =
-          patchSetPanels.get(patchSetId);
-      patchSetPanel.refresh();
-      activate(patchSetId);
-    }
-
-    @Override
-    public void onClick(ClickEvent event) {
-      // when a user clicks on a patch table the corresponding
-      // patch set panel should automatically become active
-      activate(patchSetId);
-    }
-
-  }
-
-  public class PreviousPatchSetKeyCommand extends KeyCommand {
-    public PreviousPatchSetKeyCommand(int mask, char key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-      int index = indexOf(activePatchSetId) - 1;
-      if (0 <= index) {
-        activate(patchSets.get(index).getId());
-      }
-    }
-  }
-
-  public class NextPatchSetKeyCommand extends KeyCommand {
-    public NextPatchSetKeyCommand(int mask, char key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-      int index = indexOf(activePatchSetId) + 1;
-      if (index < patchSets.size()) {
-        activate(patchSets.get(index).getId());
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
deleted file mode 100644
index f718b5d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
+++ /dev/null
@@ -1,523 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
-import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
-import com.google.gerrit.client.patches.AbstractPatchContentTable;
-import com.google.gerrit.client.patches.CommentEditorContainer;
-import com.google.gerrit.client.patches.CommentEditorPanel;
-import com.google.gerrit.client.projects.ConfigInfoCache;
-import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.AccountScreen;
-import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.client.ui.PatchLink;
-import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.ChangeDetail;
-import com.google.gerrit.common.data.SubmitTypeRecord;
-import com.google.gerrit.extensions.common.ListChangesOption;
-import com.google.gerrit.extensions.common.SubmitType;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.FormPanel;
-import com.google.gwt.user.client.ui.FormPanel.SubmitEvent;
-import com.google.gwt.user.client.ui.Panel;
-import com.google.gwt.user.client.ui.RadioButton;
-import com.google.gwt.user.client.ui.VerticalPanel;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.NpTextArea;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import com.google.gwtjsonrpc.common.VoidResult;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class PublishCommentScreen extends AccountScreen implements
-    ClickHandler, CommentEditorContainer {
-  private static SavedState lastState;
-
-  private final PatchSet.Id patchSetId;
-  private Collection<ValueRadioButton> approvalButtons;
-  private ChangeDescriptionBlock descBlock;
-  private ApprovalTable approvals;
-  private Panel approvalPanel;
-  private NpTextArea message;
-  private FlowPanel draftsPanel;
-  private Button send;
-  private Button cancel;
-  private boolean saveStateOnUnload = true;
-  private List<CommentEditorPanel> commentEditors;
-  private ChangeInfo change;
-  private ChangeInfo detail;
-  private NativeMap<JsArray<CommentInfo>> drafts;
-  private SubmitTypeRecord submitTypeRecord;
-  private CommentLinkProcessor commentLinkProcessor;
-
-  public PublishCommentScreen(final PatchSet.Id psi) {
-    patchSetId = psi;
-  }
-
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    addStyleName(Gerrit.RESOURCES.css().publishCommentsScreen());
-
-    approvalButtons = new ArrayList<>();
-    descBlock = new ChangeDescriptionBlock(null);
-    add(descBlock);
-
-    approvals = new ApprovalTable();
-    add(approvals);
-
-    final FormPanel form = new FormPanel();
-    final FlowPanel body = new FlowPanel();
-    form.setWidget(body);
-    form.addSubmitHandler(new FormPanel.SubmitHandler() {
-      @Override
-      public void onSubmit(final SubmitEvent event) {
-        event.cancel();
-      }
-    });
-    add(form);
-
-    approvalPanel = new FlowPanel();
-    body.add(approvalPanel);
-    initMessage(body);
-
-    draftsPanel = new FlowPanel();
-    body.add(draftsPanel);
-
-    final FlowPanel buttonRow = new FlowPanel();
-    buttonRow.setStyleName(Gerrit.RESOURCES.css().patchSetActions());
-    body.add(buttonRow);
-
-    send = new Button(Util.C.buttonPublishCommentsSend());
-    send.addClickHandler(this);
-    buttonRow.add(send);
-
-    cancel = new Button(Util.C.buttonPublishCommentsCancel());
-    cancel.addClickHandler(this);
-    buttonRow.add(cancel);
-  }
-
-  private void enableForm(final boolean enabled) {
-    for (final ValueRadioButton approvalButton : approvalButtons) {
-      approvalButton.setEnabled(enabled);
-    }
-    message.setEnabled(enabled);
-    for (final CommentEditorPanel commentEditor : commentEditors) {
-      commentEditor.enableButtons(enabled);
-    }
-    send.setEnabled(enabled);
-    cancel.setEnabled(enabled);
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-
-    CallbackGroup group = new CallbackGroup();
-    RestApi call = ChangeApi.detail(patchSetId.getParentKey().get());
-    ChangeList.addOptions(call, EnumSet.of(
-      ListChangesOption.CURRENT_ACTIONS,
-      ListChangesOption.ALL_REVISIONS,
-      ListChangesOption.ALL_COMMITS));
-    call.get(group.add(new GerritCallback<ChangeInfo>() {
-        @Override
-        public void onSuccess(ChangeInfo result) {
-          detail = result;
-        }
-      }));
-    ChangeApi.revision(patchSetId)
-      .view("submit_type")
-      .get(group.add(new GerritCallback<NativeString>() {
-        @Override
-        public void onSuccess(NativeString result) {
-          submitTypeRecord = SubmitTypeRecord.OK(
-              SubmitType.valueOf(result.asString()));
-        }
-        public void onFailure(Throwable caught) {}
-      }));
-    ChangeApi.revision(patchSetId.getParentKey().get(), "" + patchSetId.get())
-      .view("drafts")
-      .get(group.add(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
-        @Override
-        public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
-          drafts = result;
-        }
-        public void onFailure(Throwable caught) {}
-      }));
-    ChangeApi.revision(patchSetId).view("review")
-      .get(group.addFinal(new GerritCallback<ChangeInfo>() {
-        @Override
-        public void onSuccess(ChangeInfo result) {
-          result.init();
-          change = result;
-          preDisplay(result);
-        }
-      }));
-  }
-
-  private void preDisplay(final ChangeInfo info) {
-    ConfigInfoCache.get(info.project_name_key(),
-        new ScreenLoadCallback<ConfigInfoCache.Entry>(this) {
-          @Override
-          protected void preDisplay(ConfigInfoCache.Entry result) {
-            send.setEnabled(true);
-            commentLinkProcessor = result.getCommentLinkProcessor();
-            setTheme(result.getTheme());
-            displayScreen();
-          }
-
-          @Override
-          protected void postDisplay() {
-            message.setFocus(true);
-          }
-        });
-  }
-
-  @Override
-  protected void onUnload() {
-    super.onUnload();
-    lastState = saveStateOnUnload ? new SavedState(this) : null;
-  }
-
-  @Override
-  public void onClick(final ClickEvent event) {
-    final Widget sender = (Widget) event.getSource();
-    if (send == sender) {
-      onSend(false);
-    } else if (cancel == sender) {
-      saveStateOnUnload = false;
-      goChange();
-    }
-  }
-
-  @Override
-  public void notifyDraftDelta(int delta) {
-  }
-
-  @Override
-  public void remove(CommentEditorPanel editor) {
-    commentEditors.remove(editor);
-
-    // The editor should be embedded into a panel holding all
-    // editors for the same file.
-    //
-    FlowPanel parent = (FlowPanel) editor.getParent();
-    parent.remove(editor);
-
-    // If the panel now holds no editors, remove it.
-    //
-    int editorCount = 0;
-    for (Widget w : parent) {
-      if (w instanceof CommentEditorPanel) {
-        editorCount++;
-      }
-    }
-    if (editorCount == 0) {
-      parent.removeFromParent();
-    }
-
-    // If that was the last file with a draft, remove the heading.
-    //
-    if (draftsPanel.getWidgetCount() == 1) {
-      draftsPanel.clear();
-    }
-  }
-
-  private void initMessage(final Panel body) {
-    body.add(new SmallHeading(Util.C.headingCoverMessage()));
-
-    final VerticalPanel mwrap = new VerticalPanel();
-    mwrap.setStyleName(Gerrit.RESOURCES.css().coverMessage());
-    body.add(mwrap);
-
-    message = new NpTextArea();
-    message.setCharacterWidth(60);
-    message.setVisibleLines(10);
-    message.setSpellCheck(true);
-    mwrap.add(message);
-  }
-
-  private void initApprovals(Panel body) {
-    for (String labelName : change.labels()) {
-      initLabel(labelName, body);
-    }
-  }
-
-  private void initLabel(String labelName, Panel body) {
-    if (!change.has_permitted_labels()) {
-      return;
-    }
-    JsArrayString nativeValues = change.permitted_values(labelName);
-    if (nativeValues == null || nativeValues.length() == 0) {
-      return;
-    }
-    List<String> values = new ArrayList<>(nativeValues.length());
-    for (int i = 0; i < nativeValues.length(); i++) {
-      values.add(nativeValues.get(i));
-    }
-    Collections.reverse(values);
-    LabelInfo label = change.label(labelName);
-
-    body.add(new SmallHeading(label.name() + ":"));
-
-    VerticalPanel vp = new VerticalPanel();
-    vp.setStyleName(Gerrit.RESOURCES.css().labelList());
-
-    Short prior = null;
-    if (label.all() != null) {
-      for (ApprovalInfo app : Natives.asList(label.all())) {
-        if (app._account_id() == Gerrit.getUserAccount().getId().get()) {
-          prior = app.value();
-          break;
-        }
-      }
-    }
-
-    for (String value : values) {
-      ValueRadioButton b = new ValueRadioButton(label, value);
-      SafeHtml buf = new SafeHtmlBuilder().append(b.format());
-      buf = commentLinkProcessor.apply(buf);
-      SafeHtml.set(b, buf);
-
-      if (lastState != null && patchSetId.equals(lastState.patchSetId)
-          && lastState.approvals.containsKey(label.name())) {
-        b.setValue(lastState.approvals.get(label.name()).equals(value));
-      } else {
-        b.setValue(b.parseValue() == (prior != null ? prior : 0));
-      }
-
-      approvalButtons.add(b);
-      vp.add(b);
-    }
-    body.add(vp);
-  }
-
-  private void displayScreen() {
-    ChangeDetail r = ChangeDetailCache.reverse(detail);
-
-    setPageTitle(Util.M.publishComments(r.getChange().getKey().abbreviate(),
-        patchSetId.get()));
-    descBlock.display(r, null, false, r.getCurrentPatchSetDetail().getInfo(),
-        r.getAccounts(), submitTypeRecord, commentLinkProcessor);
-
-    if (r.getChange().getStatus().isOpen()) {
-      initApprovals(approvalPanel);
-      approvals.display(change);
-    } else {
-      approvals.setVisible(false);
-    }
-
-    if (lastState != null && patchSetId.equals(lastState.patchSetId)) {
-      message.setText(lastState.message);
-    }
-
-    draftsPanel.clear();
-    commentEditors = new ArrayList<>();
-
-    if (!drafts.isEmpty()) {
-      draftsPanel.add(new SmallHeading(Util.C.headingPatchComments()));
-
-      Panel panel = null;
-      String priorFile = "";
-      for (final PatchLineComment c : draftList()) {
-        final Patch.Key patchKey = c.getKey().getParentKey();
-        final String fn = patchKey.get();
-        if (!fn.equals(priorFile)) {
-          panel = new FlowPanel();
-          panel.addStyleName(Gerrit.RESOURCES.css().patchComments());
-          draftsPanel.add(panel);
-          // Parent table can be null here since we are not showing any
-          // next/previous links
-          panel.add(new PatchLink.SideBySide(
-              PatchTable.getDisplayFileName(patchKey), null, patchKey, 0, null, null));
-          priorFile = fn;
-        }
-
-        final CommentEditorPanel editor =
-            new CommentEditorPanel(c, commentLinkProcessor);
-        if (c.getLine() == AbstractPatchContentTable.R_HEAD) {
-          editor.setAuthorNameText(Gerrit.getUserAccountInfo(),
-              Util.C.fileCommentHeader());
-        } else {
-          editor.setAuthorNameText(Gerrit.getUserAccountInfo(),
-              Util.M.lineHeader(c.getLine()));
-        }
-        editor.setOpen(true);
-        commentEditors.add(editor);
-        panel.add(editor);
-      }
-    }
-  }
-
-  private void onSend(final boolean submit) {
-    if (commentEditors.isEmpty()) {
-      onSend2(submit);
-    } else {
-      final GerritCallback<VoidResult> afterSaveDraft =
-          new GerritCallback<VoidResult>() {
-            private int done;
-
-            @Override
-            public void onSuccess(final VoidResult result) {
-              if (++done == commentEditors.size()) {
-                onSend2(submit);
-              }
-            }
-          };
-      for (final CommentEditorPanel p : commentEditors) {
-        p.saveDraft(afterSaveDraft);
-      }
-    }
-  }
-
-  private void onSend2(final boolean submit) {
-    ReviewInput data = ReviewInput.create();
-    data.message(ChangeApi.emptyToNull(message.getText().trim()));
-    for (final ValueRadioButton b : approvalButtons) {
-      if (b.getValue()) {
-        data.label(b.label.name(), b.parseValue());
-      }
-    }
-
-    enableForm(false);
-    new RestApi("/changes/")
-      .id(String.valueOf(patchSetId.getParentKey().get()))
-      .view("revisions").id(patchSetId.get()).view("review")
-      .post(data, new GerritCallback<ReviewInput>() {
-          @Override
-          public void onSuccess(ReviewInput result) {
-            if (submit) {
-              submit();
-            } else {
-              saveStateOnUnload = false;
-              goChange();
-            }
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            super.onFailure(caught);
-            enableForm(true);
-          }
-        });
-  }
-
-  private void submit() {
-    ChangeApi.submit(
-      patchSetId.getParentKey().get(),
-      "" + patchSetId.get(),
-      new GerritCallback<SubmitInfo>() {
-          public void onSuccess(SubmitInfo result) {
-            saveStateOnUnload = false;
-            goChange();
-          }
-
-          @Override
-          public void onFailure(Throwable err) {
-            if (SubmitFailureDialog.isConflict(err)) {
-              new SubmitFailureDialog(err.getMessage()).center();
-            } else {
-              super.onFailure(err);
-            }
-            goChange();
-          }
-        });
-  }
-
-  private void goChange() {
-    final Change.Id ck = patchSetId.getParentKey();
-    Gerrit.display(PageLinks.toChange(ck), new ChangeScreen(ck));
-  }
-
-  private List<PatchLineComment> draftList() {
-    List<PatchLineComment> d = new ArrayList<>();
-    List<String> paths = new ArrayList<>(drafts.keySet());
-    Collections.sort(paths);
-    for (String path : paths) {
-      JsArray<CommentInfo> comments = drafts.get(path);
-      for (int i = 0; i < comments.length(); i++) {
-        d.add(CommentEditorPanel.toComment(patchSetId, path, comments.get(i)));
-      }
-    }
-    return d;
-  }
-
-  private static class ValueRadioButton extends RadioButton {
-    final LabelInfo label;
-    final String value;
-
-    ValueRadioButton(LabelInfo label, String value) {
-      super(label.name());
-      this.label = label;
-      this.value = value;
-    }
-
-    String format() {
-      return new StringBuilder().append(value).append(' ')
-          .append(label.value_text(value)).toString();
-    }
-
-    short parseValue() {
-      String value = this.value;
-      if (value.startsWith(" ") || value.startsWith("+")) {
-        value = value.substring(1);
-      }
-      return Short.parseShort(value);
-    }
-  }
-
-  private static class SavedState {
-    final PatchSet.Id patchSetId;
-    final String message;
-    final Map<String, String> approvals;
-
-    SavedState(final PublishCommentScreen p) {
-      patchSetId = p.patchSetId;
-      message = p.message.getText();
-      approvals = new HashMap<>();
-      for (final ValueRadioButton b : p.approvalButtons) {
-        if (b.getValue()) {
-          approvals.put(b.label.name(), b.value);
-        }
-      }
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
index 9c16330..d34492c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
@@ -14,27 +14,17 @@
 
 package com.google.gerrit.client.changes;
 
-import com.google.gerrit.common.data.ChangeDetailService;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.GWT;
-import com.google.gwtjsonrpc.client.JsonUtil;
 
 public class Util {
   public static final ChangeConstants C = GWT.create(ChangeConstants.class);
   public static final ChangeMessages M = GWT.create(ChangeMessages.class);
-  public static final ChangeResources R = GWT.create(ChangeResources.class);
-
-  public static final ChangeDetailService DETAIL_SVC;
 
   private static final int SUBJECT_MAX_LENGTH = 80;
   private static final String SUBJECT_CROP_APPENDIX = "...";
   private static final int SUBJECT_CROP_RANGE = 10;
 
-  static {
-    DETAIL_SVC = GWT.create(ChangeDetailService.class);
-    JsonUtil.bind(DETAIL_SVC, "rpc/ChangeDetailService");
-  }
-
   public static String toLongString(final Change.Status status) {
     if (status == null) {
       return "";
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/removeReviewerPressed.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/removeReviewerPressed.png
deleted file mode 100644
index e46f0aa..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/removeReviewerPressed.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
index bc13ac6..4420940 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
@@ -16,9 +16,6 @@
 
 import static com.google.gerrit.client.diff.DisplaySide.A;
 import static com.google.gerrit.client.diff.DisplaySide.B;
-import static com.google.gerrit.client.diff.OverviewBar.MarkType.DELETE;
-import static com.google.gerrit.client.diff.OverviewBar.MarkType.EDIT;
-import static com.google.gerrit.client.diff.OverviewBar.MarkType.INSERT;
 
 import com.google.gerrit.client.diff.DiffInfo.Region;
 import com.google.gerrit.client.diff.DiffInfo.Span;
@@ -35,8 +32,8 @@
 import net.codemirror.lib.CodeMirror;
 import net.codemirror.lib.CodeMirror.LineClassWhere;
 import net.codemirror.lib.Configuration;
-import net.codemirror.lib.LineCharacter;
 import net.codemirror.lib.LineWidget;
+import net.codemirror.lib.Pos;
 import net.codemirror.lib.TextMarker;
 
 import java.util.ArrayList;
@@ -44,7 +41,7 @@
 import java.util.Comparator;
 import java.util.List;
 
-/** Colors modified regions for {@link SideBySide2}. */
+/** Colors modified regions for {@link SideBySide}. */
 class ChunkManager {
   private static final String DATA_LINES = "_cs2h";
   private static double guessedLineHeightPx = 15;
@@ -62,12 +59,12 @@
     Element e = Element.as(event.getEventTarget());
     for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
       EventListener l = DOM.getEventListener(e);
-      if (l instanceof SideBySide2) {
-        ((SideBySide2) l).getCmFromSide(side).focus();
+      if (l instanceof SideBySide) {
+        ((SideBySide) l).getCmFromSide(side).focus();
         event.stopPropagation();
       }
     }
-  };
+  }
 
   static void focusOnClick(Element e, DisplaySide side) {
     onClick(e, side == A ? focusA : focusB);
@@ -76,10 +73,10 @@
   private static final native void onClick(Element e, JavaScriptObject f)
   /*-{ e.onclick = f }-*/;
 
-  private final SideBySide2 host;
+  private final SideBySide host;
   private final CodeMirror cmA;
   private final CodeMirror cmB;
-  private final OverviewBar sidePanel;
+  private final Scrollbar scrollbar;
   private final LineMapper mapper;
 
   private List<DiffChunkInfo> chunks;
@@ -88,14 +85,14 @@
   private List<LineWidget> padding;
   private List<Element> paddingDivs;
 
-  ChunkManager(SideBySide2 host,
+  ChunkManager(SideBySide host,
       CodeMirror cmA,
       CodeMirror cmB,
-      OverviewBar sidePanel) {
+      Scrollbar scrollbar) {
     this.host = host;
     this.cmA = cmA;
     this.cmB = cmB;
-    this.sidePanel = sidePanel;
+    this.scrollbar = scrollbar;
     this.mapper = new LineMapper();
   }
 
@@ -150,7 +147,7 @@
 
   void adjustPadding() {
     if (paddingDivs != null) {
-      double h = host.getLineHeightPx();
+      double h = cmB.extras().lineHeightPx();
       for (Element div : paddingDivs) {
         int lines = div.getPropertyInt(DATA_LINES);
         div.getStyle().setHeight(lines * h, Unit.PX);
@@ -188,20 +185,20 @@
     int endA = mapper.getLineA() - 1;
     int endB = mapper.getLineB() - 1;
     if (aLen > 0) {
-      addDiffChunk(cmB, endB, endA, aLen, bLen > 0);
+      addDiffChunk(cmB, endA, aLen, bLen > 0);
     }
     if (bLen > 0) {
-      addDiffChunk(cmA, endA, endB, bLen, aLen > 0);
+      addDiffChunk(cmA, endB, bLen, aLen > 0);
     }
   }
 
   private void addGutterTag(Region region, int startA, int startB) {
     if (region.a() == null) {
-      sidePanel.add(cmB, startB, region.b().length(), INSERT);
+      scrollbar.insert(cmB, startB, region.b().length());
     } else if (region.b() == null) {
-      sidePanel.add(cmA, startA, region.a().length(), DELETE);
+      scrollbar.delete(cmA, cmB, startA, region.a().length());
     } else {
-      sidePanel.add(cmB, startB, region.b().length(), EDIT);
+      scrollbar.edit(cmB, startB, region.b().length());
     }
   }
 
@@ -220,20 +217,20 @@
         .set("className", DiffTable.style.diff())
         .set("readOnly", true);
 
-    LineCharacter last = CodeMirror.pos(0, 0);
+    Pos last = Pos.create(0, 0);
     for (Span span : Natives.asList(edits)) {
-      LineCharacter from = iter.advance(span.skip());
-      LineCharacter to = iter.advance(span.mark());
-      if (from.getLine() == last.getLine()) {
+      Pos from = iter.advance(span.skip());
+      Pos to = iter.advance(span.mark());
+      if (from.line() == last.line()) {
         markers.add(cm.markText(last, from, bg));
       } else {
-        markers.add(cm.markText(CodeMirror.pos(from.getLine(), 0), from, bg));
+        markers.add(cm.markText(Pos.create(from.line(), 0), from, bg));
       }
       markers.add(cm.markText(from, to, diff));
       last = to;
       colorLines(cm, LineClassWhere.BACKGROUND,
           DiffTable.style.diff(),
-          from.getLine(), to.getLine());
+          from.line(), to.line());
     }
   }
 
@@ -284,8 +281,8 @@
     }
   }
 
-  private void addDiffChunk(CodeMirror cmToPad, int lineToPad,
-      int lineOnOther, int chunkSize, boolean edit) {
+  private void addDiffChunk(CodeMirror cmToPad, int lineOnOther,
+      int chunkSize, boolean edit) {
     chunks.add(new DiffChunkInfo(host.otherCm(cmToPad).side(),
         lineOnOther - chunkSize + 1, lineOnOther, edit));
   }
@@ -294,7 +291,9 @@
     return new Runnable() {
       @Override
       public void run() {
-        int line = cm.hasActiveLine() ? cm.getLineNumber(cm.getActiveLine()) : 0;
+        int line = cm.extras().hasActiveLine()
+            ? cm.getLineNumber(cm.extras().activeLine())
+            : 0;
         int res = Collections.binarySearch(
                 chunks,
                 new DiffChunkInfo(cm.side(), line, 0, false),
@@ -318,11 +317,11 @@
 
         DiffChunkInfo target = chunks.get(res);
         CodeMirror targetCm = host.getCmFromSide(target.getSide());
-        targetCm.setCursor(LineCharacter.create(target.getStart()));
+        targetCm.setCursor(Pos.create(target.getStart()));
         targetCm.focus();
         targetCm.scrollToY(
             targetCm.heightAtLine(target.getStart(), "local") -
-            0.5 * cmB.getScrollbarV().getClientHeight());
+            0.5 * cmB.scrollbarV().getClientHeight());
       }
     };
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.css
similarity index 96%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.css
index da0754b..441af22 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.css
@@ -123,15 +123,15 @@
   color: #fff;
 }
 
-@sprite .go_prev {
-  gwt-image: "go_prev";
+@sprite .goPrev {
+  gwt-image: "goPrev";
   display: inline-block;
 }
-@sprite .go_next {
-  gwt-image: "go_next";
+@sprite .goNext {
+  gwt-image: "goNext";
   display: inline-block;
 }
-@sprite .go_up {
-  gwt-image: "go_up";
+@sprite .goUp {
+  gwt-image: "goUp";
   display: inline-block;
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
index 1d74520..ca56bf4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
@@ -19,6 +19,7 @@
 import com.google.gwt.event.dom.client.MouseOutHandler;
 import com.google.gwt.event.dom.client.MouseOverEvent;
 import com.google.gwt.event.dom.client.MouseOverHandler;
+import com.google.gwt.resources.client.CssResource;
 import com.google.gwt.user.client.ui.Composite;
 
 import net.codemirror.lib.CodeMirror;
@@ -32,8 +33,22 @@
     Resources.I.style().ensureInjected();
   }
 
+  interface Style extends CssResource {
+    String commentWidgets();
+    String commentBox();
+    String contents();
+    String message();
+    String header();
+    String summary();
+    String date();
+
+    String goPrev();
+    String goNext();
+    String goUp();
+  }
+
   private final CommentGroup group;
-  private OverviewBar.MarkHandle mark;
+  private ScrollbarAnnotation annotation;
   private FromTo fromTo;
   private TextMarker rangeMarker;
   private TextMarker rangeHighlightMarker;
@@ -43,8 +58,8 @@
     if (range != null) {
       fromTo = FromTo.create(range);
       rangeMarker = group.getCm().markText(
-          fromTo.getFrom(),
-          fromTo.getTo(),
+          fromTo.from(),
+          fromTo.to(),
           Configuration.create()
               .set("className", DiffTable.style.range()));
     }
@@ -79,20 +94,20 @@
     return group.getCommentManager();
   }
 
-  OverviewBar.MarkHandle getMark() {
-    return mark;
+  ScrollbarAnnotation getAnnotation() {
+    return annotation;
   }
 
-  void setMark(OverviewBar.MarkHandle mh) {
-    mark = mh;
+  void setAnnotation(ScrollbarAnnotation mh) {
+    annotation = mh;
   }
 
   void setRangeHighlight(boolean highlight) {
     if (fromTo != null) {
       if (highlight && rangeHighlightMarker == null) {
         rangeHighlightMarker = group.getCm().markText(
-            fromTo.getFrom(),
-            fromTo.getTo(),
+            fromTo.from(),
+            fromTo.to(),
             Configuration.create()
                 .set("className", DiffTable.style.rangeHighlight()));
       } else if (!highlight && rangeHighlightMarker != null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java
index 7bcf4da..a8a56fc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java
@@ -218,8 +218,8 @@
   private void updateSelection() {
     if (cm.somethingSelected()) {
       FromTo r = cm.getSelectedRange();
-      if (r.getTo().getLine() >= line) {
-        cm.setSelection(r.getFrom(), r.getTo());
+      if (r.to().line() >= line) {
+        cm.setSelection(r.from(), r.to());
       }
     }
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
index 5d5662b..514a3be 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
@@ -20,13 +20,13 @@
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
-import com.google.gerrit.common.changes.Side;
+import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.JsArray;
 
 import net.codemirror.lib.CodeMirror;
 import net.codemirror.lib.CodeMirror.LineHandle;
-import net.codemirror.lib.LineCharacter;
+import net.codemirror.lib.Pos;
 import net.codemirror.lib.TextMarker.FromTo;
 
 import java.util.ArrayList;
@@ -38,9 +38,9 @@
 import java.util.SortedMap;
 import java.util.TreeMap;
 
-/** Tracks comment widgets for {@link SideBySide2}. */
+/** Tracks comment widgets for {@link SideBySide}. */
 class CommentManager {
-  private final SideBySide2 host;
+  private final SideBySide host;
   private final PatchSet.Id base;
   private final PatchSet.Id revision;
   private final String path;
@@ -52,16 +52,19 @@
   private final Set<DraftBox> unsavedDrafts;
   private boolean attached;
   private boolean expandAll;
+  private boolean open;
 
-  CommentManager(SideBySide2 host,
+  CommentManager(SideBySide host,
       PatchSet.Id base, PatchSet.Id revision,
       String path,
-      CommentLinkProcessor clp) {
+      CommentLinkProcessor clp,
+      boolean open) {
     this.host = host;
     this.base = base;
     this.revision = revision;
     this.path = path;
     this.commentLinkProcessor = clp;
+    this.open = open;
 
     published = new HashMap<>();
     sideA = new TreeMap<>();
@@ -69,7 +72,7 @@
     unsavedDrafts = new HashSet<>();
   }
 
-  SideBySide2 getSideBySide2() {
+  SideBySide getSideBySide() {
     return host;
   }
 
@@ -91,8 +94,8 @@
         // It is only necessary to search one side to find a comment
         // on either side of the editor pair.
         SortedMap<Integer, CommentGroup> map = map(src.side());
-        int line = src.hasActiveLine()
-            ? src.getLineNumber(src.getActiveLine()) + 1
+        int line = src.extras().hasActiveLine()
+            ? src.getLineNumber(src.extras().activeLine()) + 1
             : 0;
         if (dir == Direction.NEXT) {
           map = map.tailMap(line + 1);
@@ -115,8 +118,8 @@
 
         CodeMirror cm = g.getCm();
         double y = cm.heightAtLine(g.getLine() - 1, "local");
-        cm.setCursor(LineCharacter.create(g.getLine() - 1));
-        cm.scrollToY(y - 0.5 * cm.getScrollbarV().getClientHeight());
+        cm.setCursor(Pos.create(g.getLine() - 1));
+        cm.scrollToY(y - 0.5 * cm.scrollbarV().getClientHeight());
         cm.focus();
       }
     };
@@ -157,12 +160,12 @@
             group,
             commentLinkProcessor,
             getPatchSetIdFromSide(side),
-            info);
+            info,
+            open);
         group.add(box);
-        box.setMark(host.diffTable.overview.add(
+        box.setAnnotation(host.diffTable.scrollbar.comment(
             host.getCmFromSide(side),
-            Math.max(0, info.line() - 1), 1,
-            OverviewBar.MarkType.COMMENT));
+            Math.max(0, info.line() - 1)));
         published.put(info.id(), box);
       }
     }
@@ -223,10 +226,9 @@
     }
 
     group.add(box);
-    box.setMark(host.diffTable.overview.add(
+    box.setAnnotation(host.diffTable.scrollbar.draft(
         host.getCmFromSide(side),
-        Math.max(0, info.line() - 1), 1,
-        OverviewBar.MarkType.DRAFT));
+        Math.max(0, info.line() - 1)));
     return box;
   }
 
@@ -295,10 +297,11 @@
 
   Runnable toggleOpenBox(final CodeMirror cm) {
     return new Runnable() {
+      @Override
       public void run() {
-        if (cm.hasActiveLine()) {
+        if (cm.extras().hasActiveLine()) {
           CommentGroup w = map(cm.side()).get(
-              cm.getLineNumber(cm.getActiveLine()) + 1);
+              cm.getLineNumber(cm.extras().activeLine()) + 1);
           if (w != null) {
             w.openCloseLast();
           }
@@ -311,9 +314,9 @@
     return new Runnable() {
       @Override
       public void run() {
-        if (cm.hasActiveLine()) {
+        if (cm.extras().hasActiveLine()) {
           CommentGroup w = map(cm.side()).get(
-              cm.getLineNumber(cm.getActiveLine()) + 1);
+              cm.getLineNumber(cm.extras().activeLine()) + 1);
           if (w != null) {
             w.openCloseAll();
           }
@@ -328,8 +331,8 @@
         @Override
         public void run() {
           String token = host.getToken();
-          if (cm.hasActiveLine()) {
-            LineHandle handle = cm.getActiveLine();
+          if (cm.extras().hasActiveLine()) {
+            LineHandle handle = cm.extras().activeLine();
             int line = cm.getLineNumber(handle) + 1;
             token += "@" + (cm.side() == DisplaySide.A ? "a" : "") + line;
           }
@@ -339,22 +342,22 @@
     }
 
     return new Runnable() {
+      @Override
       public void run() {
-        if (cm.hasActiveLine()) {
-          newDraft(cm);
+        if (cm.extras().hasActiveLine()) {
+          newDraft(cm, cm.getLineNumber(cm.extras().activeLine()) + 1);
         }
       }
     };
   }
 
-  private void newDraft(CodeMirror cm) {
-    int line = cm.getLineNumber(cm.getActiveLine()) + 1;
+  void newDraft(CodeMirror cm, int line) {
     if (cm.somethingSelected()) {
       FromTo fromTo = cm.getSelectedRange();
-      LineCharacter end = fromTo.getTo();
-      if (end.getCh() == 0) {
-        end.setLine(end.getLine() - 1);
-        end.setCh(cm.getLine(end.getLine()).length());
+      Pos end = fromTo.to();
+      if (end.ch() == 0) {
+        end.line(end.line() - 1);
+        end.ch(cm.getLine(end.line()).length());
       }
 
       addDraftBox(cm.side(), CommentInfo.create(
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
index 9887dbf..a38a6ca 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
@@ -16,7 +16,7 @@
 
 import com.google.gwt.core.client.JavaScriptObject;
 
-import net.codemirror.lib.LineCharacter;
+import net.codemirror.lib.Pos;
 import net.codemirror.lib.TextMarker.FromTo;
 
 public class CommentRange extends JavaScriptObject {
@@ -31,11 +31,11 @@
       return null;
     }
 
-    LineCharacter from = fromTo.getFrom();
-    LineCharacter to = fromTo.getTo();
+    Pos from = fromTo.from();
+    Pos to = fromTo.to();
     return create(
-        from.getLine() + 1, from.getCh(),
-        to.getLine() + 1, to.getCh());
+        from.line() + 1, from.ch(),
+        to.line() + 1, to.ch());
   }
 
   public final native int start_line() /*-{ return this.start_line; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
index 1fe85b3..b23a8cf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentsCollections.java
@@ -18,11 +18,11 @@
 import com.google.gerrit.client.changes.CommentApi;
 import com.google.gerrit.client.changes.CommentInfo;
 import com.google.gerrit.client.rpc.CallbackGroup;
-import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 
 import java.util.Collections;
 import java.util.Comparator;
@@ -53,39 +53,55 @@
     }
   }
 
-  private GerritCallback<NativeMap<JsArray<CommentInfo>>> publishedBase() {
-    return new GerritCallback<NativeMap<JsArray<CommentInfo>>>() {
+  private AsyncCallback<NativeMap<JsArray<CommentInfo>>> publishedBase() {
+    return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
       @Override
       public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
         publishedBase = sort(result.get(path));
       }
+
+      @Override
+      public void onFailure(Throwable caught) {
+      }
     };
   }
 
-  private GerritCallback<NativeMap<JsArray<CommentInfo>>> publishedRevision() {
-    return new GerritCallback<NativeMap<JsArray<CommentInfo>>>() {
+  private AsyncCallback<NativeMap<JsArray<CommentInfo>>> publishedRevision() {
+    return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
       @Override
       public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
         publishedRevision = sort(result.get(path));
       }
-    };
-  }
 
-  private GerritCallback<NativeMap<JsArray<CommentInfo>>> draftsBase() {
-    return new GerritCallback<NativeMap<JsArray<CommentInfo>>>() {
       @Override
-      public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
-        draftsBase = sort(result.get(path));
+      public void onFailure(Throwable caught) {
       }
     };
   }
 
-  private GerritCallback<NativeMap<JsArray<CommentInfo>>> draftsRevision() {
-    return new GerritCallback<NativeMap<JsArray<CommentInfo>>>() {
+  private AsyncCallback<NativeMap<JsArray<CommentInfo>>> draftsBase() {
+    return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
+      @Override
+      public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
+        draftsBase = sort(result.get(path));
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+      }
+    };
+  }
+
+  private AsyncCallback<NativeMap<JsArray<CommentInfo>>> draftsRevision() {
+    return new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
       @Override
       public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
         draftsRevision = sort(result.get(path));
       }
+
+      @Override
+      public void onFailure(Throwable caught) {
+      }
     };
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
index 5e88ac9..18d673a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
@@ -63,6 +63,11 @@
     return this;
   }
 
+  public DiffApi webLinksOnly() {
+    call.addParameterTrue("weblinks-only");
+    return this;
+  }
+
   public DiffApi ignoreWhitespace(AccountDiffPreference.Whitespace w) {
     switch (w) {
       default:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
index c0d0532..7140e07 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
@@ -14,11 +14,18 @@
 
 package com.google.gerrit.client.diff;
 
+import com.google.gerrit.client.DiffWebLinkInfo;
+import com.google.gerrit.client.WebLinkInfo;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.core.client.JsArrayString;
 
+import java.util.LinkedList;
+import java.util.List;
+
 public class DiffInfo extends JavaScriptObject {
   public static final String GITLINK = "x-git/gitlink";
   public static final String SYMLINK = "x-git/symlink";
@@ -27,6 +34,34 @@
   public final native FileMeta meta_b() /*-{ return this.meta_b; }-*/;
   public final native JsArrayString diff_header() /*-{ return this.diff_header; }-*/;
   public final native JsArray<Region> content() /*-{ return this.content; }-*/;
+  public final native JsArray<DiffWebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
+  public final native boolean binary() /*-{ return this.binary || false; }-*/;
+
+  public final List<WebLinkInfo> side_by_side_web_links() {
+    return filterWebLinks(DiffView.SIDE_BY_SIDE);
+  }
+
+  public final List<WebLinkInfo> unified_web_links() {
+    return filterWebLinks(DiffView.UNIFIED_DIFF);
+  }
+
+  private final List<WebLinkInfo> filterWebLinks(DiffView diffView) {
+    List<WebLinkInfo> filteredDiffWebLinks = new LinkedList<>();
+    List<DiffWebLinkInfo> allDiffWebLinks = Natives.asList(web_links());
+    if (allDiffWebLinks != null) {
+      for (DiffWebLinkInfo webLink : allDiffWebLinks) {
+        if (diffView == DiffView.SIDE_BY_SIDE
+            && webLink.showOnSideBySideDiffView()) {
+          filteredDiffWebLinks.add(webLink);
+        }
+        if (diffView == DiffView.UNIFIED_DIFF
+            && webLink.showOnUnifiedDiffView()) {
+          filteredDiffWebLinks.add(webLink);
+        }
+      }
+    }
+    return filteredDiffWebLinks;
+  }
 
   public final ChangeType change_type() {
     return ChangeType.valueOf(change_typeRaw());
@@ -100,6 +135,7 @@
     public final native String name() /*-{ return this.name; }-*/;
     public final native String content_type() /*-{ return this.content_type; }-*/;
     public final native int lines() /*-{ return this.lines || 0 }-*/;
+    public final native JsArray<WebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
 
     protected FileMeta() {
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
index d56654f..1105476 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
@@ -48,20 +48,17 @@
     String dark();
     String diff();
     String noIntraline();
-    String activeLine();
     String range();
     String rangeHighlight();
-    String showTabs();
     String showLineNumbers();
     String hideA();
     String hideB();
-    String columnMargin();
     String padding();
   }
 
   @UiField Element cmA;
   @UiField Element cmB;
-  @UiField OverviewBar overview;
+  Scrollbar scrollbar;
   @UiField Element patchSetNavRow;
   @UiField Element patchSetNavCellA;
   @UiField Element patchSetNavCellB;
@@ -71,28 +68,27 @@
   @UiField static DiffTableStyle style;
 
   @UiField(provided = true)
-  PatchSetSelectBox2 patchSetSelectBoxA;
+  PatchSetSelectBox patchSetSelectBoxA;
 
   @UiField(provided = true)
-  PatchSetSelectBox2 patchSetSelectBoxB;
+  PatchSetSelectBox patchSetSelectBoxB;
 
-  private SideBySide2 parent;
+  private SideBySide parent;
   private boolean header;
-  private boolean headerVisible;
   private boolean visibleA;
   private ChangeType changeType;
 
-  DiffTable(SideBySide2 parent, PatchSet.Id base, PatchSet.Id revision,
+  DiffTable(SideBySide parent, PatchSet.Id base, PatchSet.Id revision,
       String path) {
-    patchSetSelectBoxA = new PatchSetSelectBox2(
+    patchSetSelectBoxA = new PatchSetSelectBox(
         parent, DisplaySide.A, revision.getParentKey(), base, path);
-    patchSetSelectBoxB = new PatchSetSelectBox2(
+    patchSetSelectBoxB = new PatchSetSelectBox(
         parent, DisplaySide.B, revision.getParentKey(), revision, path);
-    PatchSetSelectBox2.link(patchSetSelectBoxA, patchSetSelectBoxB);
+    PatchSetSelectBox.link(patchSetSelectBoxA, patchSetSelectBoxB);
 
     initWidget(uiBinder.createAndBindUi(this));
+    this.scrollbar = new Scrollbar(this);
     this.parent = parent;
-    this.headerVisible = true;
     this.visibleA = true;
   }
 
@@ -128,20 +124,17 @@
     }
   }
 
-  boolean isHeaderVisible() {
-    return headerVisible;
-  }
-
   void setHeaderVisible(boolean show) {
-    headerVisible = show;
-    UIObject.setVisible(patchSetNavRow, show);
-    UIObject.setVisible(diffHeaderRow, show && header);
-    if (show) {
-      parent.header.removeStyleName(style.fullscreen());
-    } else {
-      parent.header.addStyleName(style.fullscreen());
+    if (show != UIObject.isVisible(patchSetNavRow)) {
+      UIObject.setVisible(patchSetNavRow, show);
+      UIObject.setVisible(diffHeaderRow, show && header);
+      if (show) {
+        parent.header.removeStyleName(style.fullscreen());
+      } else {
+        parent.header.addStyleName(style.fullscreen());
+      }
+      parent.resizeCodeMirror();
     }
-    parent.resizeCodeMirror();
   }
 
   int getHeaderHeight() {
@@ -156,10 +149,13 @@
     return changeType;
   }
 
-  void set(DiffPreferences prefs, JsArray<RevisionInfo> list, DiffInfo info) {
+  void set(DiffPreferences prefs, JsArray<RevisionInfo> list, DiffInfo info,
+      boolean editExists, int currentPatchSet, boolean open, boolean binary) {
     this.changeType = info.change_type();
-    patchSetSelectBoxA.setUpPatchSetNav(list, info.meta_a());
-    patchSetSelectBoxB.setUpPatchSetNav(list, info.meta_b());
+    patchSetSelectBoxA.setUpPatchSetNav(list, info.meta_a(), editExists,
+        currentPatchSet, open, binary);
+    patchSetSelectBoxB.setUpPatchSetNav(list, info.meta_b(), editExists,
+        currentPatchSet, open, binary);
 
     JsArrayString hdr = info.diff_header();
     if (hdr != null) {
@@ -195,7 +191,6 @@
   }
 
   void refresh() {
-    overview.refresh();
     if (header) {
       CodeMirror cm = parent.getCmFromSide(DisplaySide.A);
       diffHeaderText.getStyle().setMarginLeft(
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
index e08a25d..60e8e0d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
@@ -18,11 +18,11 @@
     xmlns:g='urn:import:com.google.gwt.user.client.ui'
     xmlns:d='urn:import:com.google.gerrit.client.diff'>
   <ui:style type='com.google.gerrit.client.diff.DiffTable.DiffTableStyle'>
-    @external .CodeMirror, .CodeMirror-lines, .CodeMirror-selectedtext;
-    @external .CodeMirror-linenumber, .CodeMirror-vscrollbar .CodeMirror-scroll;
+    @external .CodeMirror, .CodeMirror-selectedtext;
+    @external .CodeMirror-linenumber;
+    @external .CodeMirror-overlayscroll-vertical, .CodeMirror-scroll;
     @external .CodeMirror-dialog-bottom;
-    @external .cm-keymap-fat-cursor, CodeMirror-cursor;
-    @external .cm-searching, .cm-trailingspace, .cm-tab;
+    @external .cm-animate-fat-cursor, .CodeMirror-cursor;
 
     .fullscreen {
       background-color: #f7f7f7;
@@ -38,13 +38,10 @@
       -ms-user-select: none;
     }
 
-    .difftable .CodeMirror-lines { padding: 0; }
     .difftable .CodeMirror pre {
-      padding: 0;
       overflow: hidden;
       border-right: 0;
       width: auto;
-      line-height: normal;
     }
 
     /* Preserve space for underscores. If this changes
@@ -75,14 +72,9 @@
     .hideA .psNavB, .hideA .b { width: 100% }
     .hideB .psNavA, .hideB .a { width: 100% }
 
-    .overview {
-      width: 10px;
-      vertical-align: top;
-    }
-
-    /* Hide scrollbars, OverviewBar controls both views. */
-    .difftable .CodeMirror-scroll { padding-right: 0; }
-    .difftable .CodeMirror-vscrollbar { display: none !important; }
+    /* Hide scrollbars on A, B controls both views. */
+    .a .CodeMirror-scroll { margin-right: -36px; }
+    .a .CodeMirror-overlayscroll-vertical { display: none !important; }
 
     .showLineNumbers .b { border-left: none; }
     .b { border-left: 1px solid #ddd; }
@@ -110,23 +102,12 @@
       overflow-x: auto;
     }
 
-    .activeLine .CodeMirror-linenumber {
-      background-color: #bcf !important;
-      color: #000;
-    }
-
     .range {
       background-color: #ffd500 !important;
     }
     .rangeHighlight {
       background-color: #ffff00 !important;
     }
-    .cm-searching {
-      background-color: #ffa !important;
-    }
-    .cm-trailingspace {
-      background-color: red !important;
-    }
     .difftable .CodeMirror-selectedtext {
       background-color: inherit !important;
     }
@@ -134,11 +115,20 @@
       height: 1.11em;
       cursor: pointer;
     }
-    .difftable .CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor {
+    .difftable .CodeMirror div.CodeMirror-cursor {
       background: transparent;
       text-decoration: underline;
+      border: none;
       z-index: 2;
     }
+    .difftable .cm-animate-fat-cursor {
+      text-decoration: underline;
+      border: none;
+      animation: none;
+      -webkit-animation: none;
+      -moz-animation: none;
+      -o-animation: none;
+    }
     .difftable .CodeMirror-dialog-bottom {
       border-top: 0;
       border-left: 1px solid #000;
@@ -149,24 +139,10 @@
       bottom: auto;
       left: auto;
     }
-    .showTabs .cm-tab:before {
-      position: absolute;
-      content: "\00bb";
-      color: #f00;
-    }
     .showLineNumbers .padding {
       margin-left: 21px;
       border-left: 2px solid #d64040;
     }
-    .columnMargin {
-      position: absolute;
-      top: 0;
-      bottom: 0;
-      width: 0;
-      border-right: 1px dashed #ffa500;
-      z-index: 2;
-      cursor: text;
-    }
 
     .diff_header {
       font-size: 12px;
@@ -181,21 +157,18 @@
     <table class='{style.table}'>
       <tr ui:field='patchSetNavRow' class='{style.patchSetNav}'>
         <td ui:field='patchSetNavCellA' class='{style.psNavA}'>
-          <d:PatchSetSelectBox2 ui:field='patchSetSelectBoxA' />
+          <d:PatchSetSelectBox ui:field='patchSetSelectBoxA' />
         </td>
         <td ui:field='patchSetNavCellB' class='{style.psNavB}'>
-          <d:PatchSetSelectBox2 ui:field='patchSetSelectBoxB' />
+          <d:PatchSetSelectBox ui:field='patchSetSelectBoxB' />
         </td>
-        <td class='{style.overview}' />
       </tr>
       <tr ui:field='diffHeaderRow' class='{style.diff_header}'>
         <td colspan='2'><pre ui:field='diffHeaderText' /></td>
-        <td class='{style.overview}' />
       </tr>
       <tr>
         <td ui:field='cmA' class='{style.a}' />
         <td ui:field='cmB' class='{style.b}' />
-        <td class='{style.overview}'><d:OverviewBar ui:field='overview'/></td>
       </tr>
     </table>
     <g:FlowPanel ui:field='widgets' visible='false'/>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
index 6b0d69a..5bf56c2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
@@ -49,6 +49,8 @@
 import com.google.gwtexpui.globalkey.client.NpTextArea;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
+import net.codemirror.lib.CodeMirror;
+
 /** An HtmlPanel for displaying and editing a draft */
 class DraftBox extends CommentBox {
   interface Binder extends UiBinder<HTMLPanel, DraftBox> {}
@@ -236,14 +238,14 @@
     getCommentManager().setUnsaved(this, false);
     setRangeHighlight(false);
     clearRange();
-    getMark().remove();
+    getAnnotation().remove();
     getCommentGroup().remove(this);
     getCm().focus();
   }
 
   private void restoreSelection() {
     if (getFromTo() != null && comment.in_reply_to() == null) {
-      getCm().setSelection(getFromTo().getFrom(), getFromTo().getTo());
+      getCm().setSelection(getFromTo().from(), getFromTo().to());
     }
   }
 
@@ -253,7 +255,7 @@
   }
 
   @UiHandler("message")
-  void onMessageDoubleClick(DoubleClickEvent e) {
+  void onMessageDoubleClick(@SuppressWarnings("unused") DoubleClickEvent e) {
     setEdit(true);
   }
 
@@ -312,7 +314,9 @@
     } else {
       CommentApi.updateDraft(psId, input.id(), input, group.add(cb));
     }
-    getCm().focus();
+    CodeMirror cm = getCm();
+    cm.vim().handleKey("<Esc>");
+    cm.focus();
   }
 
   private void enableEdit(boolean on) {
@@ -389,7 +393,7 @@
   }
 
   @UiHandler("editArea")
-  void onBlur(BlurEvent e) {
+  void onBlur(@SuppressWarnings("unused") BlurEvent e) {
     resizeTimer.cancel();
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/EditIterator.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/EditIterator.java
index 4c8d80e..ce3a562 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/EditIterator.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/EditIterator.java
@@ -16,7 +16,7 @@
 
 import com.google.gwt.core.client.JsArrayString;
 
-import net.codemirror.lib.LineCharacter;
+import net.codemirror.lib.Pos;
 
 /** An iterator for intraline edits */
 class EditIterator {
@@ -30,13 +30,13 @@
     startLine = start;
   }
 
-  LineCharacter advance(int numOfChar) {
+  Pos advance(int numOfChar) {
     numOfChar = adjustForNegativeDelta(numOfChar);
 
     while (line < lines.length()) {
       int len = lines.get(line).length() - pos + 1; // + 1 for LF
       if (numOfChar < len) {
-        LineCharacter at = LineCharacter.create(
+        Pos at = Pos.create(
             startLine + line,
             numOfChar + pos);
         pos += numOfChar;
@@ -48,7 +48,7 @@
       pos = 0;
 
       if (numOfChar == 0) {
-        return LineCharacter.create(startLine + line, 0);
+        return Pos.create(startLine + line, 0);
       }
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java
index dc52aa3..c4459b6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java
@@ -43,6 +43,20 @@
         } else if (Patch.COMMIT_MSG.equals(b.path())) {
           return 1;
         }
+        // Look at file suffixes to check if it makes sense to use a different order
+        int s1 = a.path().lastIndexOf('.');
+        int s2 = b.path().lastIndexOf('.');
+        if (s1 > 0 && s2 > 0 &&
+            a.path().substring(0, s1).equals(b.path().substring(0, s2))) {
+            String suffixA = a.path().substring(s1);
+            String suffixB = b.path().substring(s2);
+            // C++ and C: give priority to header files (.h/.hpp/...)
+            if (suffixA.indexOf(".h") == 0) {
+                return -1;
+            } else if (suffixB.indexOf(".h") == 0) {
+                return 1;
+            }
+        }
         return a.path().compareTo(b.path());
       }
     });
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 2b3e2be..2c551c0 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
@@ -17,6 +17,7 @@
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.GitwebLink;
+import com.google.gerrit.client.WebLinkInfo;
 import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
@@ -47,6 +48,7 @@
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTMLPanel;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.UIObject;
@@ -55,7 +57,9 @@
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
-class Header extends Composite {
+import java.util.List;
+
+public class Header extends Composite {
   interface Binder extends UiBinder<HTMLPanel, Header> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
   static {
@@ -71,6 +75,7 @@
   @UiField Element filePath;
 
   @UiField Element noDiff;
+  @UiField FlowPanel linkPanel;
 
   @UiField InlineHyperlink prev;
   @UiField InlineHyperlink up;
@@ -101,11 +106,10 @@
     SafeHtml.setInnerHTML(filePath, formatPath(path, null, null));
     up.setTargetHistoryToken(PageLinks.toChange(
         patchSetId.getParentKey(),
-        base != null ? String.valueOf(base.get()) : null,
-        String.valueOf(patchSetId.get())));
+        base != null ? base.getId() : null, patchSetId.getId()));
   }
 
-  private static SafeHtml formatPath(String path, String project, String commit) {
+  public static SafeHtml formatPath(String path, String project, String commit) {
     SafeHtmlBuilder b = new SafeHtmlBuilder();
     if (Patch.COMMIT_MSG.equals(path)) {
       return b.append(Util.C.commitMessage());
@@ -191,7 +195,7 @@
     GitwebLink gw = Gerrit.getGitwebLink();
     if (gw != null) {
       for (RevisionInfo rev : Natives.asList(info.revisions().values())) {
-        if (rev._number() == patchSetId.get()) {
+        if (patchSetId.getId().equals(rev.id())) {
           String c = rev.name();
           SafeHtml.setInnerHTML(filePath, formatPath(path, info.project(), c));
           SafeHtml.setInnerHTML(project, new SafeHtmlBuilder()
@@ -208,9 +212,17 @@
     project.setInnerText(info.project());
   }
 
-  void init(PreferencesAction pa) {
+  void init(PreferencesAction pa, List<InlineHyperlink> links,
+      List<WebLinkInfo> webLinks) {
     prefsAction = pa;
     prefsAction.setPartner(preferences);
+
+    for (InlineHyperlink link : links) {
+      linkPanel.add(link);
+    }
+    for (WebLinkInfo webLink : webLinks) {
+      linkPanel.add(webLink.toAnchor());
+    }
   }
 
   @UiHandler("reviewed")
@@ -243,7 +255,7 @@
   }
 
   @UiHandler("preferences")
-  void onPreferences(ClickEvent e) {
+  void onPreferences(@SuppressWarnings("unused") ClickEvent e) {
     prefsAction.show();
   }
 
@@ -275,13 +287,14 @@
       return k;
     } else {
       link.getElement().getStyle().setVisibility(Visibility.HIDDEN);
-      keys.add(new UpToChangeCommand2(patchSetId, 0, key));
+      keys.add(new UpToChangeCommand(patchSetId, 0, key));
       return null;
     }
   }
 
   Runnable toggleReviewed() {
     return new Runnable() {
+      @Override
       public void run() {
         reviewed.setValue(!reviewed.getValue(), true);
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
index 7d1f1e5..c58eef7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
@@ -18,6 +18,7 @@
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'
     xmlns:x='urn:import:com.google.gerrit.client.ui'>
+  <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
   <ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
   <ui:style>
   .header {
@@ -46,6 +47,13 @@
     vertical-align: top;
     font-weight: bold;
     margin-right: 1em;
+    float: left;
+  }
+  .linkPanel {
+    float: left;
+  }
+  .linkPanel img {
+    padding-right: 3px;
   }
   .preferences {
     cursor: pointer;
@@ -60,16 +68,17 @@
     <span class='{style.path}'><span ui:field='project'/> / <span ui:field='filePath'/></span>
     <div class='{style.navigation}'>
       <span ui:field='noDiff' class='{style.nodiff}'><ui:msg>No Differences</ui:msg></span>
-      <x:InlineHyperlink ui:field='prev' styleName='{res.style.go_prev}'/>
+      <g:FlowPanel ui:field='linkPanel' styleName='{style.linkPanel}'/>
+      <x:InlineHyperlink ui:field='prev' styleName='{res.style.goPrev}'/>
       <x:InlineHyperlink ui:field='up'
-          styleName='{res.style.go_up}'
+          styleName='{res.style.goUp}'
           title='Up to change (Shortcut: u)'>
         <ui:attribute name='title'/>
       </x:InlineHyperlink>
-      <x:InlineHyperlink ui:field='next' styleName='{res.style.go_next}'/>
+      <x:InlineHyperlink ui:field='next' styleName='{res.style.goNext}'/>
       <g:Image ui:field='preferences'
            styleName='{style.preferences}'
-           resource='{res.gear}'
+           resource='{ico.gear}'
            title='Diff preferences (Shortcut: ,)'>
          <ui:attribute name='title'/>
       </g:Image>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
index 6c7423c..9ec760c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
@@ -17,6 +17,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
 /** Helper class to handle calculations involving line gaps. */
 class LineMapper {
@@ -193,6 +194,11 @@
     }
 
     @Override
+    public int hashCode() {
+      return Objects.hash(line, aligned);
+    }
+
+    @Override
     public String toString() {
       return line + " " + aligned;
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/OverviewBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/OverviewBar.java
deleted file mode 100644
index b9e6375..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/OverviewBar.java
+++ /dev/null
@@ -1,266 +0,0 @@
-//Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.diff;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.Style.Unit;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.MouseDownEvent;
-import com.google.gwt.event.dom.client.MouseMoveEvent;
-import com.google.gwt.event.dom.client.MouseUpEvent;
-import com.google.gwt.resources.client.CssResource;
-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.DOM;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.Widget;
-
-import net.codemirror.lib.CodeMirror;
-import net.codemirror.lib.LineCharacter;
-import net.codemirror.lib.ScrollInfo;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-/** Displays overview of all edits and comments in this file. */
-class OverviewBar extends Composite implements ClickHandler {
-  interface Binder extends UiBinder<HTMLPanel, OverviewBar> {}
-  private static final Binder uiBinder = GWT.create(Binder.class);
-
-  interface Style extends CssResource {
-    String gutter();
-    String halfGutter();
-    String comment();
-    String draft();
-    String insert();
-    String delete();
-    String viewportDrag();
-  }
-
-  enum MarkType {
-    COMMENT, DRAFT, INSERT, DELETE, EDIT
-  }
-
-  @UiField Style style;
-  @UiField Label viewport;
-
-  private final List<MarkHandle> diff;
-  private final Set<MarkHandle> comments;
-  private CodeMirror cmB;
-
-  private boolean dragging;
-  private int startY;
-  private double ratio;
-
-  OverviewBar() {
-    initWidget(uiBinder.createAndBindUi(this));
-    diff = new ArrayList<>();
-    comments = new HashSet<>();
-    addDomHandler(this, ClickEvent.getType());
-  }
-
-  void init(CodeMirror cmB) {
-    this.cmB = cmB;
-  }
-
-  void refresh() {
-    update(cmB.getScrollInfo());
-  }
-
-  void update(ScrollInfo si) {
-    double viewHeight = si.getClientHeight();
-    double r = ratio(si);
-
-    com.google.gwt.dom.client.Style style = viewport.getElement().getStyle();
-    style.setTop(si.getTop() * r, Unit.PX);
-    style.setHeight(Math.max(10, viewHeight * r), Unit.PX);
-    getElement().getStyle().setHeight(viewHeight, Unit.PX);
-    for (MarkHandle info : diff) {
-      info.position(r);
-    }
-    for (MarkHandle info : comments) {
-      info.position(r);
-    }
-  }
-
-  @Override
-  protected void onUnload() {
-    super.onUnload();
-    if (dragging) {
-      DOM.releaseCapture(viewport.getElement());
-    }
-  }
-
-  @Override
-  public void onClick(ClickEvent e) {
-    if (e.getY() < viewport.getElement().getOffsetTop()) {
-      CodeMirror.handleVimKey(cmB, "<PageUp>");
-    } else {
-      CodeMirror.handleVimKey(cmB, "<PageDown>");
-    }
-    cmB.focus();
-  }
-
-  @UiHandler("viewport")
-  void onMouseDown(MouseDownEvent e) {
-    if (cmB != null) {
-      dragging = true;
-      ratio = ratio(cmB.getScrollInfo());
-      startY = e.getY();
-      viewport.addStyleName(style.viewportDrag());
-      DOM.setCapture(viewport.getElement());
-      e.preventDefault();
-      e.stopPropagation();
-    }
-  }
-
-  @UiHandler("viewport")
-  void onMouseMove(MouseMoveEvent e) {
-    if (dragging) {
-      int y = e.getRelativeY(getElement()) - startY;
-      cmB.scrollToY(Math.max(0, y / ratio));
-      e.preventDefault();
-      e.stopPropagation();
-    }
-  }
-
-  @UiHandler("viewport")
-  void onMouseUp(MouseUpEvent e) {
-    if (dragging) {
-      dragging = false;
-      DOM.releaseCapture(viewport.getElement());
-      viewport.removeStyleName(style.viewportDrag());
-      e.preventDefault();
-      e.stopPropagation();
-    }
-  }
-
-  private double ratio(ScrollInfo si) {
-    double barHeight = si.getClientHeight();
-    double contentHeight = si.getHeight();
-    return barHeight / contentHeight;
-  }
-
-  MarkHandle add(CodeMirror cm, int line, int height, MarkType type) {
-    MarkHandle mark = new MarkHandle(cm, line, height);
-    switch (type) {
-      case COMMENT:
-        mark.addStyleName(style.comment());
-        comments.add(mark);
-        break;
-      case DRAFT:
-        mark.addStyleName(style.draft());
-        mark.getElement().setInnerText("*");
-        comments.add(mark);
-        break;
-      case INSERT:
-        mark.addStyleName(style.insert());
-        diff.add(mark);
-        break;
-      case DELETE:
-        mark.addStyleName(style.delete());
-        diff.add(mark);
-        break;
-      case EDIT:
-        mark.edit = DOM.createDiv();
-        mark.edit.setClassName(style.halfGutter());
-        mark.getElement().appendChild(mark.edit);
-        mark.addStyleName(style.insert());
-        diff.add(mark);
-        break;
-    }
-    if (cmB != null) {
-      mark.position(ratio(cmB.getScrollInfo()));
-    }
-    ((HTMLPanel) getWidget()).add(mark);
-    return mark;
-  }
-
-  void clearDiffMarkers() {
-    for (MarkHandle mark : diff) {
-      mark.removeFromParent();
-    }
-    diff.clear();
-  }
-
-  class MarkHandle extends Widget implements ClickHandler {
-    private static final int MIN_HEIGHT = 3;
-
-    private final CodeMirror cm;
-    private final int line;
-    private final int height;
-    private Element edit;
-
-    MarkHandle(CodeMirror cm, int line, int height) {
-      this.cm = cm;
-      this.line = line;
-      this.height = height;
-
-      setElement((Element)(DOM.createDiv()));
-      setStyleName(style.gutter());
-      addDomHandler(this, ClickEvent.getType());
-    }
-
-    void position(double ratio) {
-      double y = cm.heightAtLine(line, "local");
-      getElement().getStyle().setTop(y * ratio, Unit.PX);
-      if (height > 1) {
-        double e = cm.heightAtLine(line + height, "local");
-        double h = Math.max(MIN_HEIGHT, (e - y) * ratio);
-        getElement().getStyle().setHeight(h, Unit.PX);
-        if (edit != null) {
-          edit.getStyle().setHeight(h, Unit.PX);
-        }
-      }
-    }
-
-    @Override
-    public void onClick(ClickEvent e) {
-      if (height == 1 || !visible()) {
-        e.stopPropagation();
-
-        double y = cm.heightAtLine(line, "local");
-        double viewport = cm.getScrollInfo().getClientHeight();
-        cm.setCursor(LineCharacter.create(line));
-        cm.scrollToY(y - 0.5 * viewport);
-        cm.focus();
-      }
-    }
-
-    private boolean visible() {
-      int markT = getElement().getOffsetTop();
-      int markE = markT + getElement().getOffsetHeight();
-
-      int viewT = viewport.getElement().getOffsetTop();
-      int viewE = viewT + viewport.getElement().getOffsetHeight();
-
-      return (viewT <= markT && markT < viewE) // mark top within viewport
-          || (viewT <= markE && markE < viewE) // mark end within viewport
-          || (markT <= viewT && viewE <= markE); // mark contains viewport
-    }
-
-    void remove() {
-      removeFromParent();
-      comments.remove(this);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/OverviewBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/OverviewBar.ui.xml
deleted file mode 100644
index a56c3da..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/OverviewBar.ui.xml
+++ /dev/null
@@ -1,71 +0,0 @@
-<?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:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style type='com.google.gerrit.client.diff.OverviewBar.Style'>
-    .overview {
-      position: relative;
-    }
-    .gutter {
-      cursor: pointer;
-      position: absolute;
-      height: 3px;
-      width: 6px;
-      border: 1px solid grey;
-      margin-left: 1px;
-    }
-    .halfGutter {
-      cursor: pointer;
-      position: absolute;
-      height: 3px;
-      width: 3px;
-      background-color: #faa;
-    }
-    .comment, .draft {
-      background-color: #fcfa96;
-      z-index: 2;
-    }
-    .draft {
-      text-align: center;
-      font-size: small;
-      line-height: 0.5;
-      color: inherit !important;
-      text-decoration: none !important;
-    }
-    .delete {
-      background-color: #faa;
-    }
-    .insert {
-      background-color: #9f9;
-    }
-    .viewport {
-      position: absolute;
-      background-color: rgba(128, 128, 128, 0.50);
-      border: 1px solid #444;
-      width: 8px;
-      cursor: default;
-      z-index: 3;
-      border-radius: 4px;
-    }
-    .viewportDrag {
-      cursor: move;
-    }
-  </ui:style>
-  <g:HTMLPanel styleName='{style.overview}'>
-    <g:Label ui:field='viewport' styleName='{style.viewport}'/>
-  </g:HTMLPanel>
-</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
similarity index 73%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
index 7154c9f..aed2218 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
@@ -16,8 +16,10 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.WebLinkInfo;
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
@@ -34,11 +36,14 @@
 import com.google.gwt.user.client.ui.HTMLPanel;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.ImageResourceRenderer;
+import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtorm.client.KeyUtil;
 
+import java.util.List;
+
 /** HTMLPanel to select among patch sets */
-class PatchSetSelectBox2 extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, PatchSetSelectBox2> {}
+class PatchSetSelectBox extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, PatchSetSelectBox> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
   interface BoxStyle extends CssResource {
@@ -49,16 +54,16 @@
   @UiField HTMLPanel linkPanel;
   @UiField BoxStyle style;
 
-  private SideBySide2 parent;
+  private SideBySide parent;
   private DisplaySide side;
   private boolean sideA;
   private String path;
   private Change.Id changeId;
   private PatchSet.Id revision;
   private PatchSet.Id idActive;
-  private PatchSetSelectBox2 other;
+  private PatchSetSelectBox other;
 
-  PatchSetSelectBox2(SideBySide2 parent,
+  PatchSetSelectBox(SideBySide parent,
       DisplaySide side,
       Change.Id changeId,
       PatchSet.Id revision,
@@ -76,7 +81,8 @@
     this.path = path;
   }
 
-  void setUpPatchSetNav(JsArray<RevisionInfo> list, DiffInfo.FileMeta meta) {
+  void setUpPatchSetNav(JsArray<RevisionInfo> list, DiffInfo.FileMeta meta,
+      boolean editExists, int currentPatchSet, boolean open, boolean binary) {
     InlineHyperlink baseLink = null;
     InlineHyperlink selectedLink = null;
     if (sideA) {
@@ -85,10 +91,10 @@
     }
     for (int i = 0; i < list.length(); i++) {
       RevisionInfo r = list.get(i);
-      InlineHyperlink link = createLink(
-          String.valueOf(r._number()), new PatchSet.Id(changeId, r._number()));
+      InlineHyperlink link = createLink(r.id(),
+          new PatchSet.Id(changeId, r._number()));
       linkPanel.add(link);
-      if (revision != null && r._number() == revision.get()) {
+      if (revision != null && r.id().equals(revision.getId())) {
         selectedLink = link;
       }
     }
@@ -97,12 +103,37 @@
     } else if (sideA) {
       baseLink.setStyleName(style.selected());
     }
-    if (meta != null && !Patch.COMMIT_MSG.equals(path)) {
+
+    if (meta == null) {
+      return;
+    }
+    if (!Patch.COMMIT_MSG.equals(path)) {
       linkPanel.add(createDownloadLink());
     }
+    if (!binary && open && idActive != null && Gerrit.isSignedIn()) {
+      if ((editExists && idActive.get() == 0)
+          || (!editExists && idActive.get() == currentPatchSet)) {
+        linkPanel.add(createEditIcon());
+      }
+    }
+    List<WebLinkInfo> webLinks = Natives.asList(meta.web_links());
+    if (webLinks != null) {
+      for (WebLinkInfo webLink : webLinks) {
+        linkPanel.add(webLink.toAnchor());
+      }
+    }
   }
 
-  static void link(PatchSetSelectBox2 a, PatchSetSelectBox2 b) {
+  private Widget createEditIcon() {
+    PatchSet.Id id = (idActive == null) ? other.idActive : idActive;
+    Anchor anchor = new Anchor(
+        new ImageResourceRenderer().render(Gerrit.RESOURCES.edit()),
+        "#" + Dispatcher.toEditScreen(id, path));
+    anchor.setTitle(PatchUtil.C.edit());
+    return anchor;
+  }
+
+  static void link(PatchSetSelectBox a, PatchSetSelectBox b) {
     a.other = b;
     b.other = a;
   }
@@ -130,7 +161,7 @@
   }
 
   @UiHandler("icon")
-  void onIconClick(ClickEvent e) {
+  void onIconClick(@SuppressWarnings("unused") ClickEvent e) {
     parent.getCmFromSide(side).scrollToY(0);
     parent.getCommentManager().insertNewDraft(side, 0);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.ui.xml
similarity index 95%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.ui.xml
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.ui.xml
index dca0cd5..cc8dd74 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.ui.xml
@@ -20,8 +20,9 @@
   <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
   <ui:with field='patchConstants'
       type='com.google.gerrit.client.patches.PatchConstants'/>
-  <ui:style type='com.google.gerrit.client.diff.PatchSetSelectBox2.BoxStyle'>
+  <ui:style type='com.google.gerrit.client.diff.PatchSetSelectBox.BoxStyle'>
     @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
+    @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
     .table {
       width: 100%;
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesAction.java
index 4cbe1be..869b4a3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesAction.java
@@ -22,13 +22,13 @@
 import com.google.gwt.user.client.ui.Widget;
 
 class PreferencesAction {
-  private final SideBySide2 view;
+  private final SideBySide view;
   private final DiffPreferences prefs;
   private PopupPanel popup;
   private PreferencesBox current;
   private Widget partner;
 
-  PreferencesAction(SideBySide2 view, DiffPreferences prefs) {
+  PreferencesAction(SideBySide view, DiffPreferences prefs) {
     this.view = view;
     this.prefs = prefs;
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
index ac11cd5..498799c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
@@ -27,14 +27,14 @@
 import com.google.gerrit.client.account.DiffPreferences;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.NpIntTextBox;
+import com.google.gerrit.extensions.client.Theme;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Theme;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gwt.core.client.GWT;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.RepeatingCommand;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.KeyDownEvent;
@@ -55,11 +55,11 @@
 import com.google.gwt.user.client.ui.PopupPanel;
 import com.google.gwt.user.client.ui.ToggleButton;
 
-import net.codemirror.lib.ModeInjector;
+import net.codemirror.mode.ModeInfo;
+import net.codemirror.mode.ModeInjector;
+import net.codemirror.theme.ThemeLoader;
 
-import java.util.HashMap;
-import java.util.Map;
-import java.util.TreeMap;
+import java.util.Objects;
 
 /** Displays current diff preferences. */
 class PreferencesBox extends Composite {
@@ -70,7 +70,7 @@
     String dialog();
   }
 
-  private final SideBySide2 view;
+  private final SideBySide view;
   private DiffPreferences prefs;
   private int contextLastValue;
   private Timer updateContextTimer;
@@ -90,6 +90,7 @@
   @UiField ToggleButton leftSide;
   @UiField ToggleButton emptyPane;
   @UiField ToggleButton topMenu;
+  @UiField ToggleButton autoHideDiffTableHeader;
   @UiField ToggleButton manualReview;
   @UiField ToggleButton expandAllComments;
   @UiField ToggleButton renderEntireFile;
@@ -98,7 +99,7 @@
   @UiField Button apply;
   @UiField Button save;
 
-  PreferencesBox(SideBySide2 view) {
+  PreferencesBox(SideBySide view) {
     this.view = view;
 
     initWidget(uiBinder.createAndBindUi(this));
@@ -147,7 +148,13 @@
 
     setIgnoreWhitespace(prefs.ignoreWhitespace());
     tabWidth.setIntValue(prefs.tabSize());
-    lineLength.setIntValue(prefs.lineLength());
+    if (Patch.COMMIT_MSG.equals(view.getPath())) {
+      lineLength.setEnabled(false);
+      lineLength.setIntValue(72);
+    } else {
+      lineLength.setEnabled(true);
+      lineLength.setIntValue(prefs.lineLength());
+    }
     syntaxHighlighting.setValue(prefs.syntaxHighlighting());
     whitespaceErrors.setValue(prefs.showWhitespaceErrors());
     showTabs.setValue(prefs.showTabs());
@@ -157,6 +164,7 @@
     leftSide.setEnabled(!(prefs.hideEmptyPane()
         && view.diffTable.getChangeType() == ChangeType.ADDED));
     topMenu.setValue(!prefs.hideTopMenu());
+    autoHideDiffTableHeader.setValue(!prefs.autoHideDiffTableHeader());
     manualReview.setValue(prefs.manualReview());
     expandAllComments.setValue(prefs.expandAllComments());
     renderEntireFile.setValue(prefs.renderEntireFile());
@@ -192,7 +200,7 @@
   }
 
   @UiHandler("ignoreWhitespace")
-  void onIgnoreWhitespace(ChangeEvent e) {
+  void onIgnoreWhitespace(@SuppressWarnings("unused") ChangeEvent e) {
     prefs.ignoreWhitespace(Whitespace.valueOf(
         ignoreWhitespace.getValue(ignoreWhitespace.getSelectedIndex())));
     view.reloadDiffInfo();
@@ -322,6 +330,12 @@
     view.resizeCodeMirror();
   }
 
+  @UiHandler("autoHideDiffTableHeader")
+  void onAutoHideDiffTableHeader(ValueChangeEvent<Boolean> e) {
+    prefs.autoHideDiffTableHeader(!e.getValue());
+    view.setAutoHideDiffHeader(!e.getValue());
+  }
+
   @UiHandler("manualReview")
   void onManualReview(ValueChangeEvent<Boolean> e) {
     prefs.manualReview(e.getValue());
@@ -338,26 +352,31 @@
   }
 
   @UiHandler("mode")
-  void onMode(ChangeEvent e) {
-    final String m = mode.getValue(mode.getSelectedIndex());
+  void onMode(@SuppressWarnings("unused") ChangeEvent e) {
+    final String mode = getSelectedMode();
     prefs.syntaxHighlighting(true);
     syntaxHighlighting.setValue(true, false);
-    Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
+    new ModeInjector().add(mode).inject(new GerritCallback<Void>() {
       @Override
-      public boolean execute() {
-        if (prefs.syntaxHighlighting() && view.isAttached()) {
+      public void onSuccess(Void result) {
+        if (prefs.syntaxHighlighting()
+            && Objects.equals(mode, getSelectedMode())
+            && view.isAttached()) {
           view.operation(new Runnable() {
             @Override
             public void run() {
-              String mode = m != null && !m.isEmpty() ? m : null;
               view.getCmFromSide(DisplaySide.A).setOption("mode", mode);
               view.getCmFromSide(DisplaySide.B).setOption("mode", mode);
             }
           });
         }
-        return false;
       }
-    }, 50);
+    });
+  }
+
+  private String getSelectedMode() {
+    String m = mode.getValue(mode.getSelectedIndex());
+    return m != null && !m.isEmpty() ? m : null;
   }
 
   @UiHandler("whitespaceErrors")
@@ -380,26 +399,38 @@
   }
 
   @UiHandler("theme")
-  void onTheme(ChangeEvent e) {
-    prefs.theme(Theme.valueOf(theme.getValue(theme.getSelectedIndex())));
-    view.setThemeStyles(prefs.theme().isDark());
-    view.operation(new Runnable() {
+  void onTheme(@SuppressWarnings("unused") ChangeEvent e) {
+    final Theme newTheme = getSelectedTheme();
+    prefs.theme(newTheme);
+    ThemeLoader.loadTheme(newTheme, new GerritCallback<Void>() {
       @Override
-      public void run() {
-        String t = prefs.theme().name().toLowerCase();
-        view.getCmFromSide(DisplaySide.A).setOption("theme", t);
-        view.getCmFromSide(DisplaySide.B).setOption("theme", t);
+      public void onSuccess(Void result) {
+        view.operation(new Runnable() {
+          @Override
+          public void run() {
+            if (getSelectedTheme() == newTheme && isAttached()) {
+              String t = newTheme.name().toLowerCase();
+              view.getCmFromSide(DisplaySide.A).setOption("theme", t);
+              view.getCmFromSide(DisplaySide.B).setOption("theme", t);
+              view.setThemeStyles(newTheme.isDark());
+            }
+          }
+        });
       }
     });
   }
 
+  private Theme getSelectedTheme() {
+    return Theme.valueOf(theme.getValue(theme.getSelectedIndex()));
+  }
+
   @UiHandler("apply")
-  void onApply(ClickEvent e) {
+  void onApply(@SuppressWarnings("unused") ClickEvent e) {
     close();
   }
 
   @UiHandler("save")
-  void onSave(ClickEvent e) {
+  void onSave(@SuppressWarnings("unused") ClickEvent e) {
     AccountApi.putDiffPreferences(prefs, new GerritCallback<DiffPreferences>() {
       @Override
       public void onSuccess(DiffPreferences result) {
@@ -454,46 +485,22 @@
         IGNORE_ALL_SPACE.name());
   }
 
-  private static final Map<String, String> NAME_TO_MODE;
-  private static final Map<String, String> NORMALIZED_MODES;
-  static {
-    NAME_TO_MODE = new TreeMap<>();
-    NORMALIZED_MODES = new HashMap<>();
-    for (String type : ModeInjector.getKnownMimeTypes()) {
-      String name = type;
-      if (name.startsWith("text/x-")) {
-        name = name.substring("text/x-".length());
-      } else if (name.startsWith("text/")) {
-        name = name.substring("text/".length());
-      } else if (name.startsWith("application/")) {
-        name = name.substring("application/".length());
-      }
-
-      String normalized = NAME_TO_MODE.get(name);
-      if (normalized == null) {
-        normalized = type;
-        NAME_TO_MODE.put(name, normalized);
-      }
-      NORMALIZED_MODES.put(type, normalized);
-    }
-  }
-
   private void initMode() {
     mode.addItem("", "");
-    for (Map.Entry<String, String> e : NAME_TO_MODE.entrySet()) {
-      mode.addItem(e.getKey(), e.getValue());
+    for (ModeInfo m : Natives.asList(ModeInfo.all())) {
+      mode.addItem(m.name(), m.mime());
     }
   }
 
   private void setMode(String modeType) {
     if (modeType != null && !modeType.isEmpty()) {
-      if (NORMALIZED_MODES.containsKey(modeType)) {
-        modeType = NORMALIZED_MODES.get(modeType);
-      }
-      for (int i = 0; i < mode.getItemCount(); i++) {
-        if (mode.getValue(i).equals(modeType)) {
-          mode.setSelectedIndex(i);
-          return;
+      ModeInfo m = ModeInfo.findModeByMIME(modeType);
+      if (m != null) {
+        for (int i = 0; i < mode.getItemCount(); i++) {
+          if (mode.getValue(i).equals(m.mime())) {
+            mode.setSelectedIndex(i);
+            return;
+          }
         }
       }
     }
@@ -512,26 +519,8 @@
   }
 
   private void initTheme() {
-    theme.addItem(
-        Theme.DEFAULT.name().toLowerCase(),
-        Theme.DEFAULT.name());
-    theme.addItem(
-        Theme.ECLIPSE.name().toLowerCase(),
-        Theme.ECLIPSE.name());
-    theme.addItem(
-        Theme.ELEGANT.name().toLowerCase(),
-        Theme.ELEGANT.name());
-    theme.addItem(
-        Theme.NEAT.name().toLowerCase(),
-        Theme.NEAT.name());
-    theme.addItem(
-        Theme.MIDNIGHT.name().toLowerCase(),
-        Theme.MIDNIGHT.name());
-    theme.addItem(
-        Theme.NIGHT.name().toLowerCase(),
-        Theme.NIGHT.name());
-    theme.addItem(
-        Theme.TWILIGHT.name().toLowerCase(),
-        Theme.TWILIGHT.name());
+    for (Theme t : Theme.values()) {
+      theme.addItem(t.name().toLowerCase(), t.name());
+    }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml
index af53916..62d2eac 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml
@@ -73,6 +73,10 @@
       color: #dddd00;
     }
 
+    .box input.gwt-TextBox:disabled {
+      background-color: #cacaca;
+    }
+
     .box .gwt-ToggleButton {
       position: relative;
       height: 19px;
@@ -250,6 +254,13 @@
         </g:ToggleButton></td>
       </tr>
       <tr>
+        <th><ui:msg>Auto Hide Diff Table Header</ui:msg></th>
+        <td><g:ToggleButton ui:field='autoHideDiffTableHeader'>
+          <g:upFace><ui:msg>Yes</ui:msg></g:upFace>
+          <g:downFace><ui:msg>No</ui:msg></g:downFace>
+        </g:ToggleButton></td>
+      </tr>
+      <tr>
         <th><ui:msg>Mark Reviewed</ui:msg></th>
         <td><g:ToggleButton ui:field='manualReview'>
           <g:upFace><ui:msg>Automatic</ui:msg></g:upFace>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
index f5f2891..7d74c2b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.client.diff;
 
 import com.google.gerrit.client.AvatarImage;
+import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.CommentApi;
@@ -60,6 +61,7 @@
   @UiField Element buttons;
   @UiField Button reply;
   @UiField Button done;
+  @UiField Button fix;
 
   @UiField(provided = true)
   AvatarImage avatar;
@@ -68,7 +70,8 @@
       CommentGroup group,
       CommentLinkProcessor clp,
       PatchSet.Id psId,
-      CommentInfo info) {
+      CommentInfo info,
+      boolean open) {
     super(group, info.range());
 
     this.psId = psId;
@@ -97,6 +100,8 @@
       message.setInnerSafeHtml(clp.apply(
           new SafeHtmlBuilder().append(msg).wikify()));
     }
+
+    fix.setVisible(open);
   }
 
   @Override
@@ -109,6 +114,7 @@
     return UIObject.isVisible(message);
   }
 
+  @Override
   void setOpen(boolean open) {
     UIObject.setVisible(summary, !open);
     UIObject.setVisible(message, open);
@@ -145,7 +151,7 @@
 
   void doReply() {
     if (!Gerrit.isSignedIn()) {
-      Gerrit.doSignIn(getCommentManager().getSideBySide2().getToken());
+      Gerrit.doSignIn(getCommentManager().getSideBySide().getToken());
     } else if (replyBox == null) {
       addReplyBox();
     } else {
@@ -163,7 +169,7 @@
   void onReplyDone(ClickEvent e) {
     e.stopPropagation();
     if (!Gerrit.isSignedIn()) {
-      Gerrit.doSignIn(getCommentManager().getSideBySide2().getToken());
+      Gerrit.doSignIn(getCommentManager().getSideBySide().getToken());
     } else if (replyBox == null) {
       done.setEnabled(false);
       CommentInfo input = CommentInfo.createReply(comment);
@@ -183,6 +189,17 @@
     }
   }
 
+  @UiHandler("fix")
+  void onFix(ClickEvent e) {
+    e.stopPropagation();
+    String t = Dispatcher.toEditScreen(psId, comment.path(), comment.line());
+    if (!Gerrit.isSignedIn()) {
+      Gerrit.doSignIn(t);
+    } else {
+      Gerrit.display(t);
+    }
+  }
+
   private static String authorName(CommentInfo info) {
     if (info.author() != null) {
       if (info.author().name() != null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
index 6e88c5b..bcc34e2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
@@ -66,6 +66,11 @@
           <ui:attribute name='title'/>
           <div><ui:msg>Done</ui:msg></div>
         </g:Button>
+        <g:Button ui:field='fix' styleName='' visible='false'
+            title='Fix this comment in the inline editor'>
+          <ui:attribute name='title'/>
+          <div><ui:msg>Fix</ui:msg></div>
+        </g:Button>
       </div>
     </div>
   </g:HTMLPanel>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java
index 8c4fc51..e379d60 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java
@@ -16,30 +16,16 @@
 
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.resources.client.ClientBundle;
-import com.google.gwt.resources.client.CssResource;
 import com.google.gwt.resources.client.ImageResource;
 
 /** Resources used by diff. */
 interface Resources extends ClientBundle {
   static final Resources I = GWT.create(Resources.class);
 
-  @Source("CommentBoxUi.css") Style style();
-  @Source("go-prev.png") ImageResource go_prev();
-  @Source("go-next.png") ImageResource go_next();
-  @Source("go-up.png") ImageResource go_up();
-  @Source("gear.png") ImageResource gear();
+  @Source("CommentBox.css") CommentBox.Style style();
+  @Source("Scrollbar.css") Scrollbar.Style scrollbarStyle();
 
-  interface Style extends CssResource {
-    String commentWidgets();
-    String commentBox();
-    String contents();
-    String message();
-    String header();
-    String summary();
-    String date();
-
-    String go_prev();
-    String go_next();
-    String go_up();
-  }
+  @Source("goPrev.png") ImageResource goPrev();
+  @Source("goNext.png") ImageResource goNext();
+  @Source("goUp.png") ImageResource goUp();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java
index abff8ef..4ee09d2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java
@@ -22,17 +22,18 @@
 class ScrollSynchronizer {
   private DiffTable diffTable;
   private LineMapper mapper;
-  private OverviewBar overview;
   private ScrollCallback active;
   private ScrollCallback callbackA;
   private ScrollCallback callbackB;
+  private CodeMirror cmB;
+  private boolean autoHideDiffTableHeader;
 
   ScrollSynchronizer(DiffTable diffTable,
       CodeMirror cmA, CodeMirror cmB,
       LineMapper mapper) {
     this.diffTable = diffTable;
     this.mapper = mapper;
-    this.overview = diffTable.overview;
+    this.cmB = cmB;
 
     callbackA = new ScrollCallback(cmA, cmB, DisplaySide.A);
     callbackB = new ScrollCallback(cmB, cmA, DisplaySide.B);
@@ -40,15 +41,23 @@
     cmB.on("scroll", callbackB);
   }
 
+  void setAutoHideDiffTableHeader(boolean autoHide) {
+    if (autoHide) {
+      updateDiffTableHeader(cmB.getScrollInfo());
+    } else {
+      diffTable.setHeaderVisible(true);
+    }
+    autoHideDiffTableHeader = autoHide;
+  }
+
   void syncScroll(DisplaySide masterSide) {
     (masterSide == DisplaySide.A ? callbackA : callbackB).sync();
   }
 
-  private void updateScreenHeader(ScrollInfo si) {
-    if (si.getTop() == 0 && !diffTable.isHeaderVisible()) {
+  private void updateDiffTableHeader(ScrollInfo si) {
+    if (si.top() == 0) {
       diffTable.setHeaderVisible(true);
-    } else if (si.getTop() > 0.5 * si.getClientHeight()
-        && diffTable.isHeaderVisible()) {
+    } else if (si.top() > 0.5 * si.clientHeight()) {
       diffTable.setHeaderVisible(false);
     }
   }
@@ -75,7 +84,7 @@
     }
 
     void sync() {
-      dst.scrollToY(align(src.getScrollInfo().getTop()));
+      dst.scrollToY(align(src.getScrollInfo().top()));
     }
 
     @Override
@@ -86,9 +95,10 @@
       }
       if (active == this) {
         ScrollInfo si = src.getScrollInfo();
-        updateScreenHeader(si);
-        overview.update(si);
-        dst.scrollTo(si.getLeft(), align(si.getTop()));
+        if (autoHideDiffTableHeader) {
+          updateDiffTableHeader(si);
+        }
+        dst.scrollTo(si.left(), align(si.top()));
         state = 0;
       }
     }
@@ -97,7 +107,7 @@
       switch (state) {
         case 0:
           state = 1;
-          dst.scrollToY(align(src.getScrollInfo().getTop()));
+          dst.scrollToY(align(src.getScrollInfo().top()));
           break;
         case 1:
           state = 2;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.css
new file mode 100644
index 0000000..383f278
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.css
@@ -0,0 +1,38 @@
+/* Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.comment, .draft, .insert, .delete, .edit {
+  min-height: 5px;
+  position: absolute;
+  right: 0;
+  z-index: 7;
+}
+
+.comment, .draft {
+  color: #0d0d0d;
+  font-size: 9px;
+}
+
+.delete {
+  background-color: #faa;
+}
+.insert {
+  background-color: #9f9;
+}
+.edit {
+  border-left: 3px solid #faa;
+  width: 2px !important;
+  background-color: #9f9;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.java
new file mode 100644
index 0000000..b72ab43
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.java
@@ -0,0 +1,99 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gwt.resources.client.CssResource;
+
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.Pos;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Displays overview of all edits and comments in this file. */
+class Scrollbar {
+  static {
+    Resources.I.scrollbarStyle().ensureInjected();
+  }
+
+  interface Style extends CssResource {
+    String comment();
+    String draft();
+    String insert();
+    String delete();
+    String edit();
+  }
+
+  private final List<ScrollbarAnnotation> diff = new ArrayList<>();
+  private final DiffTable parent;
+
+  Scrollbar(DiffTable d) {
+    parent = d;
+  }
+
+  ScrollbarAnnotation comment(CodeMirror cm, int line) {
+    ScrollbarAnnotation a = new ScrollbarAnnotation(cm);
+    a.setStyleName(Resources.I.scrollbarStyle().comment());
+    a.at(line);
+    a.getElement().setInnerText("\u2736"); // Six pointed black star
+    parent.add(a);
+    return a;
+  }
+
+  ScrollbarAnnotation draft(CodeMirror cm, int line) {
+    ScrollbarAnnotation a = new ScrollbarAnnotation(cm);
+    a.setStyleName(Resources.I.scrollbarStyle().draft());
+    a.at(line);
+    a.getElement().setInnerText("\u270D"); // Writing hand
+    parent.add(a);
+    return a;
+  }
+
+  ScrollbarAnnotation insert(CodeMirror cm, int line, int len) {
+    ScrollbarAnnotation a = diff(cm, line, len);
+    a.setStyleName(Resources.I.scrollbarStyle().insert());
+    parent.add(a);
+    return a;
+  }
+
+  ScrollbarAnnotation delete(CodeMirror cmA, CodeMirror cmB, int line, int len) {
+    ScrollbarAnnotation a = diff(cmA, line, len);
+    a.setStyleName(Resources.I.scrollbarStyle().delete());
+    a.renderOn(cmB);
+    parent.add(a);
+    return a;
+  }
+
+  ScrollbarAnnotation edit(CodeMirror cm, int line, int len) {
+    ScrollbarAnnotation a = diff(cm, line, len);
+    a.setStyleName(Resources.I.scrollbarStyle().edit());
+    parent.add(a);
+    return a;
+  }
+
+  private ScrollbarAnnotation diff(CodeMirror cm, int s, int n) {
+    ScrollbarAnnotation a = new ScrollbarAnnotation(cm);
+    a.at(Pos.create(s), Pos.create(s + n));
+    diff.add(a);
+    return a;
+  }
+
+  void removeDiffAnnotations() {
+    for (ScrollbarAnnotation a : diff) {
+      a.remove();
+    }
+    diff.clear();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollbarAnnotation.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollbarAnnotation.java
new file mode 100644
index 0000000..c8f9911
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollbarAnnotation.java
@@ -0,0 +1,119 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.Widget;
+
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.CodeMirror.RegisteredHandler;
+import net.codemirror.lib.Pos;
+
+/** Displayed on the vertical scrollbar to place a chunk or comment. */
+class ScrollbarAnnotation extends Widget implements ClickHandler {
+  private final CodeMirror cm;
+  private CodeMirror cmB;
+  private RegisteredHandler refresh;
+  private Pos from;
+  private Pos to;
+  private double scale;
+
+  ScrollbarAnnotation(CodeMirror cm) {
+    setElement((Element) DOM.createDiv());
+    getElement().setAttribute("not-content", "true");
+    addDomHandler(this, ClickEvent.getType());
+    this.cm = cm;
+    this.cmB = cm;
+  }
+
+  void remove() {
+    removeFromParent();
+  }
+
+  void at(int line) {
+    at(Pos.create(line), Pos.create(line + 1));
+  }
+
+  void at(Pos from, Pos to) {
+    this.from = from;
+    this.to = to;
+  }
+
+  void renderOn(CodeMirror cm) {
+    this.cmB = cm;
+  }
+
+  @Override
+  protected void onLoad() {
+    cmB.getWrapperElement().appendChild(getElement());
+    refresh = cmB.on("refresh", new Runnable() {
+      @Override
+      public void run() {
+        if (updateScale()) {
+          updatePosition();
+        }
+      }
+    });
+    updateScale();
+    updatePosition();
+  }
+
+  @Override
+  protected void onUnload() {
+    cmB.off("refresh", refresh);
+  }
+
+  private boolean updateScale() {
+    double old = scale;
+    double docHeight = cmB.getWrapperElement().getClientHeight();
+    double lineHeight = cmB.heightAtLine(cmB.lastLine() + 1, "local");
+    scale = (docHeight - cmB.barHeight()) / lineHeight;
+    return old != scale;
+  }
+
+  private void updatePosition() {
+    double top = cm.charCoords(from, "local").top() * scale;
+    double bottom = cm.charCoords(to, "local").bottom() * scale;
+
+    Element e = getElement();
+    e.getStyle().setTop(top, Unit.PX);
+    e.getStyle().setWidth(Math.max(2, cm.barWidth() - 1), Unit.PX);
+    e.getStyle().setHeight(Math.max(3, bottom - top), Unit.PX);
+  }
+
+  @Override
+  public void onClick(ClickEvent event) {
+    event.stopPropagation();
+
+    int line = from.line();
+    int h = to.line() - line;
+    if (h > 5) {
+      // Map click inside of the annotation to the relative position
+      // within the region covered by the annotation.
+      double s = ((double) event.getY()) / getElement().getOffsetHeight();
+      line += (int) (s * h);
+    }
+
+    double y = cm.heightAtLine(line, "local");
+    double viewport = cm.getScrollInfo().clientHeight();
+    cm.setCursor(from);
+    cm.scrollTo(0, y - 0.5 * viewport);
+    cm.focus();
+  }
+}
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/SideBySide.java
similarity index 75%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
index 30306f2..d06b59e 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/SideBySide.java
@@ -17,12 +17,15 @@
 import static com.google.gerrit.reviewdb.client.AccountDiffPreference.WHOLE_FILE_CONTEXT;
 import static java.lang.Double.POSITIVE_INFINITY;
 
+import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.JumpKeys;
 import com.google.gerrit.client.account.DiffPreferences;
-import com.google.gerrit.client.change.ChangeScreen2;
+import com.google.gerrit.client.change.ChangeScreen;
+import com.google.gerrit.client.change.FileTable;
 import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.EditInfo;
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.changes.ChangeList;
 import com.google.gerrit.client.diff.DiffInfo.FileMeta;
@@ -33,10 +36,12 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JsArray;
@@ -45,7 +50,6 @@
 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.NativeEvent;
-import com.google.gwt.dom.client.Style;
 import com.google.gwt.event.dom.client.FocusEvent;
 import com.google.gwt.event.dom.client.FocusHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
@@ -55,10 +59,10 @@
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.ImageResourceRenderer;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
@@ -67,22 +71,24 @@
 import net.codemirror.lib.CodeMirror;
 import net.codemirror.lib.CodeMirror.BeforeSelectionChangeHandler;
 import net.codemirror.lib.CodeMirror.GutterClickHandler;
-import net.codemirror.lib.CodeMirror.LineClassWhere;
 import net.codemirror.lib.CodeMirror.LineHandle;
 import net.codemirror.lib.Configuration;
 import net.codemirror.lib.KeyMap;
-import net.codemirror.lib.LineCharacter;
-import net.codemirror.lib.ModeInjector;
+import net.codemirror.lib.Pos;
+import net.codemirror.mode.ModeInfo;
+import net.codemirror.mode.ModeInjector;
+import net.codemirror.theme.ThemeLoader;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.EnumSet;
 import java.util.List;
 
-public class SideBySide2 extends Screen {
+public class SideBySide extends Screen {
   private static final KeyMap RENDER_ENTIRE_FILE_KEYMAP = KeyMap.create()
-      .on("Ctrl-F", false);
+      .propagate("Ctrl-F");
 
-  interface Binder extends UiBinder<FlowPanel, SideBySide2> {}
+  interface Binder extends UiBinder<FlowPanel, SideBySide> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
   enum FileSize {
@@ -110,18 +116,16 @@
   private DisplaySide startSide;
   private int startLine;
   private DiffPreferences prefs;
+  private Change.Status changeStatus;
 
   private CodeMirror cmA;
   private CodeMirror cmB;
-  private Element columnMarginA;
-  private Element columnMarginB;
-  private double charWidthPx;
-  private double lineHeightPx;
 
   private HandlerRegistration resizeHandler;
   private ScrollSynchronizer scrollSynchronizer;
   private DiffInfo diff;
   private FileSize fileSize;
+  private EditInfo edit;
   private ChunkManager chunkManager;
   private CommentManager commentManager;
   private SkipManager skipManager;
@@ -133,7 +137,7 @@
   private PreferencesAction prefsAction;
   private int reloadVersionId;
 
-  public SideBySide2(
+  public SideBySide(
       PatchSet.Id base,
       PatchSet.Id revision,
       String path,
@@ -166,22 +170,36 @@
   protected void onLoad() {
     super.onLoad();
 
-    CallbackGroup cmGroup = new CallbackGroup();
-    CodeMirror.initLibrary(cmGroup.add(CallbackGroup.<Void> emptyCallback()));
-    final CallbackGroup group = new CallbackGroup();
-    final AsyncCallback<Void> modeInjectorCb =
-        group.add(CallbackGroup.<Void> emptyCallback());
+    CallbackGroup group1 = new CallbackGroup();
+    final CallbackGroup group2 = new CallbackGroup();
+
+    CodeMirror.initLibrary(group1.add(new AsyncCallback<Void>() {
+      final AsyncCallback<Void> themeCallback = group2.addEmpty();
+
+      @Override
+      public void onSuccess(Void result) {
+        // Load theme after CM library to ensure theme can override CSS.
+        ThemeLoader.loadTheme(prefs.theme(), themeCallback);
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+      }
+    }));
 
     DiffApi.diff(revision, path)
       .base(base)
       .wholeFile()
       .intraline(prefs.intralineDifference())
       .ignoreWhitespace(prefs.ignoreWhitespace())
-      .get(cmGroup.addFinal(new GerritCallback<DiffInfo>() {
+      .get(group1.addFinal(new GerritCallback<DiffInfo>() {
+        final AsyncCallback<Void> modeInjectorCb = group2.addEmpty();
+
         @Override
         public void onSuccess(DiffInfo diffInfo) {
           diff = diffInfo;
           fileSize = bucketFileSize(diffInfo);
+
           if (prefs.syntaxHighlighting()) {
             if (fileSize.compareTo(FileSize.SMALL) > 0) {
               modeInjectorCb.onSuccess(null);
@@ -194,30 +212,58 @@
         }
       }));
 
+    if (Gerrit.isSignedIn()) {
+      ChangeApi.edit(changeId.get(), group2.add(
+          new AsyncCallback<EditInfo>() {
+            @Override
+            public void onSuccess(EditInfo result) {
+              edit = result;
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+            }
+          }));
+    }
+
     final CommentsCollections comments = new CommentsCollections();
-    comments.load(base, revision, path, group);
+    comments.load(base, revision, path, group2);
 
     RestApi call = ChangeApi.detail(changeId.get());
     ChangeList.addOptions(call, EnumSet.of(
         ListChangesOption.ALL_REVISIONS));
-    call.get(group.add(new GerritCallback<ChangeInfo>() {
+    call.get(group2.add(new AsyncCallback<ChangeInfo>() {
       @Override
       public void onSuccess(ChangeInfo info) {
+        changeStatus = info.status();
         info.revisions().copyKeysIntoChildren("name");
+        if (edit != null) {
+          edit.set_name(edit.commit().commit());
+          info.set_edit(edit);
+          info.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
+        }
+        int currentPatchSet = info.revision(info.current_revision())._number();
         JsArray<RevisionInfo> list = info.revisions().values();
         RevisionInfo.sortRevisionInfoByNumber(list);
-        diffTable.set(prefs, list, diff);
+        diffTable.set(prefs, list, diff, edit != null, currentPatchSet,
+            changeStatus.isOpen(), diff.binary());
         header.setChangeInfo(info);
-      }}));
+      }
 
-    ConfigInfoCache.get(changeId, group.addFinal(
-        new ScreenLoadCallback<ConfigInfoCache.Entry>(SideBySide2.this) {
+      @Override
+      public void onFailure(Throwable caught) {
+      }
+    }));
+
+    ConfigInfoCache.get(changeId, group2.addFinal(
+        new ScreenLoadCallback<ConfigInfoCache.Entry>(SideBySide.this) {
           @Override
           protected void preDisplay(ConfigInfoCache.Entry result) {
             commentManager = new CommentManager(
-                SideBySide2.this,
+                SideBySide.this,
                 base, revision, path,
-                result.getCommentLinkProcessor());
+                result.getCommentLinkProcessor(),
+                changeStatus.isOpen());
             setTheme(result.getTheme());
             display(comments);
           }
@@ -239,18 +285,16 @@
       }
     });
 
-    final int height = getCodeMirrorHeight();
     operation(new Runnable() {
       @Override
       public void run() {
-        cmA.setHeight(height);
-        cmB.setHeight(height);
+        resizeCodeMirror();
         chunkManager.adjustPadding();
         cmA.refresh();
         cmB.refresh();
       }
     });
-    setLineLength(prefs.lineLength());
+    setLineLength(Patch.COMMIT_MSG.equals(path) ? 72 : prefs.lineLength());
     diffTable.refresh();
 
     if (startLine == 0) {
@@ -266,15 +310,11 @@
       }
     }
     if (startSide != null && startLine > 0) {
-      int line = startLine - 1;
       CodeMirror cm = getCmFromSide(startSide);
-      if (cm.lineAtHeight(height - 20) < line) {
-        cm.scrollToY(cm.heightAtLine(line, "local") - 0.5 * height);
-      }
-      cm.setCursor(LineCharacter.create(line));
+      cm.scrollToLine(startLine - 1);
       cm.focus();
     } else {
-      cmA.setCursor(LineCharacter.create(0));
+      cmA.setCursor(Pos.create(0));
       cmA.focus();
     }
     if (Gerrit.isSignedIn() && prefs.autoReview()) {
@@ -320,11 +360,9 @@
   }
 
   private void registerCmEvents(final CodeMirror cm) {
-    cm.on("beforeSelectionChange", onSelectionChange(cm));
     cm.on("cursorActivity", updateActiveLine(cm));
-    cm.on("gutterClick", onGutterClick(cm));
     cm.on("focus", updateActiveLine(cm));
-    cm.addKeyMap(KeyMap.create()
+    KeyMap keyMap = KeyMap.create()
         .on("A", upToChange(true))
         .on("U", upToChange(false))
         .on("[", header.navigate(Direction.PREV))
@@ -332,8 +370,8 @@
         .on("R", header.toggleReviewed())
         .on("O", commentManager.toggleOpenBox(cm))
         .on("Enter", commentManager.toggleOpenBox(cm))
-        .on("C", commentManager.insertNewDraft(cm))
         .on("N", maybeNextVimSearch(cm))
+        .on("E", openEditScreen(cm))
         .on("P", chunkManager.diffChunkNav(cm, Direction.PREV))
         .on("Shift-A", diffTable.toggleA())
         .on("Shift-M", header.reviewedAndNext())
@@ -343,6 +381,7 @@
         .on("Shift-Left", moveCursorToSide(cm, DisplaySide.A))
         .on("Shift-Right", moveCursorToSide(cm, DisplaySide.B))
         .on("I", new Runnable() {
+          @Override
           public void run() {
             switch (getIntraLineStatus()) {
               case OFF:
@@ -369,19 +408,19 @@
         .on("Space", new Runnable() {
           @Override
           public void run() {
-            CodeMirror.handleVimKey(cm, "<C-d>");
+            cm.vim().handleKey("<C-d>");
           }
         })
         .on("Shift-Space", new Runnable() {
           @Override
           public void run() {
-            CodeMirror.handleVimKey(cm, "<C-u>");
+            cm.vim().handleKey("<C-u>");
           }
         })
         .on("Ctrl-F", new Runnable() {
           @Override
           public void run() {
-            CodeMirror.handleVimKey(cm, "/");
+            cm.vim().handleKey("/");
           }
         })
         .on("Ctrl-A", new Runnable() {
@@ -389,7 +428,13 @@
           public void run() {
             cm.execCommand("selectAll");
           }
-        }));
+        });
+    if (revision.get() != 0) {
+      cm.on("beforeSelectionChange", onSelectionChange(cm));
+      cm.on("gutterClick", onGutterClick(cm));
+      keyMap.on("C", commentManager.insertNewDraft(cm));
+    }
+    cm.addKeyMap(keyMap);
     if (prefs.renderEntireFile()) {
       cm.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
     }
@@ -400,10 +445,8 @@
       private InsertCommentBubble bubble;
 
       @Override
-      public void handle(CodeMirror cm, LineCharacter anchor, LineCharacter head) {
-        if (anchor == head
-            || (anchor.getLine() == head.getLine()
-             && anchor.getCh() == head.getCh())) {
+      public void handle(CodeMirror cm, Pos anchor, Pos head) {
+        if (anchor.equals(head)) {
           if (bubble != null) {
             bubble.setVisible(false);
           }
@@ -416,10 +459,10 @@
         bubble.position(cm.charCoords(head, "local"));
       }
 
-      private void init(LineCharacter anchor) {
+      private void init(Pos anchor) {
         bubble = new InsertCommentBubble(commentManager, cm);
         add(bubble);
-        cm.addWidget(anchor, bubble.getElement(), false);
+        cm.addWidget(anchor, bubble.getElement());
       }
     };
   }
@@ -428,7 +471,7 @@
   public void registerKeys() {
     super.registerKeys();
 
-    keysNavigation.add(new UpToChangeCommand2(revision, 0, 'u'));
+    keysNavigation.add(new UpToChangeCommand(revision, 0, 'u'));
     keysNavigation.add(
         new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_LEFT, PatchUtil.C.focusSideA()),
         new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_RIGHT, PatchUtil.C.focusSideB()));
@@ -445,6 +488,7 @@
         new NoOpKeyCommand(KeyCommand.M_CTRL, 'f', Gerrit.C.keySearch()));
 
     keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
+    keysAction.add(new NoOpKeyCommand(0, 'e', PatchUtil.C.openEditScreen()));
     keysAction.add(new NoOpKeyCommand(0, KeyCodes.KEY_ENTER,
         PatchUtil.C.expandComment()));
     keysAction.add(new NoOpKeyCommand(0, 'o', PatchUtil.C.expandComment()));
@@ -521,24 +565,20 @@
 
   private void display(final CommentsCollections comments) {
     setThemeStyles(prefs.theme().isDark());
-    setShowTabs(prefs.showTabs());
     setShowIntraline(prefs.intralineDifference());
     if (prefs.showLineNumbers()) {
       diffTable.addStyleName(DiffTable.style.showLineNumbers());
     }
 
-    cmA = newCM(diff.meta_a(), diff.text_a(), DisplaySide.A, diffTable.cmA);
-    cmB = newCM(diff.meta_b(), diff.text_b(), DisplaySide.B, diffTable.cmB);
-    diffTable.overview.init(cmB);
-    chunkManager = new ChunkManager(this, cmA, cmB, diffTable.overview);
-    skipManager = new SkipManager(this, commentManager);
+    cmA = newCM(diff.meta_a(), diff.text_a(), diffTable.cmA);
+    cmB = newCM(diff.meta_b(), diff.text_b(), diffTable.cmB);
 
-    columnMarginA = DOM.createDiv();
-    columnMarginB = DOM.createDiv();
-    columnMarginA.setClassName(DiffTable.style.columnMargin());
-    columnMarginB.setClassName(DiffTable.style.columnMargin());
-    cmA.getMoverElement().appendChild(columnMarginA);
-    cmB.getMoverElement().appendChild(columnMarginB);
+    cmA.extras().side(DisplaySide.A);
+    cmB.extras().side(DisplaySide.B);
+    setShowTabs(prefs.showTabs());
+
+    chunkManager = new ChunkManager(this, cmA, cmB, diffTable.scrollbar);
+    skipManager = new SkipManager(this, commentManager);
 
     if (prefs.renderEntireFile() && !canEnableRenderEntireFile(prefs)) {
       // CodeMirror is too slow to layout an entire huge file.
@@ -546,6 +586,7 @@
     }
 
     operation(new Runnable() {
+      @Override
       public void run() {
         // Estimate initial CM3 height, fixed up in onShowView.
         int height = Window.getClientHeight()
@@ -565,7 +606,8 @@
             chunkManager.getLineMapper());
 
     prefsAction = new PreferencesAction(this, prefs);
-    header.init(prefsAction);
+    header.init(prefsAction, getLinks(), diff.side_by_side_web_links());
+    scrollSynchronizer.setAutoHideDiffTableHeader(prefs.autoHideDiffTableHeader());
 
     if (prefs.syntaxHighlighting() && fileSize.compareTo(FileSize.SMALL) > 0) {
       Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
@@ -580,22 +622,29 @@
     }
   }
 
+  private List<InlineHyperlink> getLinks() {
+    InlineHyperlink toUnifiedDiffLink = new InlineHyperlink();
+    toUnifiedDiffLink.setHTML(
+        new ImageResourceRenderer().render(Gerrit.RESOURCES.unifiedDiff()));
+    toUnifiedDiffLink.setTargetHistoryToken(
+        Dispatcher.toUnified(base, revision, path));
+    toUnifiedDiffLink.setTitle(PatchUtil.C.unifiedDiff());
+    return Collections.singletonList(toUnifiedDiffLink);
+  }
+
   private CodeMirror newCM(
       DiffInfo.FileMeta meta,
       String contents,
-      DisplaySide side,
       Element parent) {
-    String mode = fileSize == FileSize.SMALL
-        ? getContentType(meta)
-        : null;
-    return CodeMirror.create(side, parent, Configuration.create()
+    return CodeMirror.create(parent, Configuration.create()
       .set("readOnly", true)
       .set("cursorBlinkRate", 0)
       .set("cursorHeight", 0.85)
       .set("lineNumbers", prefs.showLineNumbers())
       .set("tabSize", prefs.tabSize())
-      .set("mode", mode)
+      .set("mode", fileSize == FileSize.SMALL ? getContentType(meta) : null)
       .set("lineWrapping", false)
+      .set("scrollbarStyle", "overlay")
       .set("styleSelectedText", true)
       .set("showTrailingSpace", prefs.showWhitespaceErrors())
       .set("keyMap", "vim_ro")
@@ -625,61 +674,14 @@
     }
   }
 
-  void setShowTabs(boolean b) {
-    if (b) {
-      diffTable.addStyleName(DiffTable.style.showTabs());
-    } else {
-      diffTable.removeStyleName(DiffTable.style.showTabs());
-    }
+  void setShowTabs(boolean show) {
+    cmA.extras().showTabs(show);
+    cmB.extras().showTabs(show);
   }
 
   void setLineLength(int columns) {
-    double w = columns * getCharWidthPx();
-    columnMarginA.getStyle().setMarginLeft(w, Style.Unit.PX);
-    columnMarginB.getStyle().setMarginLeft(w, Style.Unit.PX);
-  }
-
-  double getLineHeightPx() {
-    if (lineHeightPx <= 1) {
-      Element p = DOM.createDiv();
-      int lines = 1;
-      for (int i = 0; i < lines; i++) {
-        Element e = DOM.createDiv();
-        p.appendChild(e);
-
-        Element pre = DOM.createElement("pre");
-        pre.setInnerText("gqyŚŻŹŃ");
-        e.appendChild(pre);
-      }
-
-      cmB.getMeasureElement().appendChild(p);
-      lineHeightPx = ((double) p.getOffsetHeight()) / lines;
-      p.removeFromParent();
-    }
-    return lineHeightPx;
-  }
-
-  private double getCharWidthPx() {
-    if (charWidthPx <= 1) {
-      int len = 100;
-      StringBuilder s = new StringBuilder();
-      for (int i = 0; i < len; i++) {
-        s.append('m');
-      }
-      Element e = DOM.createSpan();
-      e.getStyle().setDisplay(Style.Display.INLINE_BLOCK);
-      e.setInnerText(s.toString());
-
-      cmA.getMeasureElement().appendChild(e);
-      double a = ((double) e.getOffsetWidth()) / len;
-      e.removeFromParent();
-
-      cmB.getMeasureElement().appendChild(e);
-      double b = ((double) e.getOffsetWidth()) / len;
-      e.removeFromParent();
-      charWidthPx = Math.max(a, b);
-    }
-    return charWidthPx;
+    cmA.extras().lineLength(columns);
+    cmB.extras().lineLength(columns);
   }
 
   void setShowLineNumbers(boolean b) {
@@ -736,11 +738,14 @@
       public void run() {
         skipManager.removeAll();
         skipManager.render(context, diff);
-        diffTable.overview.refresh();
       }
     });
   }
 
+  void setAutoHideDiffHeader(boolean hide) {
+    scrollSynchronizer.setAutoHideDiffTableHeader(hide);
+  }
+
   private void render(DiffInfo diff) {
     header.setNoDiff(diff);
     chunkManager.render(diff);
@@ -758,18 +763,10 @@
     return chunkManager.getLineMapper().lineOnOther(side, line);
   }
 
-  private void clearActiveLine(CodeMirror cm) {
-    if (cm.hasActiveLine()) {
-      LineHandle activeLine = cm.getActiveLine();
-      cm.removeLineClass(activeLine,
-          LineClassWhere.WRAP, DiffTable.style.activeLine());
-      cm.setActiveLine(null);
-    }
-  }
-
   private Runnable updateActiveLine(final CodeMirror cm) {
     final CodeMirror other = otherCm(cm);
     return new Runnable() {
+      @Override
       public void run() {
         // The rendering of active lines has to be deferred. Reflow
         // caused by adding and removing styles chokes Firefox when arrow
@@ -780,25 +777,20 @@
           @Override
           public void execute() {
             operation(new Runnable() {
+              @Override
               public void run() {
-                LineHandle handle = cm.getLineHandleVisualStart(
-                    cm.getCursor("end").getLine());
-                if (cm.hasActiveLine() && cm.getActiveLine().equals(handle)) {
+                LineHandle handle =
+                    cm.getLineHandleVisualStart(cm.getCursor("end").line());
+                if (!cm.extras().activeLine(handle)) {
                   return;
                 }
 
-                clearActiveLine(cm);
-                clearActiveLine(other);
-                cm.setActiveLine(handle);
-                cm.addLineClass(
-                    handle, LineClassWhere.WRAP, DiffTable.style.activeLine());
                 LineOnOtherInfo info =
                     lineOnOther(cm.side(), cm.getLineNumber(handle));
                 if (info.isAligned()) {
-                  LineHandle oLineHandle = other.getLineHandle(info.getLine());
-                  other.setActiveLine(oLineHandle);
-                  other.addLineClass(oLineHandle, LineClassWhere.WRAP,
-                      DiffTable.style.activeLine());
+                  other.extras().activeLine(other.getLineHandle(info.getLine()));
+                } else {
+                  other.extras().clearActiveLine();
                 }
               }
             });
@@ -811,21 +803,18 @@
   private GutterClickHandler onGutterClick(final CodeMirror cm) {
     return new GutterClickHandler() {
       @Override
-      public void handle(CodeMirror instance, int line, String gutter,
+      public void handle(CodeMirror instance, final int line, String gutter,
           NativeEvent clickEvent) {
         if (clickEvent.getButton() == NativeEvent.BUTTON_LEFT
             && !clickEvent.getMetaKey()
             && !clickEvent.getAltKey()
             && !clickEvent.getCtrlKey()
             && !clickEvent.getShiftKey()) {
-          if (!(cm.hasActiveLine() &&
-              cm.getLineNumber(cm.getActiveLine()) == line)) {
-            cm.setCursor(LineCharacter.create(line));
-          }
+          cm.setCursor(Pos.create(line));
           Scheduler.get().scheduleDeferred(new ScheduledCommand() {
             @Override
             public void execute() {
-              commentManager.insertNewDraft(cm).run();
+              commentManager.newDraft(cm, line + 1);
             }
           });
         }
@@ -835,6 +824,7 @@
 
   private Runnable upToChange(final boolean openReplyBox) {
     return new Runnable() {
+      @Override
       public void run() {
         CallbackGroup group = new CallbackGroup();
         commentManager.saveAllDrafts(group);
@@ -842,11 +832,12 @@
         group.addListener(new GerritCallback<Void>() {
           @Override
           public void onSuccess(Void result) {
-            String b = base != null ? String.valueOf(base.get()) : null;
-            String rev = String.valueOf(revision.get());
+            String b = base != null ? base.getId() : null;
+            String rev = revision.getId();
             Gerrit.display(
               PageLinks.toChange(changeId, b, rev),
-              new ChangeScreen2(changeId, b, rev, openReplyBox));
+              new ChangeScreen(changeId, b, rev, openReplyBox,
+                  FileTable.Mode.REVIEW));
           }
         });
       }
@@ -865,11 +856,12 @@
 
     final DisplaySide sideSrc = cmSrc.side();
     return new Runnable() {
+      @Override
       public void run() {
-        if (cmSrc.hasActiveLine()) {
-          cmDst.setCursor(LineCharacter.create(lineOnOther(
+        if (cmSrc.extras().hasActiveLine()) {
+          cmDst.setCursor(Pos.create(lineOnOther(
               sideSrc,
-              cmSrc.getLineNumber(cmSrc.getActiveLine())).getLine()));
+              cmSrc.getLineNumber(cmSrc.extras().activeLine())).getLine()));
         }
         cmDst.focus();
       }
@@ -880,8 +872,8 @@
     return new Runnable() {
       @Override
       public void run() {
-        if (cm.hasVimSearchHighlight()) {
-          CodeMirror.handleVimKey(cm, "N");
+        if (cm.vim().hasSearchHighlight()) {
+          cm.vim().handleKey("N");
         } else {
           commentManager.commentNav(cm, Direction.NEXT).run();
         }
@@ -893,8 +885,8 @@
     return new Runnable() {
       @Override
       public void run() {
-        if (cm.hasVimSearchHighlight()) {
-          CodeMirror.handleVimKey(cm, "n");
+        if (cm.vim().hasSearchHighlight()) {
+          cm.vim().handleKey("n");
         } else {
           chunkManager.diffChunkNav(cm, Direction.NEXT).run();
         }
@@ -902,6 +894,69 @@
     };
   }
 
+  private int adjustCommitMessageLine(int line) {
+    /* When commit messages are shown in the side-by-side screen they include
+      a header block that looks like this:
+
+      1 Parent:     deadbeef (Parent commit title)
+      2 Author:     A. U. Thor <author@example.com>
+      3 AuthorDate: 2015-02-27 19:20:52 +0900
+      4 Commit:     A. U. Thor <author@example.com>
+      5 CommitDate: 2015-02-27 19:20:52 +0900
+      6 [blank line]
+      7 Commit message title
+      8
+      9 Commit message body
+     10 ...
+     11 ...
+
+    If the commit is a merge commit, both parent commits are listed in the
+    first two lines instead of a 'Parent' line:
+
+      1 Merge Of:   deadbeef (Parent 1 commit title)
+      2             beefdead (Parent 2 commit title)
+
+    */
+
+    // Offset to compensate for header lines until the blank line
+    // after 'CommitDate'
+    int offset = 6;
+
+    // Adjust for merge commits, which have two parent lines
+    if (diff.text_b().startsWith("Merge")) {
+      offset += 1;
+    }
+
+    // If the cursor is inside the header line, reset to the first line of the
+    // commit message. Otherwise if the cursor is on an actual line of the commit
+    // message, adjust the line number to compensate for the header lines, so the
+    // focus is on the correct line.
+    if (line <= offset) {
+      return 1;
+    } else {
+      return line - offset;
+    }
+  }
+
+  private Runnable openEditScreen(final CodeMirror cm) {
+    return new Runnable() {
+      @Override
+      public void run() {
+        LineHandle handle = cm.extras().activeLine();
+        int line = cm.getLineNumber(handle) + 1;
+        if (Patch.COMMIT_MSG.equals(path)) {
+          line = adjustCommitMessageLine(line);
+        }
+        String token = Dispatcher.toEditScreen(revision, path, line);
+        if (!Gerrit.isSignedIn()) {
+          Gerrit.doSignIn(token);
+        } else {
+          Gerrit.display(token);
+        }
+      }
+    };
+  }
+
   void updateRenderEntireFile() {
     cmA.removeKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
     cmB.removeKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
@@ -915,18 +970,9 @@
   }
 
   void resizeCodeMirror() {
-    int height = getCodeMirrorHeight();
-    cmA.setHeight(height);
-    cmB.setHeight(height);
-    diffTable.overview.refresh();
-  }
-
-  private int getCodeMirrorHeight() {
-    int rest = Gerrit.getHeaderFooterHeight()
-        + header.getOffsetHeight()
-        + diffTable.getHeaderHeight()
-        + 5; // Estimate
-    return Window.getClientHeight() - rest;
+    int hdr = header.getOffsetHeight() + diffTable.getHeaderHeight();
+    cmA.adjustHeight(hdr);
+    cmB.adjustHeight(hdr);
   }
 
   void syncScroll(DisplaySide masterSide) {
@@ -936,11 +982,12 @@
   }
 
   private String getContentType(DiffInfo.FileMeta meta) {
-    return prefs.syntaxHighlighting()
-          && meta != null
-          && meta.content_type() != null
-        ? ModeInjector.getContentType(meta.content_type())
-        : null;
+    if (prefs.syntaxHighlighting() && meta != null
+        && meta.content_type() != null) {
+     ModeInfo m = ModeInfo.findMode(meta.content_type(), path);
+     return m != null ? m.mime() : null;
+   }
+   return null;
   }
 
   private void injectMode(DiffInfo diffInfo, AsyncCallback<Void> cb) {
@@ -950,6 +997,10 @@
       .inject(cb);
   }
 
+  String getPath() {
+    return path;
+  }
+
   DiffPreferences getPrefs() {
     return prefs;
   }
@@ -1021,7 +1072,7 @@
               public void run() {
                 skipManager.removeAll();
                 chunkManager.reset();
-                diffTable.overview.clearDiffMarkers();
+                diffTable.scrollbar.removeDiffAnnotations();
                 setShowIntraline(prefs.intralineDifference());
                 render(diff);
                 chunkManager.adjustPadding();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml
similarity index 94%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.ui.xml
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml
index 8c26873..da5b351 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml
@@ -18,12 +18,12 @@
     xmlns:g='urn:import:com.google.gwt.user.client.ui'
     xmlns:d='urn:import:com.google.gerrit.client.diff'>
   <ui:style>
-    .sbs2 {
+    .sbs {
       margin-left: -5px;
       margin-right: -5px;
     }
   </ui:style>
-  <g:FlowPanel styleName='{style.sbs2}'>
+  <g:FlowPanel styleName='{style.sbs}'>
     <d:Header ui:field='header'/>
     <d:DiffTable ui:field='diffTable'/>
   </g:FlowPanel>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
index 4efaf54..4c12cc0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
@@ -30,6 +30,7 @@
 import net.codemirror.lib.CodeMirror;
 import net.codemirror.lib.Configuration;
 import net.codemirror.lib.LineWidget;
+import net.codemirror.lib.Pos;
 import net.codemirror.lib.TextMarker;
 import net.codemirror.lib.TextMarker.FromTo;
 
@@ -95,8 +96,8 @@
     }
 
     textMarker = cm.markText(
-        CodeMirror.pos(start, 0),
-        CodeMirror.pos(end),
+        Pos.create(start, 0),
+        Pos.create(end),
         Configuration.create()
           .set("collapsed", true)
           .set("inclusiveLeft", true)
@@ -133,14 +134,13 @@
   void expandBefore(int cnt) {
     expandSideBefore(cnt);
     otherBar.expandSideBefore(cnt);
-    manager.getOverviewBar().refresh();
   }
 
   private void expandSideBefore(int cnt) {
     FromTo range = textMarker.find();
-    int oldStart = range.getFrom().getLine();
+    int oldStart = range.from().line();
     int newStart = oldStart + cnt;
-    int end = range.getTo().getLine();
+    int end = range.to().line();
     clearMarkerAndWidget();
     collapse(newStart, end, true);
     updateSelection();
@@ -153,8 +153,8 @@
 
   private void expandAfter() {
     FromTo range = textMarker.find();
-    int start = range.getFrom().getLine();
-    int oldEnd = range.getTo().getLine();
+    int start = range.from().line();
+    int oldEnd = range.to().line();
     int newEnd = oldEnd - NUM_ROWS_TO_EXPAND;
     boolean attach = start == 0;
     if (attach) {
@@ -169,12 +169,12 @@
   private void updateSelection() {
     if (cm.somethingSelected()) {
       FromTo sel = cm.getSelectedRange();
-      cm.setSelection(sel.getFrom(), sel.getTo());
+      cm.setSelection(sel.from(), sel.to());
     }
   }
 
   @UiHandler("skipNum")
-  void onExpandAll(ClickEvent e) {
+  void onExpandAll(@SuppressWarnings("unused") ClickEvent e) {
     expandAll();
     updateSelection();
     otherBar.updateSelection();
@@ -185,20 +185,18 @@
     expandSideAll();
     otherBar.expandSideAll();
     manager.remove(this, otherBar);
-    manager.getOverviewBar().refresh();
   }
 
   @UiHandler("upArrow")
-  void onExpandBefore(ClickEvent e) {
+  void onExpandBefore(@SuppressWarnings("unused") ClickEvent e) {
     expandBefore(NUM_ROWS_TO_EXPAND);
     cm.focus();
   }
 
   @UiHandler("downArrow")
-  void onExpandAfter(ClickEvent e) {
+  void onExpandAfter(@SuppressWarnings("unused") ClickEvent e) {
     expandAfter();
     otherBar.expandAfter();
-    manager.getOverviewBar().refresh();
     cm.focus();
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
index 5ba275f..4972f5d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
@@ -26,22 +26,18 @@
 import java.util.List;
 import java.util.Set;
 
-/** Collapses common regions with {@link SkipBar} for {@link SideBySide2}. */
+/** Collapses common regions with {@link SkipBar} for {@link SideBySide}. */
 class SkipManager {
-  private final SideBySide2 host;
+  private final SideBySide host;
   private final CommentManager commentManager;
   private Set<SkipBar> skipBars;
   private SkipBar line0;
 
-  SkipManager(SideBySide2 host, CommentManager commentManager) {
+  SkipManager(SideBySide host, CommentManager commentManager) {
     this.host = host;
     this.commentManager = commentManager;
   }
 
-  OverviewBar getOverviewBar() {
-    return host.diffTable.overview;
-  }
-
   void render(int context, DiffInfo diff) {
     if (context == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
       return;
@@ -111,7 +107,6 @@
       for (SkipBar bar : skipBars) {
         bar.expandSideAll();
       }
-      getOverviewBar().refresh();
       skipBars = null;
       line0 = null;
     }
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/UpToChangeCommand.java
similarity index 88%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand2.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand.java
index 7071e7f..5d55494 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/UpToChangeCommand.java
@@ -21,10 +21,10 @@
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 
-class UpToChangeCommand2 extends KeyCommand {
+class UpToChangeCommand extends KeyCommand {
   private final PatchSet.Id revision;
 
-  UpToChangeCommand2(PatchSet.Id revision, int mask, int key) {
+  UpToChangeCommand(PatchSet.Id revision, int mask, int key) {
     super(mask, key, PatchUtil.C.upToChange());
     this.revision = revision;
   }
@@ -33,6 +33,6 @@
   public void onKeyPress(final KeyPressEvent event) {
     Gerrit.display(PageLinks.toChange(
         revision.getParentKey(),
-        String.valueOf(revision.get())));
+        revision.getId()));
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/gear.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/gear.png
deleted file mode 100644
index 2f84e47..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/gear.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/go-next.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/goNext.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/go-next.png
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/goNext.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/go-prev.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/goPrev.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/go-prev.png
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/goPrev.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/go-up.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/goUp.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/go-up.png
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/goUp.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocTable.java
index f176372..b57cdac 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/documentation/DocTable.java
@@ -16,9 +16,9 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.ui.NavigationTable;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandLink.java
index a323a76..cea9106 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandLink.java
@@ -129,7 +129,7 @@
       protected void setCurrentUrl(DownloadUrlLink link) {
         widget.setVisible(true);
 
-        String sshPort = "29418";
+        String sshPort = null;
         String sshAddr = Gerrit.getConfig().getSshdAddress();
         int p = sshAddr.lastIndexOf(':');
         if (p != -1 && !sshAddr.endsWith(":")) {
@@ -139,9 +139,12 @@
         StringBuilder cmd = new StringBuilder();
         cmd.append("git clone ");
         cmd.append(link.getUrlData());
-        cmd.append(" && scp -p -P ");
-        cmd.append(sshPort);
-        cmd.append(" ");
+        cmd.append(" && scp -p ");
+        if (sshPort != null) {
+          cmd.append("-P ");
+          cmd.append(sshPort);
+          cmd.append(" ");
+        }
         cmd.append(Gerrit.getUserAccount().getUserName());
         cmd.append("@");
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlLink.java
index 845537e..ce5c060 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlLink.java
@@ -19,9 +19,9 @@
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gwt.aria.client.Roles;
+import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.core.client.GWT;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Widget;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditConstants.java
new file mode 100644
index 0000000..e95fdfc
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditConstants.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.editor;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.i18n.client.Constants;
+
+interface EditConstants extends Constants {
+  static final EditConstants I = GWT.create(EditConstants.class);
+
+  String closeUnsavedChanges();
+  String cancelUnsavedChanges();
+
+  String gotoLineNumber();
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditConstants.properties
new file mode 100644
index 0000000..2e8a087
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditConstants.properties
@@ -0,0 +1,7 @@
+closeUnsavedChanges = Unsaved changes were made to this file.
+
+cancelUnsavedChanges = Unsaved changes were made to this file.\n\
+  \n\
+  Discard unsaved changes?
+
+gotoLineNumber = Go to Line:
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditFileInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditFileInfo.java
new file mode 100644
index 0000000..c833c5d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditFileInfo.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.editor;
+
+import com.google.gerrit.client.DiffWebLinkInfo;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+
+public class EditFileInfo extends JavaScriptObject {
+  public final native JsArray<DiffWebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
+
+  protected EditFileInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
new file mode 100644
index 0000000..dd36657
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
@@ -0,0 +1,477 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.editor;
+
+import static com.google.gwt.dom.client.Style.Visibility.HIDDEN;
+import static com.google.gwt.dom.client.Style.Visibility.VISIBLE;
+
+import com.google.gerrit.client.DiffWebLinkInfo;
+import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.JumpKeys;
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.account.DiffPreferences;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeEditApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.diff.DiffApi;
+import com.google.gerrit.client.diff.DiffInfo;
+import com.google.gerrit.client.diff.FileInfo;
+import com.google.gerrit.client.diff.Header;
+import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.HttpCallback;
+import com.google.gerrit.client.rpc.HttpResponse;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.client.ui.Screen;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.logical.shared.ResizeEvent;
+import com.google.gwt.event.logical.shared.ResizeHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+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.Window;
+import com.google.gwt.user.client.Window.ClosingEvent;
+import com.google.gwt.user.client.Window.ClosingHandler;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.ImageResourceRenderer;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.CodeMirror.ChangesHandler;
+import net.codemirror.lib.Configuration;
+import net.codemirror.lib.KeyMap;
+import net.codemirror.lib.Pos;
+import net.codemirror.mode.ModeInfo;
+import net.codemirror.mode.ModeInjector;
+import net.codemirror.theme.ThemeLoader;
+
+import java.util.List;
+
+public class EditScreen extends Screen {
+  interface Binder extends UiBinder<HTMLPanel, EditScreen> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  private final PatchSet.Id base;
+  private final PatchSet.Id revision;
+  private final String path;
+  private final int startLine;
+  private DiffPreferences prefs;
+  private CodeMirror cm;
+  private HttpResponse<NativeString> content;
+  private EditFileInfo editFileInfo;
+  private JsArray<DiffWebLinkInfo> diffLinks;
+
+  @UiField Element header;
+  @UiField Element project;
+  @UiField Element filePath;
+  @UiField FlowPanel linkPanel;
+  @UiField Element cursLine;
+  @UiField Element cursCol;
+  @UiField Element dirty;
+  @UiField Button close;
+  @UiField Button save;
+  @UiField Element editor;
+
+  private HandlerRegistration resizeHandler;
+  private HandlerRegistration closeHandler;
+  private int generation;
+
+  public EditScreen(PatchSet.Id base, Patch.Key patch, int startLine) {
+    this.base = base;
+    this.revision = patch.getParentKey();
+    this.path = patch.get();
+    this.startLine = startLine - 1;
+    prefs = DiffPreferences.create(Gerrit.getAccountDiffPreference());
+    add(uiBinder.createAndBindUi(this));
+    addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
+  }
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    setHeaderVisible(false);
+    setWindowTitle(FileInfo.getFileName(path));
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+
+    CallbackGroup group1 = new CallbackGroup();
+    final CallbackGroup group2 = new CallbackGroup();
+    final CallbackGroup group3 = new CallbackGroup();
+
+    CodeMirror.initLibrary(group1.add(new AsyncCallback<Void>() {
+      final AsyncCallback<Void> themeCallback = group3.addEmpty();
+
+      @Override
+      public void onSuccess(Void result) {
+        // Load theme after CM library to ensure theme can override CSS.
+        ThemeLoader.loadTheme(prefs.theme(), themeCallback);
+
+        group2.done();
+        group3.done();
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+      }
+    }));
+
+    ChangeApi.detail(revision.getParentKey().get(),
+        group1.add(new AsyncCallback<ChangeInfo>() {
+          @Override
+          public void onSuccess(ChangeInfo c) {
+            project.setInnerText(c.project());
+            SafeHtml.setInnerHTML(filePath, Header.formatPath(path, null, null));
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+          }
+        }));
+
+
+    if (revision.get() == 0) {
+      ChangeEditApi.getMeta(revision, path,
+          group1.add(new AsyncCallback<EditFileInfo>() {
+            @Override
+            public void onSuccess(EditFileInfo editInfo) {
+              editFileInfo = editInfo;
+            }
+
+            @Override
+            public void onFailure(Throwable e) {
+            }
+          }));
+    } else {
+      // TODO(davido): We probably want to create dedicated GET EditScreenMeta
+      // REST endpoint. Abuse GET diff for now, as it retrieves links we need.
+      DiffApi.diff(revision, path)
+        .base(base)
+        .webLinksOnly()
+        .get(group1.add(new AsyncCallback<DiffInfo>() {
+          @Override
+          public void onSuccess(DiffInfo diffInfo) {
+            diffLinks = diffInfo.web_links();
+          }
+
+          @Override
+          public void onFailure(Throwable e) {
+          }
+      }));
+    }
+
+    ChangeEditApi.get(revision, path,
+        group2.add(new HttpCallback<NativeString>() {
+          final AsyncCallback<Void> modeCallback = group3.addEmpty();
+
+          @Override
+          public void onSuccess(HttpResponse<NativeString> fc) {
+            content = fc;
+            if (prefs.syntaxHighlighting()) {
+              injectMode(fc.getContentType(), modeCallback);
+            } else {
+              modeCallback.onSuccess(null);
+            }
+          }
+
+          @Override
+          public void onFailure(Throwable e) {
+            // "Not Found" means it's a new file.
+            if (RestApi.isNotFound(e)) {
+              content = null;
+              modeCallback.onSuccess(null);
+            } else {
+              GerritCallback.showFailure(e);
+            }
+          }
+        }));
+
+    group3.addListener(new ScreenLoadCallback<Void>(this) {
+      @Override
+      protected void preDisplay(Void result) {
+        initEditor(content);
+        content = null;
+
+        renderLinks(editFileInfo, diffLinks);
+        editFileInfo = null;
+        diffLinks = null;
+      }
+    });
+    group1.done();
+  }
+
+  @Override
+  public void registerKeys() {
+    super.registerKeys();
+    cm.addKeyMap(KeyMap.create()
+        .on("Ctrl-L", gotoLine())
+        .on("Cmd-L", gotoLine()));
+  }
+
+  private Runnable gotoLine() {
+    return new Runnable() {
+      @Override
+      public void run() {
+        String n = Window.prompt(EditConstants.I.gotoLineNumber(), "");
+        if (n != null) {
+          try {
+            int line = Integer.parseInt(n);
+            line--;
+            if (line >= 0) {
+              cm.scrollToLine(line);
+            }
+          } catch (NumberFormatException e) {
+            // ignore non valid numbers
+            // We don't want to popup another ugly dialog just to say
+            // "The number you've provided is invalid, try again"
+          }
+        }
+      }
+    };
+  }
+
+  @Override
+  public void onShowView() {
+    super.onShowView();
+    Window.enableScrolling(false);
+    JumpKeys.enable(false);
+    if (prefs.hideTopMenu()) {
+      Gerrit.setHeaderVisible(false);
+    }
+    resizeHandler = Window.addResizeHandler(new ResizeHandler() {
+      @Override
+      public void onResize(ResizeEvent event) {
+        cm.adjustHeight(header.getOffsetHeight());
+      }
+    });
+    closeHandler = Window.addWindowClosingHandler(new ClosingHandler() {
+      @Override
+      public void onWindowClosing(ClosingEvent event) {
+        if (!cm.isClean(generation)) {
+          event.setMessage(EditConstants.I.closeUnsavedChanges());
+        }
+      }
+    });
+
+    generation = cm.changeGeneration(true);
+    setClean(true);
+    cm.on(new ChangesHandler() {
+      @Override
+      public void handle(CodeMirror cm) {
+        setClean(cm.isClean(generation));
+      }
+    });
+
+    cm.adjustHeight(header.getOffsetHeight());
+    cm.on("cursorActivity", updateCursorPosition());
+    cm.extras().showTabs(prefs.showTabs());
+    cm.extras().lineLength(
+        Patch.COMMIT_MSG.equals(path) ? 72 : prefs.lineLength());
+    cm.refresh();
+    cm.focus();
+
+    if (startLine > 0) {
+      cm.scrollToLine(startLine);
+    }
+    updateActiveLine();
+  }
+
+  @Override
+  protected void onUnload() {
+    super.onUnload();
+    if (cm != null) {
+      cm.getWrapperElement().removeFromParent();
+    }
+    if (resizeHandler != null) {
+      resizeHandler.removeHandler();
+    }
+    if (closeHandler != null) {
+      closeHandler.removeHandler();
+    }
+    Window.enableScrolling(true);
+    Gerrit.setHeaderVisible(true);
+    JumpKeys.enable(true);
+  }
+
+  @UiHandler("save")
+  void onSave(@SuppressWarnings("unused") ClickEvent e) {
+    save().run();
+  }
+
+  @UiHandler("close")
+  void onClose(@SuppressWarnings("unused") ClickEvent e) {
+    if (cm.isClean(generation)
+        || Window.confirm(EditConstants.I.cancelUnsavedChanges())) {
+      upToChange();
+    }
+  }
+
+  private void upToChange() {
+    Gerrit.display(PageLinks.toChangeInEditMode(revision.getParentKey()));
+  }
+
+  private void initEditor(HttpResponse<NativeString> file) {
+    ModeInfo mode = null;
+    String content = "";
+    if (file != null && file.getResult() != null) {
+      content = file.getResult().asString();
+      if (prefs.syntaxHighlighting()) {
+        mode = ModeInfo.findMode(file.getContentType(), path);
+      }
+    }
+    cm = CodeMirror.create(editor, Configuration.create()
+        .set("value", content)
+        .set("readOnly", false)
+        .set("cursorBlinkRate", 0)
+        .set("cursorHeight", 0.85)
+        .set("lineNumbers", true)
+        .set("tabSize", prefs.tabSize())
+        .set("lineWrapping", false)
+        .set("scrollbarStyle", "overlay")
+        .set("styleSelectedText", true)
+        .set("showTrailingSpace", true)
+        .set("keyMap", "default")
+        .set("theme", prefs.theme().name().toLowerCase())
+        .set("mode", mode != null ? mode.mode() : null));
+    cm.addKeyMap(KeyMap.create()
+        .on("Cmd-S", save())
+        .on("Ctrl-S", save()));
+  }
+
+  private void renderLinks(EditFileInfo editInfo,
+      JsArray<DiffWebLinkInfo> diffLinks) {
+    renderLinksToDiff();
+
+    if (editInfo != null) {
+      renderLinks(Natives.asList(editInfo.web_links()));
+    } else if (diffLinks != null) {
+      renderLinks(Natives.asList(diffLinks));
+    }
+  }
+
+  private void renderLinks(List<DiffWebLinkInfo> links) {
+    if (links != null) {
+      for (DiffWebLinkInfo webLink : links) {
+        linkPanel.add(webLink.toAnchor());
+      }
+    }
+  }
+
+  private void renderLinksToDiff() {
+    InlineHyperlink sbs = new InlineHyperlink();
+    sbs.setHTML(new ImageResourceRenderer()
+        .render(Gerrit.RESOURCES.sideBySideDiff()));
+    sbs.setTargetHistoryToken(
+        Dispatcher.toPatch("sidebyside", base, new Patch.Key(revision, path)));
+    sbs.setTitle(PatchUtil.C.sideBySideDiff());
+    linkPanel.add(sbs);
+
+    InlineHyperlink unified = new InlineHyperlink();
+    unified.setHTML(new ImageResourceRenderer()
+        .render(Gerrit.RESOURCES.unifiedDiff()));
+    unified.setTargetHistoryToken(
+        Dispatcher.toPatch("unified", base, new Patch.Key(revision, path)));
+    unified.setTitle(PatchUtil.C.unifiedDiff());
+    linkPanel.add(unified);
+  }
+
+  private Runnable updateCursorPosition() {
+    return new Runnable() {
+      @Override
+      public void run() {
+        // The rendering of active lines has to be deferred. Reflow
+        // caused by adding and removing styles chokes Firefox when arrow
+        // key (or j/k) is held down. Performance on Chrome is fine
+        // without the deferral.
+        //
+        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+          @Override
+          public void execute() {
+            cm.operation(new Runnable() {
+              @Override
+              public void run() {
+                updateActiveLine();
+              }
+            });
+          }
+        });
+      }
+    };
+  }
+
+  private void updateActiveLine() {
+    Pos p = cm.getCursor("end");
+    cursLine.setInnerText(Integer.toString(p.line() + 1));
+    cursCol.setInnerText(Integer.toString(p.ch() + 1));
+    cm.extras().activeLine(cm.getLineHandleVisualStart(p.line()));
+  }
+
+  private void setClean(boolean clean) {
+    save.setEnabled(!clean);
+    close.setEnabled(true);
+    dirty.getStyle().setVisibility(!clean ? VISIBLE : HIDDEN);
+  }
+
+  private Runnable save() {
+    return new Runnable() {
+      @Override
+      public void run() {
+        if (!cm.isClean(generation)) {
+          close.setEnabled(false);
+          String text = cm.getValue();
+          final int g = cm.changeGeneration(false);
+          ChangeEditApi.put(revision.getParentKey().get(), path, text,
+              new GerritCallback<VoidResult>() {
+                @Override
+                public void onSuccess(VoidResult result) {
+                  generation = g;
+                  setClean(cm.isClean(g));
+                }
+                @Override
+                public void onFailure(final Throwable caught) {
+                  close.setEnabled(true);
+                }
+              });
+        }
+      }
+    };
+  }
+
+  private void injectMode(String type, AsyncCallback<Void> cb) {
+    new ModeInjector().add(type).inject(cb);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml
new file mode 100644
index 0000000..342cd77
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml
@@ -0,0 +1,140 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+  <ui:style>
+    .header {
+      position: relative;
+      height: 16px;
+      line-height: 16px;
+    }
+
+    .headerLine {
+      background-color: #f7f7f7;
+      border-bottom: 1px solid #ddd;
+      padding-left: 30px;
+    }
+
+    .headerButtons {
+      display: inline-block;
+      padding-right: 5px;
+      border-right: 1px inset #ddd;
+      margin-right: 5px;
+    }
+
+    .headerButtons button:disabled {
+      background-color: #ddd;
+      font-weight: normal;
+      cursor: default;
+    }
+
+    .headerButtons button {
+      margin: 2px 0 2px 0;
+      text-align: center;
+      font-size: 8pt;
+      cursor: pointer;
+      border: 1px solid;
+      color: rgba(0, 0, 0, 0.15);
+      background-color: #f5f5f5;
+      -webkit-border-radius: 1px;
+      -webkit-box-sizing: content-box;
+    }
+
+    .headerButtons button div {
+      color: #444;
+      min-width: 54px;
+      white-space: nowrap;
+      line-height: 8pt;
+    }
+
+    .save {
+      font-weight: bold;
+    }
+
+    .path {
+      white-space: nowrap;
+    }
+
+    .statusLine {
+      position: fixed;
+      bottom: 0;
+      left: 0;
+      width: 175px;
+      height: 19px;
+      background-color: #f7f7f7;
+      border-top: 1px solid #ddd;
+      border-right: 1px solid #ddd;
+    }
+    .statusLine div {
+      height: inherit;
+    }
+
+    .cursorPosition {
+      display: inline-block;
+      margin: 0 5px 0 35px;
+      white-space: nowrap;
+    }
+
+    .dirty {
+      display: inline-block;
+      margin: 0 5px 0 5px;
+      padding: 0 0 0 5px;
+      border-left: 1px solid #ddd;
+      font-weight: bold;
+    }
+
+    .navigation {
+      position: absolute;
+      top: 0;
+      right: 10px;
+    }
+    .linkPanel {
+      float: left;
+    }
+    .linkPanel img {
+      padding-top: 2px;
+      padding-right: 3px;
+    }
+  </ui:style>
+  <g:HTMLPanel styleName='{style.header}'>
+    <div class='{style.headerLine}' ui:field='header'>
+       <div class='{style.headerButtons}'>
+         <g:Button ui:field='close'
+             styleName=''
+             title='Close file and return to change'>
+           <ui:attribute name='title'/>
+           <div><ui:msg>Close</ui:msg></div>
+         </g:Button>
+         <g:Button ui:field='save'
+             styleName='{style.save}'
+             title='Save'>
+           <ui:attribute name='title'/>
+           <div><ui:msg>Save</ui:msg></div>
+         </g:Button>
+       </div>
+       <span class='{style.path}'><span ui:field='project'/> / <span ui:field='filePath'/></span>
+       <div class='{style.navigation}'>
+         <g:FlowPanel ui:field='linkPanel' styleName='{style.linkPanel}'/>
+       </div>
+    </div>
+    <div ui:field='editor' />
+    <div class='{style.statusLine}'>
+      <div class='{style.cursorPosition}'><span ui:field='cursLine'/> : <span ui:field='cursCol'/></div>
+      <div class='{style.dirty}' ui:field='dirty'>Unsaved</div>
+    </div>
+  </g:HTMLPanel>
+</ui:UiBinder>
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 b3602a2..8d961ca 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
@@ -84,10 +84,6 @@
   color: black;
 }
 
-.hyperlink {
-  text-decoration: underline;
-}
-
 .accountLinkPanel {
   display: inline;
 }
@@ -105,10 +101,6 @@
   top: -1px;
 }
 
-.accountName {
-  white-space: nowrap;
-}
-
 .inputFieldTypeHint {
   color: grey;
 }
@@ -118,13 +110,6 @@
   font-weight: bold;
 }
 
-.blockHeader {
-  font-size: small;
-  font-weight: bold;
-  background: trimColor;
-  padding: 0.2em 0.2em 0.2em 0.5em;
-}
-
 .link {
   cursor: pointer;
 }
@@ -156,47 +141,6 @@
 }
 
 /** CommentPanel **/
-.commentPanelBorder {
-  border-top: 1px solid lightgray;
-  border-left: 1px solid lightgray;
-  border-right: 1px solid lightgray;
-  border-top-left-radius: 8px;
-  border-top-right-radius: 8px;
-  border-bottom-left-radius: 0px;
-  border-bottom-right-radius: 0px;
-}
-.commentPanelBorder.commentPanelLast {
-  border-bottom: 1px solid lightgray;
-  border-bottom-left-radius: 8px;
-  border-bottom-right-radius: 8px;
-}
-
-@if user.agent safari {
-  .commentPanelBorder {
-    \-webkit-border-top-left-radius: 8px;
-    \-webkit-border-top-right-radius: 8px;
-    \-webkit-border-bottom-left-radius: 0px;
-    \-webkit-border-bottom-right-radius: 0px;
-  }
-  .commentPanelBorder.commentPanelLast {
-    \-webkit-border-bottom-left-radius: 8px;
-    \-webkit-border-bottom-right-radius: 8px;
-  }
-}
-
-@if user.agent gecko1_8 {
-  .commentPanelBorder {
-    \-moz-border-radius-topleft: 8px;
-    \-moz-border-radius-topright: 8px;
-    \-moz-border-radius-bottomleft: 0;
-    \-moz-border-radius-bottomright: 0;
-  }
-  .commentPanelBorder.commentPanelLast {
-    \-moz-border-radius-bottomleft: 8px;
-    \-moz-border-radius-bottomright: 8px;
-  }
-}
-
 .commentPanelHeader {
   cursor: pointer;
   width: 100%;
@@ -224,14 +168,12 @@
   padding-left: 0.5em;
   padding-right: 0.5em;
 }
-.commentPanelMenuBar {
-  float: right;
-}
 .commentPanelMessage p {
   margin-top: 0px;
   margin-bottom: 0px;
   padding-top: 0.5em;
   padding-bottom: 0.5em;
+  max-height: 100000px;
 }
 .commentPanelButtons {
   margin-left: 0.5em;
@@ -427,10 +369,6 @@
   width: 100%;
   margin-top: 15px;
 }
-.errorDialogText {
-  font-size: 15px;
-  font-family: verdana;
-}
 .errorDialog a,
 .errorDialog a:visited,
 .errorDialog a:hover {
@@ -458,10 +396,6 @@
   overflow: hidden;
 }
 
-.screenNoHeader {
-  margin-top: 5px;
-}
-
 /** ChangeTable **/
 .changeTable {
   border-collapse: separate;
@@ -476,10 +410,6 @@
   background: tableOddRowColor;
 }
 
-.changeTable .outdated {
-  background: changeTableOutdatedColor !important;
-}
-
 .changeTable .iconCell {
   width: 1px;
   padding: 0px;
@@ -674,15 +604,15 @@
 
 
 /** PatchScreen **/
-.patchScreenDisplayControls .gwt-CheckBox {
-  margin-right: 1em;
-}
-
 .reviewedPanelBottom {
   float: right;
   font-size: small;
 }
 
+.linkPanel img {
+  padding-right: 3px;
+}
+
 
 /** PatchContentTable **/
 .patchContentTable {
@@ -711,10 +641,6 @@
   border-left: thin solid #b0bdcc;
 }
 
-.patchContentTable .diffTextForBinaryInSideBySide {
- width: 50%;
-}
-
 .patchContentTable .diffTextFileHeader {
   color: grey;
   font-weight: bold;
@@ -782,9 +708,6 @@
   background: white;
   border-bottom: 1px solid white;
 }
-.lineNumber.rightmost {
-  border-left: thin solid #b0bdcc;
-}
 .lineNumber.rightBorder {
   border-right: thin solid #b0bdcc;
 }
@@ -804,14 +727,6 @@
 .lineNumber.fileColumnHeader {
   border-bottom: 1px solid trimColor;
 }
-.noLineLineNumber {
-  font-family: mono-font;
-  width: 3.5em;
-  padding-left: 0.2em;
-  padding-right: 0.2em;
-  background: white;
-  border-bottom: 1px solid white;
-}
 
 .fileLine {
   padding-left: 0;
@@ -819,22 +734,11 @@
   white-space: pre;
   border-left: thin solid #b0bdcc;
 }
-.fileLineNone {
-  background: #eeeeee;
-  border-bottom: 1px solid #eeeeee;
-}
-.fileLineMode {
-  font-weight: bold;
-}
 .fileLineDELETE,
 .fileLineDELETE .wdc {
   background: #ffeeee;
   border-bottom: 1px solid #ffeeee;
 }
-.fileLineCONTEXT {
-  background: white;
-  border-bottom: 1px solid white;
-}
 .fileLineINSERT,
 .fileLineINSERT .wdc {
   background: #ddffdd;
@@ -847,27 +751,6 @@
   border-bottom: 1px solid #9F9;
 }
 
-.patchContentTable .skipLine .iconCell,
-.patchContentTable .skipLine {
-  font-family: norm-font;
-  text-align: center;
-  font-style: italic;
-  background: #def;
-  color: grey;
-}
-.patchContentTable .skipLine div {
-  display: inline;
-}
-.patchContentTable a.skipLine {
-  color: grey;
-  text-decoration: none;
-}
-.patchContentTable a:hover.skipLine {
-  background: white;
-  color: #00A;
-  text-decoration: underline;
-}
-
 .patchContentTable td.cellsNextToFileComment {
   background: trimColor;
   border-top: trimColor;
@@ -903,36 +786,6 @@
   margin-right: 5px;
 }
 
-.changeScreen .gwt-DisclosurePanel .header td {
-  font-weight: bold;
-  white-space: nowrap;
-}
-
-.changeScreen .gwt-DisclosurePanel .complexHeader {
-  white-space: nowrap;
-}
-.changeScreen .gwt-DisclosurePanel .complexHeader span {
-  white-space: nowrap;
-}
-
-.patchSetRevision {
-  padding-left: 20px;
-  font-size: 8pt;
-}
-
-.patchSetLink {
-  padding-left: 0.5em;
-  font-size: 8pt;
-}
-
-.changeScreen .gwt-DisclosurePanel .content {
-  margin-bottom: 10px;
-}
-
-.gwt-DisclosurePanel .content {
-  margin-left: 10px;
-}
-
 .changeScreenDescription,
 .changeScreenDescription textarea {
   white-space: pre;
@@ -944,81 +797,6 @@
   padding-top: 0.5em;
 }
 
-.changeComments {
-  padding-top: 1em;
-  width: 60em;
-}
-
-.infoTable {
-  border-collapse: collapse;
-  border-spacing: 0;
-}
-
-.infoTable td {
-  border-left: 1px solid trimColor;
-  border-bottom: 1px solid trimColor;
-  padding: 2px 6px 1px;
-}
-
-.infoTable td.header {
-  background-color: trimColor;
-  font-weight: normal;
-  padding: 2px 4px 0 6px;
-  font-style: italic;
-  text-align: left;
-  vertical-align: top;
-  white-space: nowrap;
-}
-
-.rightmost  {
-  border-right: 1px solid trimColor;
-}
-
-.sideBySideTableBinaryHeader {
-  border-left:  thin solid #b0bdcc;
-  width: 100%;
-  color: grey;
-  font-weight: bold;
-}
-
-.infoTable td.approvalrole {
-  width: 5em;
-  border-left: none;
-  font-style: italic;
-  white-space: nowrap;
-}
-
-.infoTable td.approvalscore {
-  text-align: center;
-}
-.infoTable td.notVotable {
-  background: #F5F5F5;
-}
-.infoTable td.negscore {
-  color: red;
-}
-.infoTable td.posscore {
-  color: #08a400;
-}
-
-.infoTable td.approvalhint {
-  white-space: nowrap;
-  text-align: left;
-  color: #444444;
-}
-
-.changeInfoBlock {
-  margin-right: 15px;
-}
-
-.changeInfoTopicPanel img {
-  float: right;
-}
-
-.changeInfoTopicPanel a {
-  float: left;
-}
-
 .avatarInfoPanel {
   margin-right: 10px;
 }
@@ -1062,27 +840,6 @@
   border-bottom: 1px solid trimColor;
 }
 
-.infoBlock td.closedstate {
-  font-weight: bold;
-}
-
-.infoBlock td.useridentity {
-  white-space: nowrap;
-}
-.changeInfoBlock td.changeid {
-  font-size: x-small;
-}
-
-
-.patchSetInfoBlock {
-  margin-bottom: 10px;
-}
-.patchSetUserIdentity {
-  white-space: nowrap;
-}
-.patchSetUserIdentity .gwt-InlineLabel {
-  margin-left: 0.2em;
-}
 
 .patchSetActions {
   margin-bottom: 10px;
@@ -1092,42 +849,10 @@
   font-size: 8pt;
 }
 
-.selectPatchSetOldVersion {
-  font-weight: bold;
-  margin-right: 30px;
-  margin-top: 5px;
-}
-
-.approvalTable {
-  margin-top: 1em;
-  margin-bottom: 1em;
-}
-.missingApprovalList {
-  margin-top: 5px;
-  margin-left: 1em;
-  padding-left: 1em;
-  margin-bottom: 0px;
-}
-.missingApproval {
-  font-size: small;
-  white-space: nowrap;
-}
-.addReviewer {
-  margin-left: 1em;
-  margin-top: 5px;
-  white-space: nowrap;
-}
-.removeReviewer {
-  padding: 0px;
-}
-td.removeReviewerCell {
-  padding-left: 4em;
-  border-left: none;
-}
-
 .downloadBox {
   min-width: 580px;
   margin: 5px;
+  margin-right: 15px;
 }
 .downloadBoxTable {
   border-spacing: 0;
@@ -1169,9 +894,6 @@
 .downloadBoxCopyLabel div {
   float: right;
 }
-td.downloadLinkListCell {
-  padding: 0px;
-}
 .downloadLinkHeader {
   background: trimColor;
   white-space: nowrap;
@@ -1210,28 +932,6 @@
   width: 30em;
 }
 
-.parentsTable {
-  border-style: none;
-  border: 1px 1px 1px 0px;
-  outline: 0px;
-  padding: 0px;
-  border-spacing: 0px;
-  text-align: left;
-  font-family: mono-font;
-  font-size: 10px;
-}
-
-.parentsTable td.noborder {
-  border: none;
-}
-
-.parentsTable td.monospace {
-  font-family: mono-font;
-  font-size: 10px;
-  margin: 0px;
-  padding-left: 0px;
-}
-
 /** UnifiedScreen **/
 .unifiedTable {
   width: 100%;
@@ -1239,17 +939,6 @@
   display: table;
 }
 
-/** SideBySideScreen **/
-.sideBySideScreenSideBySideTable {
-  width: 100%;
-  border: 1px solid #B0BDCC;
-  display: table;
-}
-
-.sideBySideScreenSideBySideTable .fileLine {
-  width: 50%;
-}
-
 .sideBySideScreenLinkTable {
   width: 100%;
 }
@@ -1361,10 +1050,6 @@
   width: 100%;
 }
 
-.createGroupLink {
-  margin-bottom: 10px;
-}
-
 .createProjectPanel {
   margin-bottom: 10px;
   background-color: trimColor;
@@ -1450,66 +1135,10 @@
   width: 45em;
 }
 
-.projectAdminLabelRangeLine {
-  white-space: nowrap;
-}
-.projectAdminLabelValue {
-  font-family: mono-font;
-  font-size: small;
-}
 .projectActions {
   margin-bottom: 10px;
 }
 
-/** PublishCommentsScreen **/
-.publishCommentsScreen .smallHeading {
-  font-size: small;
-  font-weight: bold;
-  white-space: nowrap;
-}
-.publishCommentsScreen .labelList {
-  margin-bottom: 10px;
-  margin-left: 10px;
-  background: trimColor;
-  width: 25em;
-  white-space: nowrap;
-  padding-top: 2px;
-  padding-bottom: 2px;
-}
-.publishCommentsScreen .coverMessage {
-  margin-left: 10px;
-  padding: 5px 5px 5px 5px;
-}
-.publishCommentsScreen .coverMessage textarea {
-  font-size: small;
-}
-.publishCommentsScreen .labelList .gwt-RadioButton {
-  font-size: smaller;
-}
-.publishCommentsScreen .patchComments {
-  margin-left: 1em;
-  margin-right: 0.5em;
-  margin-bottom: 0.5em;
-}
-.publishCommentsScreen .gwt-Hyperlink {
-  white-space: nowrap;
-  font-size: small;
-}
-.publishCommentsScreen .lineHeader {
-  white-space: nowrap;
-  font-family: mono-font;
-  font-size: small;
-  font-style: italic;
-  padding-left: 3px;
-}
-.publishCommentsScreen .commentPanel {
-  border: none;
-  width: 35em;
-}
-.publishCommentsScreen .commentPanelDateCell {
-  display: none;
-}
-
 
 /** CommentedActionDialog **/
 .commentedActionDialog .gwt-DisclosurePanel .header td {
@@ -1533,6 +1162,16 @@
   white-space: nowrap;
   font-size: small;
 }
+.commentedActionDialog .rebaseContentPanel {
+  margin-left: 10px;
+  background: trimColor;
+  padding: 5px 5px 5px 5px;
+  width: 300px;
+}
+.commentedActionDialog .rebaseContentPanel .rebaseSuggestBox {
+  font-size: small;
+  width: 100%;
+}
 
 /** PatchBrowserPopup **/
 .patchBrowserPopup {
@@ -1552,10 +1191,6 @@
 .groupDescriptionPanel {
   margin-bottom: 3px;
 }
-.groupExternalNameFilterTextBox {
-  margin-right: 2px;
-  margin-bottom: 2px;
-}
 .groupNamePanel {
   margin-bottom: 3px;
 }
@@ -1571,9 +1206,6 @@
 .groupOwnerTextBox {
   margin-bottom: 2px;
 }
-.groupTypeSelectListBox {
-  margin-bottom: 2px;
-}
 
 
 /** AccountGroupMembersScreen **/
@@ -1610,6 +1242,20 @@
   cursor: pointer;
 }
 
+.branchTablePrevNextLinks {
+  position: relative;
+}
+.branchTablePrevNextLinks td {
+  float: left;
+  width: 5em;
+  text-align: left;
+  padding-right: 10px;
+}
+.branchTablePrevNextLinks .gwt-Hyperlink {
+  font-size: 9pt;
+  color: #2a5db0;
+}
+
 /** PluginListScreen **/
 .pluginsTable {
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gwt_override.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gwt_override.css
index f9a8cc0..c92117c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gwt_override.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gwt_override.css
@@ -36,7 +36,7 @@
 .gwt-DialogBox .dialogMiddleCenter {
   background: backgroundColor;
   color: textColor;
-} 
+}
 
 .gwt-Button {
   white-space: nowrap;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
index 26dd173..efeb2ec 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
@@ -14,14 +14,11 @@
 
 package com.google.gerrit.client.patches;
 
-import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.client.changes.CommentApi;
 import com.google.gerrit.client.changes.CommentInfo;
-import com.google.gerrit.client.changes.PatchTable;
-import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.client.ui.CommentPanel;
@@ -72,7 +69,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
-public abstract class AbstractPatchContentTable extends NavigationTable<Object>
+abstract class AbstractPatchContentTable extends NavigationTable<Object>
     implements CommentEditorContainer, FocusHandler, BlurHandler {
   public static final int R_HEAD = 0;
   static final short FILE_SIDE_A = (short) 0;
@@ -110,8 +107,6 @@
     if (Gerrit.isSignedIn()) {
       keysAction.add(new InsertCommentCommand(0, 'c', PatchUtil.C
           .commentInsert()));
-      keysAction.add(new PublishCommentsKeyCommand(0, 'r', Util.C
-          .keyPublishComments()));
 
       // See CommentEditorPanel
       //
@@ -131,11 +126,8 @@
 
   abstract void createFileCommentEditorOnSideB();
 
-  abstract PatchScreen.Type getPatchScreenType();
-
   protected void initHeaders(PatchScript script, PatchSetDetail detail) {
-    PatchScreen.Type type = getPatchScreenType();
-    headerSideA = new PatchSetSelectBox(PatchSetSelectBox.Side.A, type);
+    headerSideA = new PatchSetSelectBox(PatchSetSelectBox.Side.A);
     headerSideA.display(detail, script, patchKey, idSideA, idSideB);
     headerSideA.addDoubleClickHandler(new DoubleClickHandler() {
       @Override
@@ -145,7 +137,7 @@
         }
       }
     });
-    headerSideB = new PatchSetSelectBox(PatchSetSelectBox.Side.B, type);
+    headerSideB = new PatchSetSelectBox(PatchSetSelectBox.Side.B);
     headerSideB.display(detail, script, patchKey, idSideA, idSideB);
     headerSideB.addDoubleClickHandler(new DoubleClickHandler() {
       @Override
@@ -541,6 +533,11 @@
     }
   }
 
+  /**
+   * Update cursor after selecting a comment.
+   *
+   * @param newComment comment that was selected.
+   */
   protected void updateCursor(final PatchLineComment newComment) {
   }
 
@@ -724,7 +721,7 @@
   }
 
   protected void bindComment(final int row, final int col,
-      final PatchLineComment line, final boolean isLast, boolean expandComment) {
+      final PatchLineComment line, boolean expandComment) {
     if (line.getStatus() == PatchLineComment.Status.DRAFT) {
       final CommentEditorPanel plc =
           new CommentEditorPanel(line, commentLinkProcessor);
@@ -833,18 +830,6 @@
     }
   }
 
-  public class PublishCommentsKeyCommand extends NeedsSignInKeyCommand {
-    public PublishCommentsKeyCommand(int mask, char key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-      final PatchSet.Id id = patchKey.getParentKey();
-      Gerrit.display(Dispatcher.toPublish(id));
-    }
-  }
-
   public class PrevChunkKeyCmd extends KeyCommand {
     public PrevChunkKeyCmd(int mask, int key, String help) {
       super(mask, key, help);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
index 32302f4..2538102 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.client.ui.CommentPanel;
-import com.google.gerrit.common.changes.Side;
+import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -245,6 +245,7 @@
     final PatchSet.Id psId = comment.getKey().getParentKey().getParentKey();
     final boolean wasNew = isNew();
     GerritCallback<CommentInfo> cb = new GerritCallback<CommentInfo>() {
+      @Override
       public void onSuccess(CommentInfo result) {
         notifyDraftDelta(wasNew ? 1 : 0);
         comment = toComment(psId, comment.getKey().getParentKey().get(), result);
@@ -300,6 +301,7 @@
         comment.getKey().getParentKey().getParentKey(),
         comment.getKey().get(),
         new GerritCallback<JavaScriptObject>() {
+          @Override
           public void onSuccess(JavaScriptObject result) {
             notifyDraftDelta(-1);
             removeUI();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommitMessageBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommitMessageBlock.java
new file mode 100644
index 0000000..f336382
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommitMessageBlock.java
@@ -0,0 +1,117 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.patches;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.StarredChanges;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.ui.ChangeLink;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.PreElement;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwtexpui.clippy.client.CopyableLabel;
+import com.google.gwtexpui.globalkey.client.KeyCommandSet;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+class CommitMessageBlock extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, CommitMessageBlock> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  private KeyCommandSet keysAction;
+
+  @UiField
+  SimplePanel starPanel;
+  @UiField
+  FlowPanel permalinkPanel;
+  @UiField
+  PreElement commitSummaryPre;
+  @UiField
+  PreElement commitBodyPre;
+
+  CommitMessageBlock() {
+    initWidget(uiBinder.createAndBindUi(this));
+  }
+
+  CommitMessageBlock(KeyCommandSet keysAction) {
+    this.keysAction = keysAction;
+    initWidget(uiBinder.createAndBindUi(this));
+  }
+
+  void display(String commitMessage,
+      CommentLinkProcessor commentLinkProcessor) {
+    display(null, null, null, commitMessage, commentLinkProcessor);
+  }
+
+  void display(final PatchSet.Id patchSetId, final String revision,
+      Boolean starred, final String commitMessage,
+      CommentLinkProcessor commentLinkProcessor) {
+    starPanel.clear();
+    if (patchSetId != null && starred != null && Gerrit.isSignedIn()) {
+      Change.Id changeId = patchSetId.getParentKey();
+      StarredChanges.Icon star = StarredChanges.createIcon(changeId, starred);
+      star.setStyleName(Gerrit.RESOURCES.css().changeScreenStarIcon());
+      starPanel.add(star);
+
+      if (keysAction != null) {
+        keysAction.add(StarredChanges.newKeyCommand(star));
+      }
+    }
+
+    permalinkPanel.clear();
+    if (patchSetId != null && revision != null) {
+      final Change.Id changeId = patchSetId.getParentKey();
+      permalinkPanel.add(new ChangeLink(Util.C.changePermalink(), changeId));
+      permalinkPanel.add(new CopyableLabel(ChangeLink.permalink(changeId),
+          false));
+    }
+
+    String[] splitCommitMessage = commitMessage.split("\n", 2);
+
+    String commitSummary = splitCommitMessage[0];
+    String commitBody = "";
+    if (splitCommitMessage.length > 1) {
+      commitBody = splitCommitMessage[1];
+    }
+
+    // Linkify commit summary
+    SafeHtml commitSummaryLinkified = new SafeHtmlBuilder().append(commitSummary);
+    commitSummaryLinkified = commitSummaryLinkified.linkify();
+    commitSummaryLinkified = commentLinkProcessor.apply(commitSummaryLinkified);
+    commitSummaryPre.setInnerHTML(commitSummaryLinkified.asString());
+
+    // Hide commit body if there is no body
+    if (commitBody.trim().isEmpty()) {
+      commitBodyPre.getStyle().setDisplay(Display.NONE);
+    } else {
+      // Linkify commit body
+      SafeHtml commitBodyLinkified = new SafeHtmlBuilder().append(commitBody);
+      commitBodyLinkified = commitBodyLinkified.linkify();
+      commitBodyLinkified = commentLinkProcessor.apply(commitBodyLinkified);
+      commitBodyLinkified = commitBodyLinkified.replaceAll("\n\n", "<p></p>");
+      commitBodyLinkified = commitBodyLinkified.replaceAll("\n", "<br />");
+      commitBodyPre.setInnerHTML(commitBodyLinkified.asString());
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommitMessageBlock.ui.xml
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.ui.xml
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommitMessageBlock.ui.xml
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java
index 6875a96..c4fc1b0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java
@@ -33,10 +33,10 @@
  * A table used to specify which two patch sets should be diff'ed.
  */
 class HistoryTable extends FancyFlexTable<Patch> {
-  private final PatchScreen screen;
+  private final UnifiedPatchScreen screen;
   final List<HistoryRadio> all = new ArrayList<>();
 
-  HistoryTable(final PatchScreen parent) {
+  HistoryTable(final UnifiedPatchScreen parent) {
     setStyleName(Gerrit.RESOURCES.css().patchHistoryTable());
     screen = parent;
     table.setWidth("auto");
@@ -58,14 +58,7 @@
     }
     enableAll(false);
     Patch.Key k = new Patch.Key(sideB, screen.getPatchKey().get());
-    switch (screen.getPatchScreenType()) {
-      case SIDE_BY_SIDE:
-        Gerrit.display(Dispatcher.toPatchSideBySide(sideA, k));
-        break;
-      case UNIFIED:
-        Gerrit.display(Dispatcher.toPatchUnified(sideA, k));
-        break;
-    }
+    Gerrit.display(Dispatcher.toUnified(sideA, k));
   }
 
   void enableAll(final boolean on) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
index 9f36342..2a04c38 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
@@ -15,13 +15,14 @@
 package com.google.gerrit.client.patches;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.PatchTable;
+import com.google.gerrit.client.WebLinkInfo;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.ui.ChangeLink;
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwt.user.client.ui.HasHorizontalAlignment;
@@ -29,10 +30,12 @@
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 
+import java.util.List;
+
 class NavLinks extends Composite {
   public enum Nav {
     PREV (0, '[', PatchUtil.C.previousFileHelp(), 0),
-    NEXT (2, ']', PatchUtil.C.nextFileHelp(), 1);
+    NEXT (3, ']', PatchUtil.C.nextFileHelp(), 1);
 
     public int col;      // Table Cell column to display link in
     public int key;      // key code shortcut to activate link
@@ -56,7 +59,7 @@
   NavLinks(KeyCommandSet kcs, PatchSet.Id forPatch) {
     patchSetId = forPatch;
     keys = kcs;
-    table = new Grid(1, 3);
+    table = new Grid(1, 4);
     initWidget(table);
 
     final CellFormatter fmt = table.getCellFormatter();
@@ -64,20 +67,32 @@
     fmt.setHorizontalAlignment(0, 0, HasHorizontalAlignment.ALIGN_LEFT);
     fmt.setHorizontalAlignment(0, 1, HasHorizontalAlignment.ALIGN_CENTER);
     fmt.setHorizontalAlignment(0, 2, HasHorizontalAlignment.ALIGN_RIGHT);
+    fmt.setHorizontalAlignment(0, 3, HasHorizontalAlignment.ALIGN_RIGHT);
 
     final ChangeLink up = new ChangeLink("", patchSetId);
     SafeHtml.set(up, SafeHtml.asis(Util.C.upToChangeIconLink()));
     table.setWidget(0, 1, up);
   }
 
-  void display(int patchIndex, PatchScreen.Type type, PatchTable fileList) {
+  void display(int patchIndex, PatchTable fileList,
+      List<InlineHyperlink> links, List<WebLinkInfo> webLinks) {
     if (fileList != null) {
-      setupNav(Nav.PREV, fileList.getPreviousPatchLink(patchIndex, type));
-      setupNav(Nav.NEXT, fileList.getNextPatchLink(patchIndex, type));
+      setupNav(Nav.PREV, fileList.getPreviousPatchLink(patchIndex));
+      setupNav(Nav.NEXT, fileList.getNextPatchLink(patchIndex));
     } else {
       setupNav(Nav.PREV, null);
       setupNav(Nav.NEXT, null);
     }
+
+    FlowPanel linkPanel = new FlowPanel();
+    linkPanel.setStyleName(Gerrit.RESOURCES.css().linkPanel());
+    for (InlineHyperlink link : links) {
+      linkPanel.add(link);
+    }
+    for (WebLinkInfo webLink : webLinks) {
+      linkPanel.add(webLink.toAnchor());
+    }
+    table.setWidget(0, 2, linkPanel);
   }
 
   protected void setupNav(final Nav nav, final InlineHyperlink link) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchBrowserPopup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchBrowserPopup.java
index 9af1aa3..e60ce76 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchBrowserPopup.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchBrowserPopup.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.client.patches;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.PatchTable;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gwt.event.logical.shared.ResizeEvent;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
index 9ff9893..39aadc3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
@@ -58,6 +58,8 @@
   String toggleIntraline();
   String showPreferences();
 
+  String openEditScreen();
+
   String toggleReviewed();
   String markAsReviewedAndGoToNext();
 
@@ -77,6 +79,7 @@
   String reviewedAnd();
   String next();
   String download();
+  String edit();
   String addFileCommentToolTip();
   String addFileCommentByDoubleClick();
 
@@ -88,4 +91,7 @@
 
   String patchSkipRegionStart();
   String patchSkipRegionEnd();
+
+  String sideBySideDiff();
+  String unifiedDiff();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
index 0a47613..2f68822 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
@@ -40,6 +40,7 @@
 toggleIntraline = Toggle intraline difference
 showPreferences = Show diff preferences
 
+openEditScreen = Edit file in browser
 toggleReviewed = Toggle the reviewed flag
 markAsReviewedAndGoToNext = Mark patch as reviewed and go to next unreviewed patch
 
@@ -59,6 +60,7 @@
 reviewedAnd = Reviewed &
 next = next
 download = Download
+edit = Edit
 addFileCommentToolTip = Click to add file comment
 addFileCommentByDoubleClick = Double click to add file comment
 
@@ -67,3 +69,6 @@
 
 patchSkipRegionStart = ... skipped
 patchSkipRegionEnd = common lines ...
+
+sideBySideDiff = Side-by-side diff
+unifiedDiff = Unified diff
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
index 2fe2d45..264e043 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
@@ -230,12 +230,12 @@
   }
 
   @UiHandler("update")
-  void onUpdate(ClickEvent event) {
+  void onUpdate(@SuppressWarnings("unused") ClickEvent event) {
     update();
   }
 
   @UiHandler("save")
-  void onSave(ClickEvent event) {
+  void onSave(@SuppressWarnings("unused") ClickEvent event) {
     save();
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
index 083820b..6762383 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
@@ -41,18 +41,13 @@
 import java.util.Map;
 
 public class PatchSetSelectBox extends Composite {
-  interface Binder extends UiBinder<HTMLPanel, PatchSetSelectBox> {
-  }
-
+  interface Binder extends UiBinder<HTMLPanel, PatchSetSelectBox> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
   interface BoxStyle extends CssResource {
     String selected();
-
     String hidden();
-
     String sideMarker();
-
     String patchSetLabel();
   }
 
@@ -66,7 +61,6 @@
   PatchSet.Id idSideB;
   PatchSet.Id idActive;
   Side side;
-  PatchScreen.Type screenType;
   Map<Integer, Anchor> links;
   private Label patchSet;
 
@@ -76,9 +70,8 @@
   @UiField
   BoxStyle style;
 
-  public PatchSetSelectBox(Side side, final PatchScreen.Type type) {
+  public PatchSetSelectBox(Side side) {
     this.side = side;
-    this.screenType = type;
 
     initWidget(uiBinder.createAndBindUi(this));
   }
@@ -102,11 +95,9 @@
     patchSet.addStyleName(style.patchSetLabel());
     linkPanel.add(patchSet);
 
-    if (screenType == PatchScreen.Type.UNIFIED) {
-      Label sideMarker = new Label((side == Side.A) ? "(-)" : "(+)");
-      sideMarker.addStyleName(style.sideMarker());
-      linkPanel.add(sideMarker);
-    }
+    Label sideMarker = new Label((side == Side.A) ? "(-)" : "(+)");
+    sideMarker.addStyleName(style.sideMarker());
+    linkPanel.add(sideMarker);
 
     Anchor baseLink;
     if (detail.getInfo().getParents().size() > 1) {
@@ -116,9 +107,7 @@
     }
 
     links.put(0, baseLink);
-    if (screenType == PatchScreen.Type.UNIFIED || side == Side.A) {
-      linkPanel.add(baseLink);
-    }
+    linkPanel.add(baseLink);
 
     if (side == Side.B) {
       links.get(0).setStyleName(style.hidden());
@@ -126,7 +115,7 @@
 
     for (Patch patch : script.getHistory()) {
       PatchSet.Id psId = patch.getKey().getParentKey();
-      Anchor anchor = createLink(Integer.toString(psId.get()), psId);
+      Anchor anchor = createLink(psId.getId(), psId);
       links.put(psId.get(), anchor);
       linkPanel.add(anchor);
     }
@@ -161,19 +150,9 @@
         }
 
         Patch.Key keySideB = new Patch.Key(idSideB, patchKey.get());
-
-        switch (screenType) {
-          case SIDE_BY_SIDE:
-            Gerrit.display(Dispatcher.toPatchSideBySide(idSideA, keySideB));
-            break;
-          case UNIFIED:
-            Gerrit.display(Dispatcher.toPatchUnified(idSideA, keySideB));
-            break;
-        }
+        Gerrit.display(Dispatcher.toUnified(idSideA, keySideB));
       }
-
     });
-
     return anchor;
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java
similarity index 88%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java
index 638ec13..178a583 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java
@@ -12,11 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.changes;
+package com.google.gerrit.client.patches;
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.patches.PatchScreen;
+import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
 import com.google.gerrit.client.ui.NavigationTable;
@@ -25,7 +25,6 @@
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
-import com.google.gerrit.reviewdb.client.Patch.Key;
 import com.google.gerrit.reviewdb.client.Patch.PatchType;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.Scheduler;
@@ -53,8 +52,8 @@
 import java.util.List;
 import java.util.Map;
 
-public class PatchTable extends Composite {
-  public interface PatchValidator {
+class PatchTable extends Composite {
+  interface PatchValidator {
     /**
      * @param patch
      * @return true if patch is valid.
@@ -62,7 +61,7 @@
     boolean isValid(Patch patch);
   }
 
-  public final PatchValidator PREFERENCE_VALIDATOR =
+  final PatchValidator PREFERENCE_VALIDATOR =
       new PatchValidator() {
         @Override
         public boolean isValid(Patch patch) {
@@ -88,22 +87,22 @@
   private boolean active;
   private boolean registerKeys;
 
-  public PatchTable(ListenableAccountDiffPreference prefs) {
+  PatchTable(ListenableAccountDiffPreference prefs) {
     listenablePrefs = prefs;
     myBody = new FlowPanel();
     initWidget(myBody);
   }
 
-  public PatchTable() {
+  PatchTable() {
     this(new ListenableAccountDiffPreference());
   }
 
-  public int indexOf(Patch.Key patch) {
+  int indexOf(Patch.Key patch) {
     Integer i = patchMap().get(patch);
     return i != null ? i : -1;
   }
 
-  private Map<Key, Integer> patchMap() {
+  private Map<Patch.Key, Integer> patchMap() {
     if (patchMap == null) {
       patchMap = new HashMap<>();
       for (int i = 0; i < patchList.size(); i++) {
@@ -113,7 +112,7 @@
     return patchMap;
   }
 
-  public void display(PatchSet.Id base, PatchSetDetail detail) {
+  void display(PatchSet.Id base, PatchSetDetail detail) {
     this.base = base;
     this.detail = detail;
     this.patchList = detail.getPatches();
@@ -129,19 +128,19 @@
     }
   }
 
-  public PatchSet.Id getBase() {
+  PatchSet.Id getBase() {
     return base;
   }
 
-  public void setSavePointerId(final String id) {
+  void setSavePointerId(final String id) {
     savePointerId = id;
   }
 
-  public boolean isLoaded() {
+  boolean isLoaded() {
     return myTable != null;
   }
 
-  public void onTableLoaded(final Command cmd) {
+  void onTableLoaded(final Command cmd) {
     if (myTable != null) {
       cmd.execute();
     } else {
@@ -149,7 +148,7 @@
     }
   }
 
-  public void addClickHandler(final ClickHandler clickHandler) {
+  void addClickHandler(final ClickHandler clickHandler) {
     if (myTable != null) {
       myTable.addClickHandler(clickHandler);
     } else {
@@ -160,27 +159,27 @@
     }
   }
 
-  public void setRegisterKeys(final boolean on) {
+  void setRegisterKeys(final boolean on) {
     registerKeys = on;
     if (myTable != null) {
       myTable.setRegisterKeys(on);
     }
   }
 
-  public void movePointerTo(final Patch.Key k) {
+  void movePointerTo(final Patch.Key k) {
     if (myTable != null) {
       myTable.movePointerTo(k);
     }
   }
 
-  public void setActive(boolean active) {
+  void setActive(boolean active) {
     this.active = active;
     if (myTable != null) {
       myTable.setActive(active);
     }
   }
 
-  public void notifyDraftDelta(final Patch.Key k, final int delta) {
+  void notifyDraftDelta(final Patch.Key k, final int delta) {
     if (myTable != null) {
       myTable.notifyDraftDelta(k, delta);
     }
@@ -214,46 +213,42 @@
   /**
    * @return a link to the previous file in this patch set, or null.
    */
-  public InlineHyperlink getPreviousPatchLink(int index,
-      PatchScreen.Type patchType) {
+  InlineHyperlink getPreviousPatchLink(int index) {
     int previousPatchIndex = getPreviousPatch(index, PREFERENCE_VALIDATOR);
     if (previousPatchIndex < 0) {
       return null;
     }
-    return createLink(previousPatchIndex, patchType,
+    return createLink(previousPatchIndex,
         SafeHtml.asis(Util.C.prevPatchLinkIcon()), null);
   }
 
   /**
    * @return a link to the next file in this patch set, or null.
    */
-  public InlineHyperlink getNextPatchLink(int index, PatchScreen.Type patchType) {
+  InlineHyperlink getNextPatchLink(int index) {
     int nextPatchIndex = getNextPatch(index, false, PREFERENCE_VALIDATOR);
     if (nextPatchIndex < 0) {
       return null;
     }
-    return createLink(nextPatchIndex, patchType, null,
+    return createLink(nextPatchIndex, null,
         SafeHtml.asis(Util.C.nextPatchLinkIcon()));
   }
 
   /**
    * @return a link to the the given patch.
    * @param index The patch to link to
-   * @param screenType The screen type of patch display
    * @param before A string to display at the beginning of the href text
    * @param after A string to display at the end of the href text
    */
-  public PatchLink createLink(int index, PatchScreen.Type screenType,
-      SafeHtml before, SafeHtml after) {
+  PatchLink createLink(int index, SafeHtml before, SafeHtml after) {
     Patch patch = patchList.get(index);
-
-    Key thisKey = patch.getKey();
+    Patch.Key thisKey = patch.getKey();
     PatchLink link;
 
-    if (isUnifiedPatchLink(patch, screenType)) {
-      link = new PatchLink.Unified("", base, thisKey, index, detail, this);
+    if (isUnifiedPatchLink(patch)) {
+      link = new PatchLink.Unified("", base, thisKey);
     } else {
-      link = new PatchLink.SideBySide("", base, thisKey, index, detail, this);
+      link = new PatchLink.SideBySide("", base, thisKey);
     }
 
     SafeHtmlBuilder text = new SafeHtmlBuilder();
@@ -264,15 +259,11 @@
     return link;
   }
 
-  private static boolean isUnifiedPatchLink(final Patch patch,
-      final PatchScreen.Type screenType) {
-    if (Dispatcher.isChangeScreen2()) {
-      return (patch.getPatchType().equals(PatchType.BINARY)
-          || (Gerrit.isSignedIn()
-              && Gerrit.getUserAccount().getGeneralPreferences().getDiffView()
-                 .equals(DiffView.UNIFIED_DIFF)));
-    }
-    return screenType == PatchScreen.Type.UNIFIED;
+  private static boolean isUnifiedPatchLink(final Patch patch) {
+    return (patch.getPatchType().equals(PatchType.BINARY)
+        || (Gerrit.isSignedIn()
+            && Gerrit.getUserAccount().getGeneralPreferences().getDiffView()
+            .equals(DiffView.UNIFIED_DIFF)));
   }
 
   private static String getFileNameOnly(Patch patch) {
@@ -287,11 +278,11 @@
     return fileName;
   }
 
-  public static String getDisplayFileName(Patch patch) {
+  static String getDisplayFileName(Patch patch) {
     return getDisplayFileName(patch.getKey());
   }
 
-  public static String getDisplayFileName(Patch.Key patchKey) {
+  static String getDisplayFileName(Patch.Key patchKey) {
     if (Patch.COMMIT_MSG.equals(patchKey.get())) {
       return Util.C.commitMessage();
     }
@@ -301,13 +292,13 @@
   /**
    * Update the reviewed status for the given patch.
    */
-  public void updateReviewedStatus(Patch.Key patchKey, boolean reviewed) {
+  void updateReviewedStatus(Patch.Key patchKey, boolean reviewed) {
     if (myTable != null) {
       myTable.updateReviewedStatus(patchKey, reviewed);
     }
   }
 
-  public ListenableAccountDiffPreference getPreferences() {
+  ListenableAccountDiffPreference getPreferences() {
     return listenablePrefs;
   }
 
@@ -385,7 +376,7 @@
     }
 
     /** Activates / Deactivates the key navigation and the highlighting of the current row for this table */
-    public void setActive(boolean active) {
+    void setActive(boolean active) {
       if (active) {
         if(activeRow > 0 && getCurrentRow() != activeRow) {
           super.movePointerTo(activeRow);
@@ -404,9 +395,8 @@
       Patch patch = PatchTable.this.patchList.get(row - 1);
       setRowItem(row, patch);
 
-      Widget nameCol;
-      nameCol = new PatchLink.SideBySide(getDisplayFileName(patch), base,
-          patch.getKey(), row - 1, detail, PatchTable.this);
+      Widget nameCol = new PatchLink.SideBySide(getDisplayFileName(patch), base,
+          patch.getKey());
 
       if (patch.getSourceFileName() != null) {
         final String text;
@@ -428,14 +418,12 @@
 
       int C_UNIFIED = C_SIDEBYSIDE + 1;
 
-      PatchLink sideBySide =
-          new PatchLink.SideBySide(Util.C.patchTableDiffSideBySide(), base,
-              patch.getKey(), row - 1, detail, PatchTable.this);
+      PatchLink sideBySide = new PatchLink.SideBySide(
+          Util.C.patchTableDiffSideBySide(), base, patch.getKey());
       sideBySide.setStyleName("gwt-Anchor");
 
-      PatchLink unified =
-          new PatchLink.Unified(Util.C.patchTableDiffUnified(), base,
-              patch.getKey(), row - 1, detail, PatchTable.this);
+      PatchLink unified = new PatchLink.Unified(Util.C.patchTableDiffUnified(),
+          base, patch.getKey());
       unified.setStyleName("gwt-Anchor");
 
       table.setWidget(row, C_SIDEBYSIDE, sideBySide);
@@ -448,7 +436,7 @@
         @Override
         public void onClick(ClickEvent event) {
           for (Patch p : detail.getPatches()) {
-            openWindow(Dispatcher.toPatchSideBySide(base, p.getKey()));
+            openWindow(Dispatcher.toSideBySide(base, p.getKey()));
           }
         }
       });
@@ -457,9 +445,10 @@
       int C_UNIFIED = C_SIDEBYSIDE - 2 + 1;
       Anchor unified = new Anchor(Util.C.diffAllUnified());
       unified.addClickHandler(new ClickHandler() {
+        @Override
         public void onClick(ClickEvent event) {
           for (Patch p : detail.getPatches()) {
-            openWindow(Dispatcher.toPatchUnified(base, p.getKey()));
+            openWindow(Dispatcher.toUnified(base, p.getKey()));
           }
         }
       });
@@ -744,6 +733,7 @@
     /**
      * Add the files contained in the list of patches to the table, one per row.
      */
+    @Override
     @SuppressWarnings("fallthrough")
     public boolean execute() {
       final boolean attachedNow = isAttached();
@@ -844,7 +834,7 @@
    *        true
    * @return index of next valid patch, or -1 if no valid patches
    */
-  public int getNextPatch(int currentIndex, boolean loopAround,
+  int getNextPatch(int currentIndex, boolean loopAround,
       PatchValidator... validators) {
     return getNextPatchHelper(currentIndex, loopAround, detail.getPatches()
         .size(), validators);
@@ -878,7 +868,7 @@
   /**
    * @return the index to the previous patch
    */
-  public int getPreviousPatch(int currentIndex, PatchValidator... validators) {
+  int getPreviousPatch(int currentIndex, PatchValidator... validators) {
     for (int i = currentIndex - 1; i >= 0; i--) {
       Patch patch = detail.getPatches().get(i);
       if (patch != null && patchIsValid(patch, validators)) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchUtil.java
index 709685f..e949194 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchUtil.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchUtil.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client.patches;
 
+import com.google.gerrit.common.data.ChangeDetailService;
 import com.google.gerrit.common.data.PatchDetailService;
 import com.google.gwt.core.client.GWT;
 import com.google.gwtjsonrpc.client.JsonUtil;
@@ -21,10 +22,14 @@
 public class PatchUtil {
   public static final PatchConstants C = GWT.create(PatchConstants.class);
   public static final PatchMessages M = GWT.create(PatchMessages.class);
-  public static final PatchDetailService DETAIL_SVC;
+  public static final ChangeDetailService CHANGE_SVC;
+  public static final PatchDetailService PATCH_SVC;
 
   static {
-    DETAIL_SVC = GWT.create(PatchDetailService.class);
-    JsonUtil.bind(DETAIL_SVC, "rpc/PatchDetailService");
+    CHANGE_SVC = GWT.create(ChangeDetailService.class);
+    JsonUtil.bind(CHANGE_SVC, "rpc/ChangeDetailService");
+
+    PATCH_SVC = GWT.create(PatchDetailService.class);
+    JsonUtil.bind(PATCH_SVC, "rpc/PatchDetailService");
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/ReviewedPanels.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/ReviewedPanels.java
index 68df45d..d889c79 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/ReviewedPanels.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/ReviewedPanels.java
@@ -16,9 +16,8 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.VoidResult;
-import com.google.gerrit.client.changes.PatchTable;
-import com.google.gerrit.client.changes.PatchTable.PatchValidator;
 import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.patches.PatchTable.PatchValidator;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.ui.ChangeLink;
 import com.google.gerrit.client.ui.InlineHyperlink;
@@ -35,10 +34,9 @@
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
-public class ReviewedPanels {
-
-  public final FlowPanel top;
-  public final FlowPanel bottom;
+class ReviewedPanels {
+  final FlowPanel top;
+  final FlowPanel bottom;
 
   private Patch.Key patchKey;
   private PatchTable fileList;
@@ -46,17 +44,16 @@
   private CheckBox checkBoxTop;
   private CheckBox checkBoxBottom;
 
-  public ReviewedPanels() {
+  ReviewedPanels() {
     this.top = new FlowPanel();
     this.bottom = new FlowPanel();
     this.bottom.setStyleName(Gerrit.RESOURCES.css().reviewedPanelBottom());
   }
 
-  public void populate(Patch.Key pk, PatchTable pt, int patchIndex,
-      PatchScreen.Type patchScreenType) {
+  void populate(Patch.Key pk, PatchTable pt, int patchIndex) {
     patchKey = pk;
     fileList = pt;
-    reviewedLink = createReviewedLink(patchIndex, patchScreenType);
+    reviewedLink = createReviewedLink(patchIndex);
 
     top.clear();
     checkBoxTop = createReviewedCheckbox();
@@ -87,54 +84,56 @@
     return checkBox;
   }
 
-  public boolean getValue() {
+  boolean getValue() {
     return checkBoxTop.getValue();
   }
 
-  public void setValue(final boolean value) {
+  void setValue(final boolean value) {
     checkBoxTop.setValue(value);
     checkBoxBottom.setValue(value);
   }
 
-  public void setReviewedByCurrentUser(boolean reviewed) {
-    if (fileList != null) {
-      fileList.updateReviewedStatus(patchKey, reviewed);
-    }
-
+  void setReviewedByCurrentUser(boolean reviewed) {
     PatchSet.Id ps = patchKey.getParentKey();
-    RestApi api = new RestApi("/changes/").id(ps.getParentKey().get())
-        .view("revisions").id(ps.get())
-        .view("files").id(patchKey.getFileName())
-        .view("reviewed");
-
-    AsyncCallback<VoidResult> cb = new AsyncCallback<VoidResult>() {
-      @Override
-      public void onFailure(Throwable arg0) {
-        // nop
+    if (ps.get() != 0) {
+      if (fileList != null) {
+        fileList.updateReviewedStatus(patchKey, reviewed);
       }
 
-      @Override
-      public void onSuccess(VoidResult result) {
-        // nop
+      RestApi api = new RestApi("/changes/").id(ps.getParentKey().get())
+          .view("revisions").id(ps.get())
+          .view("files").id(patchKey.getFileName())
+          .view("reviewed");
+
+      AsyncCallback<VoidResult> cb = new AsyncCallback<VoidResult>() {
+        @Override
+        public void onFailure(Throwable arg0) {
+          // nop
+        }
+
+        @Override
+        public void onSuccess(VoidResult result) {
+          // nop
+        }
+      };
+      if (reviewed) {
+        api.put(cb);
+      } else {
+        api.delete(cb);
       }
-    };
-    if (reviewed) {
-      api.put(cb);
-    } else {
-      api.delete(cb);
     }
   }
 
-  public void go() {
+  void go() {
     if (reviewedLink != null) {
       setReviewedByCurrentUser(true);
       reviewedLink.go();
     }
   }
 
-  private InlineHyperlink createReviewedLink(final int patchIndex,
-      final PatchScreen.Type patchScreenType) {
+  private InlineHyperlink createReviewedLink(final int patchIndex) {
     final PatchValidator unreviewedValidator = new PatchValidator() {
+      @Override
       public boolean isValid(Patch patch) {
         return !patch.isReviewedByCurrentUser();
       }
@@ -149,8 +148,7 @@
       if (nextUnreviewedPatchIndex > -1) {
         // Create invisible patch link to change page
         reviewedLink =
-            fileList.createLink(nextUnreviewedPatchIndex, patchScreenType,
-                null, null);
+            fileList.createLink(nextUnreviewedPatchIndex, null, null);
         reviewedLink.setText("");
       }
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
deleted file mode 100644
index 2d58816..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
+++ /dev/null
@@ -1,682 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.patches;
-
-import static com.google.gerrit.client.patches.PatchLine.Type.CONTEXT;
-import static com.google.gerrit.client.patches.PatchLine.Type.DELETE;
-import static com.google.gerrit.client.patches.PatchLine.Type.INSERT;
-import static com.google.gerrit.client.patches.PatchLine.Type.REPLACE;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.common.data.CommentDetail;
-import com.google.gerrit.common.data.PatchScript;
-import com.google.gerrit.common.data.PatchScript.DisplayMethod;
-import com.google.gerrit.common.data.PatchScript.FileMode;
-import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.prettify.client.SparseHtmlFile;
-import com.google.gerrit.prettify.common.EditList;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.Event;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.HasVerticalAlignment;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-import org.eclipse.jgit.diff.Edit;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-
-public class SideBySideTable extends AbstractPatchContentTable {
-  private static final int A = 2;
-  private static final int B = 3;
-  private static final int NUM_ROWS_TO_EXPAND = 10;
-
-  private SparseHtmlFile a;
-  private SparseHtmlFile b;
-  private boolean isHugeFile;
-  protected boolean isFileCommentBorderRowExist;
-
-  protected void createFileCommentEditorOnSideA() {
-    createCommentEditor(R_HEAD + 1, A, R_HEAD, FILE_SIDE_A);
-  }
-
-  protected void createFileCommentEditorOnSideB() {
-    createCommentEditor(R_HEAD + 1, B, R_HEAD, FILE_SIDE_B);
-  }
-
-  @Override
-  protected void onCellDoubleClick(final int row, int column) {
-    if (column > C_ARROW && getRowItem(row) instanceof PatchLine) {
-      final PatchLine line = (PatchLine) getRowItem(row);
-      if (column == 1 || column == A) {
-        createCommentEditor(row + 1, A, line.getLineA(), (short) 0);
-      } else if (column == B || column == 4) {
-        createCommentEditor(row + 1, B, line.getLineB(), (short) 1);
-      }
-    }
-  }
-
-  @Override
-  protected void onCellSingleClick(Event event, int row, int column) {
-    super.onCellSingleClick(event, row, column);
-    if (column == 1 || column == 4) {
-      onCellDoubleClick(row, column);
-    }
-  }
-
-  @Override
-  protected void onInsertComment(final PatchLine line) {
-    final int row = getCurrentRow();
-    createCommentEditor(row + 1, B, line.getLineB(), (short) 1);
-  }
-
-  @Override
-  protected void render(final PatchScript script, final PatchSetDetail detail) {
-    final ArrayList<Object> lines = new ArrayList<>();
-    final SafeHtmlBuilder nc = new SafeHtmlBuilder();
-    isHugeFile = script.isHugeFile();
-    allocateTableHeader(script, nc);
-    lines.add(null);
-    if (!isDisplayBinary) {
-      if (script.getFileModeA() != FileMode.FILE
-          || script.getFileModeB() != FileMode.FILE) {
-        openLine(nc);
-        appendModeLine(nc, script.getFileModeA());
-        appendModeLine(nc, script.getFileModeB());
-        closeLine(nc);
-        lines.add(null);
-      }
-
-      if (hasDifferences(script)) {
-        int lastA = 0;
-        int lastB = 0;
-        final boolean ignoreWS = script.isIgnoreWhitespace();
-        a = getSparseHtmlFileA(script);
-        b = getSparseHtmlFileB(script);
-        final boolean intraline =
-            script.getDiffPrefs().isIntralineDifference()
-                && script.hasIntralineDifference();
-        for (final EditList.Hunk hunk : script.getHunks()) {
-          if (!hunk.isStartOfFile()) {
-            appendSkipLine(nc, hunk.getCurB() - lastB);
-            lines.add(new SkippedLine(lastA, lastB, hunk.getCurB() - lastB));
-          }
-
-          while (hunk.next()) {
-            if (hunk.isContextLine()) {
-              openLine(nc);
-              final SafeHtml ctx = a.getSafeHtmlLine(hunk.getCurA());
-              appendLineNumber(nc, hunk.getCurA(), false);
-              appendLineText(nc, CONTEXT, ctx, false, false);
-              if (ignoreWS && b.contains(hunk.getCurB())) {
-                appendLineText(nc, CONTEXT, b, hunk.getCurB(), false);
-              } else {
-                appendLineText(nc, CONTEXT, ctx, false, false);
-              }
-              appendLineNumber(nc, hunk.getCurB(), true);
-              closeLine(nc);
-              hunk.incBoth();
-              lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB()));
-
-            } else if (hunk.isModifiedLine()) {
-              final boolean del = hunk.isDeletedA();
-              final boolean ins = hunk.isInsertedB();
-              final boolean full =
-                  intraline && hunk.getCurEdit().getType() != Edit.Type.REPLACE;
-              openLine(nc);
-
-              if (del) {
-                appendLineNumber(nc, hunk.getCurA(), false);
-                appendLineText(nc, DELETE, a, hunk.getCurA(), full);
-                hunk.incA();
-              } else if (hunk.getCurEdit().getType() == Edit.Type.REPLACE) {
-                appendLineNumber(nc, false);
-                appendLineNone(nc, DELETE);
-              } else {
-                appendLineNumber(nc, false);
-                appendLineNone(nc, CONTEXT);
-              }
-
-              if (ins) {
-                appendLineText(nc, INSERT, b, hunk.getCurB(), full);
-                appendLineNumber(nc, hunk.getCurB(), true);
-                hunk.incB();
-              } else if (hunk.getCurEdit().getType() == Edit.Type.REPLACE) {
-                appendLineNone(nc, INSERT);
-                appendLineNumber(nc, true);
-              } else {
-                appendLineNone(nc, CONTEXT);
-                appendLineNumber(nc, true);
-              }
-
-              closeLine(nc);
-
-              if (del && ins) {
-                lines.add(new PatchLine(REPLACE, hunk.getCurA(), hunk.getCurB()));
-              } else if (del) {
-                lines.add(new PatchLine(DELETE, hunk.getCurA(), -1));
-              } else if (ins) {
-                lines.add(new PatchLine(INSERT, -1, hunk.getCurB()));
-              }
-            }
-          }
-          lastA = hunk.getCurA();
-          lastB = hunk.getCurB();
-        }
-        if (lastB != b.size()) {
-          appendSkipLine(nc, b.size() - lastB);
-          lines.add(new SkippedLine(lastA, lastB, b.size() - lastB));
-        }
-      }
-    } else {
-      // Display the patch header for binary
-      for (final String line : script.getPatchHeader()) {
-        appendFileHeader(nc, line);
-      }
-      // If there is a safe picture involved, we show it
-      if (script.getDisplayMethodA() == DisplayMethod.IMG
-          || script.getDisplayMethodB() == DisplayMethod.IMG) {
-        appendImageLine(script, nc);
-      }
-    }
-    if (!hasDifferences(script)) {
-      appendNoDifferences(nc);
-    }
-    resetHtml(nc);
-    populateTableHeader(script, detail);
-    if (hasDifferences(script)) {
-      initScript(script);
-      if (!isDisplayBinary) {
-        for (int row = 0; row < lines.size(); row++) {
-          setRowItem(row, lines.get(row));
-          if (lines.get(row) instanceof SkippedLine) {
-            createSkipLine(row, (SkippedLine) lines.get(row), isHugeFile);
-          }
-        }
-      }
-    }
-  }
-
-  private SafeHtml createImage(String url) {
-    SafeHtmlBuilder m = new SafeHtmlBuilder();
-    m.openElement("img");
-    m.setAttribute("src", url);
-    m.closeElement("img");
-    return m.toSafeHtml();
-  }
-
-  private void appendImageLine(final PatchScript script,
-      final SafeHtmlBuilder m) {
-    m.openTr();
-    m.setAttribute("valign", "center");
-    m.setAttribute("align", "center");
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().iconCell());
-    m.closeTd();
-
-    appendLineNumber(m, false);
-    if (script.getDisplayMethodA() == DisplayMethod.IMG) {
-      final String url = getUrlA();
-      appendLineText(m, DELETE, createImage(url), false, true);
-    } else {
-      appendLineNone(m, DELETE);
-    }
-    if (script.getDisplayMethodB() == DisplayMethod.IMG) {
-      final String url = getUrlB();
-      appendLineText(m, INSERT, createImage(url), false, true);
-    } else {
-      appendLineNone(m, INSERT);
-    }
-
-    appendLineNumber(m, true);
-    m.closeTr();
-  }
-
-  private void populateTableHeader(final PatchScript script,
-      final PatchSetDetail detail) {
-    initHeaders(script, detail);
-    table.setWidget(R_HEAD, A, headerSideA);
-    table.setWidget(R_HEAD, B, headerSideB);
-
-    // Populate icons to lineNumber column header.
-    if (headerSideA.isFileOrCommitMessage()) {
-      table.setWidget(R_HEAD, A - 1, iconA);
-    }
-    if (headerSideB.isFileOrCommitMessage()) {
-      table.setWidget(R_HEAD, B + 1, iconB);
-    }
-  }
-
-  private void appendModeLine(final SafeHtmlBuilder nc, final FileMode mode) {
-    nc.openTd();
-    nc.setStyleName(Gerrit.RESOURCES.css().lineNumber());
-    nc.nbsp();
-    nc.closeTd();
-
-    nc.openTd();
-    nc.addStyleName(Gerrit.RESOURCES.css().fileLine());
-    nc.addStyleName(Gerrit.RESOURCES.css().fileLineMode());
-    switch(mode){
-      case FILE:
-        nc.nbsp();
-        break;
-      case SYMLINK:
-        nc.append(PatchUtil.C.fileTypeSymlink());
-        break;
-      case GITLINK:
-        nc.append(PatchUtil.C.fileTypeGitlink());
-        break;
-    }
-    nc.closeTd();
-  }
-
-  @Override
-  protected PatchScreen.Type getPatchScreenType() {
-    return PatchScreen.Type.SIDE_BY_SIDE;
-  }
-
-  @Override
-  public void display(final CommentDetail cd, boolean expandComments) {
-    if (cd.isEmpty()) {
-      return;
-    }
-    setAccountInfoCache(cd.getAccounts());
-
-    for (int row = 0; row < table.getRowCount();) {
-      final Iterator<PatchLineComment> ai;
-      final Iterator<PatchLineComment> bi;
-
-      if (row == R_HEAD) {
-        ai = cd.getForA(R_HEAD).iterator();
-        bi = cd.getForB(R_HEAD).iterator();
-      } else if (getRowItem(row) instanceof PatchLine) {
-        final PatchLine pLine = (PatchLine) getRowItem(row);
-        ai = cd.getForA(pLine.getLineA()).iterator();
-        bi = cd.getForB(pLine.getLineB()).iterator();
-      } else {
-        row++;
-        continue;
-      }
-
-      row++;
-      while (ai.hasNext() && bi.hasNext()) {
-        final PatchLineComment ac = ai.next();
-        final PatchLineComment bc = bi.next();
-        if (ac.getLine() == R_HEAD) {
-          insertFileCommentRow(row);
-        } else {
-          insertRow(row);
-        }
-        bindComment(row, A, ac, !ai.hasNext(), expandComments);
-        bindComment(row, B, bc, !bi.hasNext(), expandComments);
-        row++;
-      }
-
-      row = finish(ai, row, A, expandComments);
-      row = finish(bi, row, B, expandComments);
-    }
-  }
-
-  private void defaultStyle(final int row, final CellFormatter fmt) {
-    fmt.addStyleName(row, A - 1, Gerrit.RESOURCES.css().lineNumber());
-    fmt.addStyleName(row, A, Gerrit.RESOURCES.css().diffText());
-    if (isDisplayBinary) {
-      fmt.addStyleName(row, A, Gerrit.RESOURCES.css().diffTextForBinaryInSideBySide());
-    }
-    fmt.addStyleName(row, B, Gerrit.RESOURCES.css().diffText());
-    fmt.addStyleName(row, B + 1, Gerrit.RESOURCES.css().lineNumber());
-    fmt.addStyleName(row, B + 1, Gerrit.RESOURCES.css().rightmost());
-  }
-
-  @Override
-  protected void insertRow(final int row) {
-    super.insertRow(row);
-    final CellFormatter fmt = table.getCellFormatter();
-    defaultStyle(row, fmt);
-  }
-
-  @Override
-  protected void insertFileCommentRow(final int row) {
-    table.insertRow(row);
-    final CellFormatter fmt = table.getCellFormatter();
-    fmt.addStyleName(row, C_ARROW, Gerrit.RESOURCES.css().iconCellOfFileCommentRow());
-    defaultStyle(row, fmt);
-
-    fmt.addStyleName(row, C_ARROW, //
-        Gerrit.RESOURCES.css().cellsNextToFileComment());
-    fmt.addStyleName(row, A - 1, //
-        Gerrit.RESOURCES.css().cellsNextToFileComment());
-    fmt.addStyleName(row, B + 1, //
-        Gerrit.RESOURCES.css().cellsNextToFileComment());
-    createFileCommentBorderRow(row);
-  }
-
-  private void createFileCommentBorderRow(final int row) {
-    if (row == 1 && !isFileCommentBorderRowExist) {
-      isFileCommentBorderRowExist = true;
-      table.insertRow(R_HEAD + 2);
-
-      final CellFormatter fmt = table.getCellFormatter();
-
-      fmt.addStyleName(R_HEAD + 2, C_ARROW, //
-          Gerrit.RESOURCES.css().iconCellOfFileCommentRow());
-      defaultStyle(R_HEAD + 2, fmt);
-
-      final Element iconCell = fmt.getElement(R_HEAD + 2, C_ARROW);
-      UIObject.setStyleName(DOM.getParent(iconCell), Gerrit.RESOURCES.css()
-          .fileCommentBorder(), true);
-    }
-  }
-
-  private int finish(final Iterator<PatchLineComment> i, int row, final int col, boolean expandComment) {
-    while (i.hasNext()) {
-      final PatchLineComment c = i.next();
-      if (c.getLine() == R_HEAD) {
-        insertFileCommentRow(row);
-      } else {
-        insertRow(row);
-      }
-      bindComment(row, col, c, !i.hasNext(), expandComment);
-      row++;
-    }
-    return row;
-  }
-
-  private void allocateTableHeader(PatchScript script, final SafeHtmlBuilder m) {
-    m.openTr();
-
-    m.openTd();
-    m.addStyleName(Gerrit.RESOURCES.css().iconCell());
-    m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
-    m.closeTd();
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
-    m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
-    m.nbsp();
-    m.closeTd();
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
-    m.addStyleName(Gerrit.RESOURCES.css().fileLine());
-    m.closeTd();
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
-    m.addStyleName(Gerrit.RESOURCES.css().fileLine());
-    m.closeTd();
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
-    m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
-    m.addStyleName(Gerrit.RESOURCES.css().rightmost());
-    m.closeTd();
-
-    m.closeTr();
-  }
-
-  private void appendFileHeader(final SafeHtmlBuilder m, final String line) {
-    m.openTr();
-
-    m.openTd();
-    m.addStyleName(Gerrit.RESOURCES.css().iconCell());
-    m.closeTd();
-
-    appendLineNumber(m, false);
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().sideBySideTableBinaryHeader());
-    m.setAttribute("colspan", 2);
-    m.append(line);
-    m.closeTd();
-
-    appendLineNumber(m, true);
-
-    m.closeTr();
-  }
-
-  private void appendSkipLine(final SafeHtmlBuilder m, final int skipCnt) {
-    m.openTr();
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().iconCell());
-    m.addStyleName(Gerrit.RESOURCES.css().skipLine());
-    m.closeTd();
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().skipLine());
-    m.setAttribute("colspan", 4);
-    m.closeTd();
-    m.closeTr();
-  }
-
-  private ClickHandler expandAllListener = new ClickHandler() {
-    @Override
-    public void onClick(ClickEvent event) {
-      expand(event, 0);
-    }
-  };
-
-  private ClickHandler expandBeforeListener = new ClickHandler() {
-    @Override
-    public void onClick(ClickEvent event) {
-      expand(event, NUM_ROWS_TO_EXPAND);
-    }
-  };
-
-  private ClickHandler expandAfterListener = new ClickHandler() {
-    @Override
-    public void onClick(ClickEvent event) {
-      expand(event, -NUM_ROWS_TO_EXPAND);
-    }
-  };
-
-  private void expand(ClickEvent event, final int numRows) {
-    int row = table.getCellForEvent(event).getRowIndex();
-    if (!(getRowItem(row) instanceof SkippedLine)) {
-      return;
-    }
-
-    SkippedLine line = (SkippedLine) getRowItem(row);
-    int loopTo = numRows;
-    if (numRows == 0) {
-      loopTo = line.getSize();
-    } else if (numRows < 0) {
-      loopTo = -numRows;
-    }
-    int offset = 0;
-    if (numRows < 0) {
-      offset = 1;
-    }
-
-    CellFormatter fmt = table.getCellFormatter();
-    for (int i = 0 + offset; i < loopTo + offset; i++) {
-      insertRow(row + i);
-      table.getRowFormatter().setVerticalAlign(row + i,
-          HasVerticalAlignment.ALIGN_TOP);
-      int lineA = line.getStartA() + i;
-      int lineB = line.getStartB() + i;
-      if (numRows < 0) {
-        lineA = line.getStartA() + line.getSize() + numRows + i - offset;
-        lineB = line.getStartB() + line.getSize() + numRows + i - offset;
-      }
-
-      table.setHTML(row + i, A - 1, "<a href=\"javascript:;\">" + (lineA + 1) + "</a>");
-      fmt.addStyleName(row + i, A - 1, Gerrit.RESOURCES.css().lineNumber());
-
-      table.setHTML(row + i, A, a.getSafeHtmlLine(lineA).asString());
-      fmt.addStyleName(row + i, A, Gerrit.RESOURCES.css().fileLine());
-      fmt.addStyleName(row + i, A, Gerrit.RESOURCES.css().fileLineCONTEXT());
-
-      table.setHTML(row + i, B, b.getSafeHtmlLine(lineB).asString());
-      fmt.addStyleName(row + i, B, Gerrit.RESOURCES.css().fileLine());
-      fmt.addStyleName(row + i, B, Gerrit.RESOURCES.css().fileLineCONTEXT());
-
-      table.setHTML(row + i, B + 1, "<a href=\"javascript:;\">" + (lineB + 1) + "</a>");
-      fmt.addStyleName(row + i, B + 1, Gerrit.RESOURCES.css().lineNumber());
-
-      setRowItem(row + i, new PatchLine(CONTEXT, lineA, lineB));
-    }
-
-    if (numRows > 0) {
-      line.incrementStart(numRows);
-      createSkipLine(row + loopTo, line, isHugeFile);
-    } else if (numRows < 0) {
-      line.reduceSize(-numRows);
-      createSkipLine(row, line, isHugeFile);
-    } else {
-      table.removeRow(row + loopTo);
-    }
-  }
-
-  private void createSkipLine(int row, SkippedLine line, boolean isHugeFile) {
-    FlowPanel p = new FlowPanel();
-    InlineLabel l1 = new InlineLabel(" " + PatchUtil.C.patchSkipRegionStart() + " ");
-    InlineLabel l2 = new InlineLabel(" " + PatchUtil.C.patchSkipRegionEnd() + " ");
-
-    Anchor all = new Anchor(String.valueOf(line.getSize()));
-    all.addClickHandler(expandAllListener);
-    all.setStyleName(Gerrit.RESOURCES.css().skipLine());
-
-    if (line.getSize() > 30) {
-      // Only show the expand before/after if skipped more than 30 lines.
-      Anchor b = new Anchor(PatchUtil.M.expandBefore(NUM_ROWS_TO_EXPAND), true);
-      Anchor a = new Anchor(PatchUtil.M.expandAfter(NUM_ROWS_TO_EXPAND), true);
-
-      b.addClickHandler(expandBeforeListener);
-      a.addClickHandler(expandAfterListener);
-
-      b.setStyleName(Gerrit.RESOURCES.css().skipLine());
-      a.setStyleName(Gerrit.RESOURCES.css().skipLine());
-
-      p.add(b);
-      p.add(l1);
-      if (isHugeFile) {
-        p.add(new InlineLabel(" " + line.getSize() + " "));
-      } else {
-        p.add(all);
-      }
-      p.add(l2);
-      p.add(a);
-    } else {
-      p.add(l1);
-      p.add(all);
-      p.add(l2);
-    }
-    table.setWidget(row, 1, p);
-  }
-
-  private void openLine(final SafeHtmlBuilder m) {
-    m.openTr();
-    m.setAttribute("valign", "top");
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().iconCell());
-    m.closeTd();
-  }
-
-  private void appendLineNumber(SafeHtmlBuilder m, boolean right) {
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
-    if (right) {
-      m.addStyleName(Gerrit.RESOURCES.css().rightmost());
-    }
-    m.closeTd();
-  }
-
-  private void appendLineNumber(SafeHtmlBuilder m, int lineNumberMinusOne, boolean right) {
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
-    if (right) {
-      m.addStyleName(Gerrit.RESOURCES.css().rightmost());
-    }
-    m.append(SafeHtml.asis("<a href=\"javascript:;\">"+ (lineNumberMinusOne + 1) + "</a>"));
-    m.closeTd();
-  }
-
-  private void appendLineText(final SafeHtmlBuilder m,
-      final PatchLine.Type type, final SparseHtmlFile src, final int i,
-      final boolean fullBlock) {
-    appendLineText(m, type, src.getSafeHtmlLine(i), src.hasTrailingEdit(i), fullBlock);
-  }
-
-  private void appendLineText(final SafeHtmlBuilder m,
-      final PatchLine.Type type, final SafeHtml lineHtml,
-      final boolean trailingEdit, final boolean fullBlock) {
-    m.openTd();
-    m.addStyleName(Gerrit.RESOURCES.css().fileLine());
-    switch (type) {
-      case CONTEXT:
-        m.addStyleName(Gerrit.RESOURCES.css().fileLineCONTEXT());
-        break;
-      case DELETE:
-        m.addStyleName(Gerrit.RESOURCES.css().fileLineDELETE());
-        if (trailingEdit || fullBlock) {
-          m.addStyleName("wdd");
-        }
-        break;
-      case INSERT:
-        m.addStyleName(Gerrit.RESOURCES.css().fileLineINSERT());
-        if (trailingEdit || fullBlock) {
-          m.addStyleName("wdi");
-        }
-        break;
-      case REPLACE:
-        break;
-    }
-    m.append(lineHtml);
-    m.closeTd();
-  }
-
-  private void appendLineNone(final SafeHtmlBuilder m, final PatchLine.Type type) {
-    m.openTd();
-    m.addStyleName(Gerrit.RESOURCES.css().fileLine());
-    switch (type != null ? type : PatchLine.Type.CONTEXT) {
-      case DELETE:
-        m.addStyleName(Gerrit.RESOURCES.css().fileLineDELETE());
-        break;
-      case INSERT:
-        m.addStyleName(Gerrit.RESOURCES.css().fileLineINSERT());
-        break;
-      default:
-        m.addStyleName(Gerrit.RESOURCES.css().fileLineNone());
-        break;
-    }
-    m.closeTd();
-  }
-
-  private void closeLine(final SafeHtmlBuilder m) {
-    m.closeTr();
-  }
-
-  @Override
-  protected void destroyCommentRow(final int row) {
-    super.destroyCommentRow(row);
-    if (row == R_HEAD + 1) {
-      table.removeRow(row);
-      isFileCommentBorderRowExist = false;
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
index 9ec4a8b..10c029f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
@@ -46,6 +46,7 @@
   private static final int PC = 3;
   private static final Comparator<PatchLineComment> BY_DATE =
       new Comparator<PatchLineComment>() {
+        @Override
         public int compare(final PatchLineComment o1, final PatchLineComment o2) {
           return o1.getWrittenOn().compareTo(o2.getWrittenOn());
         }
@@ -164,10 +165,12 @@
     nc.closeElement("img");
   }
 
+  @Override
   protected void createFileCommentEditorOnSideA() {
     createCommentEditor(R_HEAD + 1, PC, R_HEAD, FILE_SIDE_A);
   }
 
+  @Override
   protected void createFileCommentEditorOnSideB() {
     createCommentEditor(rowOfTableHeaderB + 1, PC, R_HEAD, FILE_SIDE_B);
     createFileCommentBorderRow();
@@ -352,7 +355,7 @@
 
   @Override
   public void display(final CommentDetail cd, boolean expandComments) {
-    if (cd.isEmpty()) {
+    if (cd == null || cd.isEmpty()) {
       return;
     }
     setAccountInfoCache(cd.getAccounts());
@@ -367,13 +370,13 @@
         row++;
 
         if (!fora.isEmpty()) {
-          row = insert(fora, row, expandComments);
+          row = insert(fora, row);
         }
         rowOfTableHeaderB = row;
         borderRowOfFileComment = row + 1;
         if (!forb.isEmpty()) {
           row++;// Skip the Header of sideB.
-          row = insert(forb, row, expandComments);
+          row = insert(forb, row);
           borderRowOfFileComment = row;
           createFileCommentBorderRow();
         }
@@ -388,13 +391,13 @@
           all.addAll(fora);
           all.addAll(forb);
           Collections.sort(all, BY_DATE);
-          row = insert(all, row, expandComments);
+          row = insert(all, row);
 
         } else if (!fora.isEmpty()) {
-          row = insert(fora, row, expandComments);
+          row = insert(fora, row);
 
         } else if (!forb.isEmpty()) {
-          row = insert(forb, row, expandComments);
+          row = insert(forb, row);
         }
       } else {
         row++;
@@ -417,12 +420,7 @@
     defaultStyle(row, fmt);
   }
 
-  @Override
-  protected PatchScreen.Type getPatchScreenType() {
-    return PatchScreen.Type.UNIFIED;
-  }
-
-  private int insert(final List<PatchLineComment> in, int row, boolean expandComment) {
+  private int insert(final List<PatchLineComment> in, int row) {
     for (Iterator<PatchLineComment> ci = in.iterator(); ci.hasNext();) {
       final PatchLineComment c = ci.next();
       if (c.getLine() == R_HEAD) {
@@ -430,7 +428,7 @@
       } else {
         insertRow(row);
       }
-      bindComment(row, PC, c, !ci.hasNext(), expandComment);
+      bindComment(row, PC, c, !ci.hasNext());
       row++;
     }
     return row;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java
similarity index 82%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java
index ad96f24..a5c1484 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java
@@ -18,14 +18,15 @@
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.RpcStatus;
-import com.google.gerrit.client.changes.CommitMessageBlock;
-import com.google.gerrit.client.changes.PatchTable;
-import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.WebLinkInfo;
+import com.google.gerrit.client.diff.DiffApi;
+import com.google.gerrit.client.diff.DiffInfo;
 import com.google.gerrit.client.projects.ConfigInfoCache;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.data.PatchScript;
@@ -43,51 +44,19 @@
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.ImageResourceRenderer;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
 
-public abstract class PatchScreen extends Screen implements
+import java.util.Collections;
+import java.util.List;
+
+public class UnifiedPatchScreen extends Screen implements
     CommentEditorContainer {
   static final PrettyFactory PRETTY = ClientSideFormatter.FACTORY;
   static final short LARGE_FILE_CONTEXT = 100;
 
-  public static class SideBySide extends PatchScreen {
-    public SideBySide(final Patch.Key id, final int patchIndex,
-        final PatchSetDetail patchSetDetail, final PatchTable patchTable,
-        final TopView topView, final PatchSet.Id baseId) {
-       super(id, patchIndex, patchSetDetail, patchTable, topView, baseId);
-    }
-
-    @Override
-    protected SideBySideTable createContentTable() {
-      return new SideBySideTable();
-    }
-
-    @Override
-    public PatchScreen.Type getPatchScreenType() {
-      return PatchScreen.Type.SIDE_BY_SIDE;
-    }
-  }
-
-  public static class Unified extends PatchScreen {
-    public Unified(final Patch.Key id, final int patchIndex,
-        final PatchSetDetail patchSetDetail, final PatchTable patchTable,
-        final TopView topView, final PatchSet.Id baseId) {
-      super(id, patchIndex, patchSetDetail, patchTable, topView, baseId);
-    }
-
-    @Override
-    protected UnifiedDiffTable createContentTable() {
-      return new UnifiedDiffTable();
-    }
-
-    @Override
-    public PatchScreen.Type getPatchScreenType() {
-      return PatchScreen.Type.UNIFIED;
-    }
-  }
-
   /**
    * What should be displayed in the top of the screen
    */
@@ -108,7 +77,7 @@
   private HistoryTable historyTable;
   private FlowPanel topPanel;
   private FlowPanel contentPanel;
-  private AbstractPatchContentTable contentTable;
+  private UnifiedDiffTable contentTable;
   private CommitMessageBlock commitMessageBlock;
   private NavLinks topNav;
   private NavLinks bottomNav;
@@ -129,24 +98,12 @@
   private boolean intralineFailure;
   private boolean intralineTimeout;
 
-  /**
-   * How this patch should be displayed in the patch screen.
-   */
-  public static enum Type {
-    UNIFIED, SIDE_BY_SIDE
-  }
-
-  protected PatchScreen(final Patch.Key id, final int patchIndex,
-      final PatchSetDetail detail, final PatchTable patchTable,
-      final TopView top, final PatchSet.Id baseId) {
+  public UnifiedPatchScreen(Patch.Key id, TopView top, PatchSet.Id baseId) {
     patchKey = id;
-    patchSetDetail = detail;
-    fileList = patchTable;
     topView = top;
 
     idSideA = baseId; // null here means we're diff'ing from the Base
     idSideB = id.getParentKey();
-    this.patchIndex = patchIndex;
 
     prefs = fileList != null
         ? fileList.getPreferences()
@@ -248,7 +205,7 @@
     topPanel = new FlowPanel();
     add(topPanel);
 
-    contentTable = createContentTable();
+    contentTable = new UnifiedDiffTable();
     contentTable.fileList = fileList;
 
     topNav = new NavLinks(keysNavigation, patchKey.getParentKey());
@@ -256,12 +213,7 @@
 
     add(topNav);
     contentPanel = new FlowPanel();
-    if (getPatchScreenType() == PatchScreen.Type.SIDE_BY_SIDE) {
-      contentPanel.setStyleName(//
-          Gerrit.RESOURCES.css().sideBySideScreenSideBySideTable());
-    } else {
-      contentPanel.setStyleName(Gerrit.RESOURCES.css().unifiedTable());
-    }
+    contentPanel.setStyleName(Gerrit.RESOURCES.css().unifiedTable());
 
     contentPanel.add(contentTable);
     add(contentPanel);
@@ -271,17 +223,48 @@
     }
 
     if (fileList != null) {
-      topNav.display(patchIndex, getPatchScreenType(), fileList);
-      bottomNav.display(patchIndex, getPatchScreenType(), fileList);
+      displayNav();
     }
   }
 
+  private void displayNav() {
+    DiffApi.diff(idSideB, patchKey.getFileName())
+      .base(idSideA)
+      .webLinksOnly()
+      .get(new GerritCallback<DiffInfo>() {
+        @Override
+        public void onSuccess(DiffInfo diffInfo) {
+          topNav.display(patchIndex, fileList,
+              getLinks(), getWebLinks(diffInfo));
+          bottomNav.display(patchIndex, fileList,
+              getLinks(), getWebLinks(diffInfo));
+        }
+      });
+  }
+
+  private List<InlineHyperlink> getLinks() {
+    InlineHyperlink toSideBySideDiffLink = new InlineHyperlink();
+    toSideBySideDiffLink.setHTML(new ImageResourceRenderer().render(Gerrit.RESOURCES.sideBySideDiff()));
+    toSideBySideDiffLink.setTargetHistoryToken(getSideBySideDiffUrl());
+    toSideBySideDiffLink.setTitle(PatchUtil.C.sideBySideDiff());
+    return Collections.singletonList(toSideBySideDiffLink);
+  }
+
+  private List<WebLinkInfo> getWebLinks(DiffInfo diffInfo) {
+    return diffInfo.unified_web_links();
+  }
+
+  private String getSideBySideDiffUrl() {
+    return Dispatcher.toPatch("sidebyside", idSideA,
+        new Patch.Key(idSideB, patchKey.getFileName()));
+  }
+
   @Override
   protected void onLoad() {
     super.onLoad();
 
     if (patchSetDetail == null) {
-      Util.DETAIL_SVC.patchSetDetail(idSideB,
+      PatchUtil.CHANGE_SVC.patchSetDetail(idSideB,
           new GerritCallback<PatchSetDetail>() {
             @Override
             public void onSuccess(PatchSetDetail result) {
@@ -334,10 +317,6 @@
     }
   }
 
-  protected abstract AbstractPatchContentTable createContentTable();
-
-  public abstract PatchScreen.Type getPatchScreenType();
-
   public PatchSet.Id getSideA() {
     return idSideA;
   }
@@ -366,7 +345,7 @@
     final int rpcseq = ++rpcSequence;
     lastScript = null;
     settingsPanel.setEnabled(false);
-    reviewedPanels.populate(patchKey, fileList, patchIndex, getPatchScreenType());
+    reviewedPanels.populate(patchKey, fileList, patchIndex);
     if (isFirst && fileList != null && fileList.isLoaded()) {
       fileList.movePointerTo(patchKey);
     }
@@ -386,7 +365,7 @@
             // Handled by ScreenLoadCallback.onFailure.
           }
         }));
-    PatchUtil.DETAIL_SVC.patchScript(patchKey, idSideA, idSideB,
+    PatchUtil.PATCH_SVC.patchScript(patchKey, idSideA, idSideB,
         settingsPanel.getValue(), cb.addFinal(
             new ScreenLoadCallback<PatchScript>(this) {
               @Override
@@ -423,7 +402,7 @@
           commentLinkProcessor);
     } else {
       commitMessageBlock.setVisible(false);
-      Util.DETAIL_SVC.patchSetDetail(idSideB,
+      PatchUtil.CHANGE_SVC.patchSetDetail(idSideB,
           new GerritCallback<PatchSetDetail>() {
             @Override
             public void onSuccess(PatchSetDetail result) {
@@ -445,22 +424,6 @@
       }
     }
 
-    if (contentTable instanceof SideBySideTable
-        && contentTable.isPureMetaChange(script)
-        && !contentTable.isDisplayBinary) {
-      // User asked for SideBySide (or a link guessed, wrong) and we can't
-      // show a pure-rename change there accurately. Switch to
-      // the unified view instead. User can set file comments on binary file
-      // in SideBySide view.
-      //
-      contentTable.removeFromParent();
-      contentTable = new UnifiedDiffTable();
-      contentTable.fileList = fileList;
-      contentTable.setCommentLinkProcessor(commentLinkProcessor);
-      contentPanel.add(contentTable);
-      setToken(Dispatcher.toPatchUnified(idSideA, patchKey));
-    }
-
     if (script.isHugeFile()) {
       AccountDiffPreference dp = script.getDiffPrefs();
       int context = dp.getContext();
@@ -485,8 +448,7 @@
     lastScript = script;
 
     if (fileList != null) {
-      topNav.display(patchIndex, getPatchScreenType(), fileList);
-      bottomNav.display(patchIndex, getPatchScreenType(), fileList);
+      displayNav();
     }
 
     if (Gerrit.isSignedIn()) {
@@ -562,8 +524,9 @@
         final PatchSet.Id psid = patchKey.getParentKey();
         fileList = new PatchTable(prefs);
         fileList.setSavePointerId("PatchTable " + psid);
-        Util.DETAIL_SVC.patchSetDetail(psid,
+        PatchUtil.CHANGE_SVC.patchSetDetail(psid,
             new GerritCallback<PatchSetDetail>() {
+              @Override
               public void onSuccess(final PatchSetDetail result) {
                 fileList.display(idSideA, result);
               }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
index 849863e..6c1a841 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.client.projects;
 
+import com.google.gerrit.client.WebLinkInfo;
 import com.google.gerrit.client.actions.ActionInfo;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
 
 public class BranchInfo extends JavaScriptObject {
   public final String getShortName() {
@@ -30,6 +32,7 @@
   public final native String revision() /*-{ return this.revision; }-*/;
   public final native boolean canDelete() /*-{ return this['can_delete'] ? true : false; }-*/;
   public final native NativeMap<ActionInfo> actions() /*-{ return this.actions }-*/;
+  public final native JsArray<WebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
 
   protected BranchInfo() {
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
index e3c77b8..7aa5be5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
@@ -17,9 +17,9 @@
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.actions.ActionInfo;
 import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.extensions.api.projects.ProjectState;
-import com.google.gerrit.extensions.common.InheritableBoolean;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.core.client.JsArrayString;
@@ -44,6 +44,9 @@
   public final native InheritedBooleanInfo use_contributor_agreements()
   /*-{ return this.use_contributor_agreements; }-*/;
 
+  public final native InheritedBooleanInfo create_new_change_for_all_not_in_target()
+  /*-{ return this.create_new_change_for_all_not_in_target; }-*/;
+
   public final native InheritedBooleanInfo use_signed_off_by()
   /*-{ return this.use_signed_off_by; }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index 4762027..b121d07 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -15,13 +15,12 @@
 
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.projects.ConfigInfo.ConfigParameterValue;
-import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.extensions.api.projects.ProjectState;
-import com.google.gerrit.extensions.common.InheritableBoolean;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
@@ -59,22 +58,36 @@
     project(name).view("branches").get(cb);
   }
 
+  public static void getBranches(Project.NameKey name, int limit, int start,
+       String match, AsyncCallback<JsArray<BranchInfo>> cb) {
+    RestApi call = project(name).view("branches");
+    call.addParameter("n", limit);
+    call.addParameter("s", start);
+    if (match != null) {
+      if (match.startsWith("^")) {
+        call.addParameter("r", match);
+      } else {
+        call.addParameter("m", match);
+      }
+    }
+    call.get(cb);
+  }
+
   /**
-   * Delete branches. For each branch to be deleted a separate DELETE request is
-   * fired to the server. The {@code onSuccess} method of the provided callback
-   * is invoked once after all requests succeeded. If any request fails the
-   * callbacks' {@code onFailure} method is invoked. In a failure case it can be
-   * that still some of the branches were successfully deleted.
+   * Delete branches. One call is fired to the server to delete all the
+   * branches.
    */
   public static void deleteBranches(Project.NameKey name,
       Set<String> refs, AsyncCallback<VoidResult> cb) {
-    CallbackGroup group = new CallbackGroup();
-    for (String ref : refs) {
-      project(name).view("branches").id(ref)
-          .delete(group.add(cb));
-      cb = CallbackGroup.emptyCallback();
+    if (refs.size() == 1) {
+      project(name).view("branches").id(refs.iterator().next()).delete(cb);
+    } else {
+      DeleteBranchesInput d = DeleteBranchesInput.create();
+      for (String ref : refs) {
+        d.add_branch(ref);
+      }
+      project(name).view("branches:delete").post(d, cb);
     }
-    group.done();
   }
 
   public static void getConfig(Project.NameKey name,
@@ -85,6 +98,7 @@
   public static void setConfig(Project.NameKey name, String description,
       InheritableBoolean useContributorAgreements,
       InheritableBoolean useContentMerge, InheritableBoolean useSignedOffBy,
+      InheritableBoolean createNewChangeForAllNotInTarget,
       InheritableBoolean requireChangeId, String maxObjectSizeLimit,
       SubmitType submitType, ProjectState state,
       Map<String, Map<String, ConfigParameterValue>> pluginConfigValues,
@@ -95,6 +109,7 @@
     in.setUseContentMerge(useContentMerge);
     in.setUseSignedOffBy(useSignedOffBy);
     in.setRequireChangeId(requireChangeId);
+    in.setCreateNewChangeForAllNotInTarget(createNewChangeForAllNotInTarget);
     in.setMaxObjectSizeLimit(maxObjectSizeLimit);
     in.setSubmitType(submitType);
     in.setState(state);
@@ -209,6 +224,12 @@
     private final native void setRequireChangeIdRaw(String v)
     /*-{ if(v)this.require_change_id=v; }-*/;
 
+    final void setCreateNewChangeForAllNotInTarget(InheritableBoolean v) {
+      setCreateNewChangeForAllNotInTargetRaw(v.name());
+    }
+    private final native void setCreateNewChangeForAllNotInTargetRaw(String v)
+    /*-{ if(v)this.create_new_change_for_all_not_in_target=v; }-*/;
+
     final native void setMaxObjectSizeLimit(String l)
     /*-{ if(l)this.max_object_size_limit=l; }-*/;
 
@@ -284,4 +305,18 @@
 
     final native void setRef(String r) /*-{ if(r)this.ref=r; }-*/;
   }
+
+  private static class DeleteBranchesInput extends JavaScriptObject {
+    static DeleteBranchesInput create() {
+      DeleteBranchesInput d = createObject().cast();
+      d.init();
+      return d;
+    }
+
+    protected DeleteBranchesInput() {
+    }
+
+    final native void init() /*-{ this.branches = []; }-*/;
+    final native void add_branch(String b) /*-{ this.branches.push(b); }-*/;
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
index 048ebbd..fe9872c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.client.projects;
 
 import com.google.gerrit.client.WebLinkInfo;
-import com.google.gerrit.extensions.api.projects.ProjectState;
+import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
index 29a8a01..0f121c8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
@@ -50,6 +50,7 @@
         .addParameter("n", limit)
         .addParameterRaw("type", "ALL")
         .addParameterTrue("d") // description
+        .background()
         .get(NativeMap.copyKeysIntoChildren(cb));
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java
index 7eaada0..071ca72 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java
@@ -42,8 +42,8 @@
  * processing it.
  */
 public class CallbackGroup {
-  private final List<CallbackImpl<?>> callbacks;
-  private final Set<CallbackImpl<?>> remaining;
+  private final List<CallbackGlue> callbacks;
+  private final Set<CallbackGlue> remaining;
   private boolean finalAdded;
 
   private boolean failed;
@@ -66,11 +66,37 @@
     remaining = new HashSet<>();
   }
 
+  public <T> Callback<T> addEmpty() {
+    Callback<T> cb = emptyCallback();
+    return add(cb);
+  }
+
   public <T> Callback<T> add(final AsyncCallback<T> cb) {
     checkFinalAdded();
     return handleAdd(cb);
   }
 
+  public <T> HttpCallback<T> add(HttpCallback<T> cb) {
+    checkFinalAdded();
+    if (failed) {
+      cb.onFailure(failedThrowable);
+      return new HttpCallback<T>() {
+        @Override
+        public void onSuccess(HttpResponse<T> result) {
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+        }
+      };
+    }
+
+    HttpCallbackImpl<T> w = new HttpCallbackImpl<>(cb);
+    callbacks.add(w);
+    remaining.add(w);
+    return w;
+  }
+
   public <T> Callback<T> addFinal(final AsyncCallback<T> cb) {
     checkFinalAdded();
     finalAdded = true;
@@ -79,7 +105,7 @@
 
   public void done() {
     finalAdded = true;
-    applyAllSuccess();
+    apply();
   }
 
   public void addListener(AsyncCallback<Void> cb) {
@@ -91,13 +117,33 @@
   }
 
   public void addListener(CallbackGroup group) {
-    addListener(group.add(CallbackGroup.<Void> emptyCallback()));
+    addListener(group.<Void> addEmpty());
   }
 
-  private void applyAllSuccess() {
-    if (!failed && finalAdded && remaining.isEmpty()) {
-      for (CallbackImpl<?> cb : callbacks) {
-        cb.applySuccess();
+  private void success(CallbackGlue cb) {
+    remaining.remove(cb);
+    apply();
+  }
+
+  private <T> void failure(CallbackGlue w, Throwable caught) {
+    if (!failed) {
+      failed = true;
+      failedThrowable = caught;
+    }
+    remaining.remove(w);
+    apply();
+  }
+
+  private void apply() {
+    if (finalAdded && remaining.isEmpty()) {
+      if (failed) {
+        for (CallbackGlue cb : callbacks) {
+          cb.applyFailed();
+        }
+      } else {
+        for (CallbackGlue cb : callbacks) {
+          cb.applySuccess();
+        }
       }
       callbacks.clear();
     }
@@ -125,7 +171,12 @@
       extends AsyncCallback<T>, com.google.gwtjsonrpc.common.AsyncCallback<T> {
   }
 
-  private class CallbackImpl<T> implements Callback<T> {
+  private interface CallbackGlue {
+    void applySuccess();
+    void applyFailed();
+  }
+
+  private class CallbackImpl<T> implements Callback<T>, CallbackGlue {
     AsyncCallback<T> delegate;
     T result;
 
@@ -135,33 +186,17 @@
 
     @Override
     public void onSuccess(T value) {
-      if (failed) {
-        return;
-      }
-
       this.result = value;
-      remaining.remove(this);
-      CallbackGroup.this.applyAllSuccess();
+      success(this);
     }
 
     @Override
     public void onFailure(Throwable caught) {
-      if (failed) {
-        return;
-      }
-
-      failed = true;
-      failedThrowable = caught;
-      for (CallbackImpl<?> cb : callbacks) {
-        cb.delegate.onFailure(failedThrowable);
-        cb.delegate = null;
-        cb.result = null;
-      }
-      callbacks.clear();
-      remaining.clear();
+      failure(this, caught);
     }
 
-    void applySuccess() {
+    @Override
+    public void applySuccess() {
       AsyncCallback<T> cb = delegate;
       if (cb != null) {
         delegate = null;
@@ -169,5 +204,55 @@
         result = null;
       }
     }
+
+    @Override
+    public void applyFailed() {
+      AsyncCallback<T> cb = delegate;
+      if (cb != null) {
+        delegate = null;
+        result = null;
+        cb.onFailure(failedThrowable);
+      }
+    }
+  }
+
+  private class HttpCallbackImpl<T> implements HttpCallback<T>, CallbackGlue {
+    private HttpCallback<T> delegate;
+    private HttpResponse<T> result;
+
+    HttpCallbackImpl(HttpCallback<T> delegate) {
+      this.delegate = delegate;
+    }
+
+    @Override
+    public void onSuccess(HttpResponse<T> result) {
+      this.result = result;
+      success(this);
+    }
+
+    @Override
+    public void onFailure(Throwable caught) {
+      failure(this, caught);
+    }
+
+    @Override
+    public void applySuccess() {
+      HttpCallback<T> cb = delegate;
+      if (cb != null) {
+        delegate = null;
+        cb.onSuccess(result);
+        result = null;
+      }
+    }
+
+    @Override
+    public void applyFailed() {
+      HttpCallback<T> cb = delegate;
+      if (cb != null) {
+        delegate = null;
+        result = null;
+        cb.onFailure(failedThrowable);
+      }
+    }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
index 06d1f0b..bccd237 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.common.errors.NotSignedInException;
-import com.google.gwt.core.client.GWT;
 import com.google.gwt.user.client.rpc.InvocationException;
 import com.google.gwtjsonrpc.client.RemoteJsonException;
 import com.google.gwtjsonrpc.client.ServerUnavailableException;
@@ -33,7 +32,12 @@
 public abstract class GerritCallback<T> implements
     com.google.gwtjsonrpc.common.AsyncCallback<T>,
     com.google.gwt.user.client.rpc.AsyncCallback<T> {
+  @Override
   public void onFailure(final Throwable caught) {
+    showFailure(caught);
+  }
+
+  public static void showFailure(Throwable caught) {
     if (isNotSignedIn(caught) || isInvalidXSRF(caught)) {
       new NotSignedInDialog().center();
 
@@ -69,7 +73,6 @@
       new ErrorDialog(RpcConstants.C.errorServerUnavailable()).center();
 
     } else {
-      GWT.log(getClass().getName() + " caught " + caught, caught);
       new ErrorDialog(caught).center();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/HttpCallback.java
similarity index 67%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/HttpCallback.java
index 407b7c7..a97642e 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/HttpCallback.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2015 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,10 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.client.rpc;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+/** AsyncCallback supplied with HTTP response headers. */
+public interface HttpCallback<T> {
+  void onSuccess(HttpResponse<T> result);
+  void onFailure(Throwable caught);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/HttpResponse.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/HttpResponse.java
new file mode 100644
index 0000000..969dd30
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/HttpResponse.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.rpc;
+
+import com.google.gwt.http.client.Response;
+
+/** Wraps decoded server reply with HTTP headers. */
+public class HttpResponse<T> {
+  private final Response httpResponse;
+  private final String contentType;
+  private final T result;
+
+  HttpResponse(Response httpResponse, String contentType, T result) {
+    this.httpResponse = httpResponse;
+    this.contentType = contentType;
+    this.result = result;
+  }
+
+  /** HTTP status code, always in the 2xx family. */
+  public int getStatusCode() {
+    return httpResponse.getStatusCode();
+  }
+
+  /**
+   * Content type supplied by the server.
+   *
+   * This helper simplifies the common {@code getHeader("Content-Type")} case.
+   */
+  public String getContentType() {
+    return contentType;
+  }
+
+  /** Lookup an arbitrary reply header. */
+  public String getHeader(String header) {
+    if ("Content-Type".equals(header)) {
+      return contentType;
+    }
+    return httpResponse.getHeader(header);
+  }
+
+  public T getResult() {
+    return result;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
index 2bc4ac1..e48477f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
@@ -45,6 +45,7 @@
   private static final String JSON_TYPE = "application/json";
   private static final String JSON_UTF8 = JSON_TYPE + "; charset=utf-8";
   private static final String TEXT_TYPE = "text/plain";
+  private static final String TEXT_UTF8 = TEXT_TYPE + "; charset=utf-8";
 
   /**
    * Expected JSON content body prefix that prevents XSSI.
@@ -104,21 +105,21 @@
     }
   }
 
-  private static class HttpCallback<T extends JavaScriptObject>
+  private static class HttpImpl<T extends JavaScriptObject>
       implements RequestCallback {
     private final boolean background;
-    private final AsyncCallback<T> cb;
+    private final HttpCallback<T> cb;
 
-    HttpCallback(boolean bg, AsyncCallback<T> cb) {
+    HttpImpl(boolean bg, HttpCallback<T> cb) {
       this.background = bg;
       this.cb = cb;
     }
 
     @Override
-    public void onResponseReceived(Request req, Response res) {
+    public void onResponseReceived(Request req, final Response res) {
       int status = res.getStatusCode();
       if (status == Response.SC_NO_CONTENT) {
-        cb.onSuccess(null);
+        cb.onSuccess(new HttpResponse<T>(res, null, null));
         if (!background) {
           RpcStatus.INSTANCE.onRpcComplete();
         }
@@ -126,12 +127,17 @@
       } else if (200 <= status && status < 300) {
         long start = System.currentTimeMillis();
         final T data;
-        if (isTextBody(res)) {
-          data = NativeString.wrap(res.getText()).cast();
-        } else if (isJsonBody(res)) {
+        final String type;
+        if (isJsonBody(res)) {
           try {
-            // javac generics bug
-            data = RestApi.<T>cast(parseJson(res));
+            JSONValue val = parseJson(res);
+            if (isJsonEncoded(res) && val.isString() != null) {
+              data = NativeString.wrap(val.isString().stringValue()).cast();
+              type = simpleType(res.getHeader("X-FYI-Content-Type"));
+            } else {
+              data = RestApi.<T> cast(val);
+              type = JSON_TYPE;
+            }
           } catch (JSONException e) {
             if (!background) {
               RpcStatus.INSTANCE.onRpcComplete();
@@ -140,6 +146,9 @@
                 "Invalid JSON: " + e.getMessage()));
             return;
           }
+        } else if (isTextBody(res)) {
+          data = NativeString.wrap(res.getText()).cast();
+          type = TEXT_TYPE;
         } else {
           if (!background) {
             RpcStatus.INSTANCE.onRpcComplete();
@@ -154,7 +163,7 @@
           @Override
           public void execute() {
             try {
-              cb.onSuccess(data);
+              cb.onSuccess(new HttpResponse<>(res, type, data));
             } finally {
               if (!background) {
                 RpcStatus.INSTANCE.onRpcComplete();
@@ -318,16 +327,24 @@
   }
 
   public <T extends JavaScriptObject> void get(AsyncCallback<T> cb) {
+    get(wrap(cb));
+  }
+
+  public <T extends JavaScriptObject> void get(HttpCallback<T> cb) {
     send(GET, cb);
   }
 
   public <T extends JavaScriptObject> void delete(AsyncCallback<T> cb) {
+    delete(wrap(cb));
+  }
+
+  public <T extends JavaScriptObject> void delete(HttpCallback<T> cb) {
     send(DELETE, cb);
   }
 
-  private <T extends JavaScriptObject> void send(
-      Method method, AsyncCallback<T> cb) {
-    HttpCallback<T> httpCallback = new HttpCallback<>(background, cb);
+  private <T extends JavaScriptObject> void send(Method method,
+      HttpCallback<T> cb) {
+    HttpImpl<T> httpCallback = new HttpImpl<>(background, cb);
     try {
       if (!background) {
         RpcStatus.INSTANCE.onRpcStart();
@@ -341,33 +358,59 @@
   public <T extends JavaScriptObject> void post(
       JavaScriptObject content,
       AsyncCallback<T> cb) {
+    post(content, wrap(cb));
+  }
+
+  public <T extends JavaScriptObject> void post(
+      JavaScriptObject content,
+      HttpCallback<T> cb) {
     sendJSON(POST, content, cb);
   }
 
   public <T extends JavaScriptObject> void post(String content,
       AsyncCallback<T> cb) {
-    sendRaw(POST, content, cb);
+    post(content, wrap(cb));
+  }
+
+  public <T extends JavaScriptObject> void post(String content,
+      HttpCallback<T> cb) {
+    sendText(POST, content, cb);
   }
 
   public <T extends JavaScriptObject> void put(AsyncCallback<T> cb) {
+    put(wrap(cb));
+  }
+
+  public <T extends JavaScriptObject> void put(HttpCallback<T> cb) {
     send(PUT, cb);
   }
 
   public <T extends JavaScriptObject> void put(String content,
       AsyncCallback<T> cb) {
-    sendRaw(PUT, content, cb);
+    put(content, wrap(cb));
+  }
+
+  public <T extends JavaScriptObject> void put(String content,
+      HttpCallback<T> cb) {
+    sendText(PUT, content, cb);
   }
 
   public <T extends JavaScriptObject> void put(
       JavaScriptObject content,
       AsyncCallback<T> cb) {
+    put(content, wrap(cb));
+  }
+
+  public <T extends JavaScriptObject> void put(
+      JavaScriptObject content,
+      HttpCallback<T> cb) {
     sendJSON(PUT, content, cb);
   }
 
   private <T extends JavaScriptObject> void sendJSON(
       Method method, JavaScriptObject content,
-      AsyncCallback<T> cb) {
-    HttpCallback<T> httpCallback = new HttpCallback<>(background, cb);
+      HttpCallback<T> cb) {
+    HttpImpl<T> httpCallback = new HttpImpl<>(background, cb);
     try {
       if (!background) {
         RpcStatus.INSTANCE.onRpcStart();
@@ -380,17 +423,18 @@
     }
   }
 
-  private static native String str(JavaScriptObject jso) /*-{ return JSON.stringify(jso); }-*/;
+  private static native String str(JavaScriptObject jso)
+  /*-{ return JSON.stringify(jso) }-*/;
 
-  private <T extends JavaScriptObject> void sendRaw(Method method, String body,
-      AsyncCallback<T> cb) {
-    HttpCallback<T> httpCallback = new HttpCallback<>(background, cb);
+  private <T extends JavaScriptObject> void sendText(Method method, String body,
+      HttpCallback<T> cb) {
+    HttpImpl<T> httpCallback = new HttpImpl<>(background, cb);
     try {
       if (!background) {
         RpcStatus.INSTANCE.onRpcStart();
       }
       RequestBuilder req = request(method);
-      req.setHeader("Content-Type", TEXT_TYPE);
+      req.setHeader("Content-Type", TEXT_UTF8);
       req.sendRequest(body, httpCallback);
     } catch (RequestException e) {
       httpCallback.onError(null, e);
@@ -417,16 +461,21 @@
     return isContentType(res, TEXT_TYPE);
   }
 
+  private static boolean isJsonEncoded(Response res) {
+    return "json".equals(res.getHeader("X-FYI-Content-Encoding"));
+  }
+
   private static boolean isContentType(Response res, String want) {
     String type = res.getHeader("Content-Type");
-    if (type == null) {
-      return false;
-    }
+    return type != null && want.equals(simpleType(type));
+  }
+
+  private static String simpleType(String type) {
     int semi = type.indexOf(';');
     if (semi >= 0) {
-      type = type.substring(0, semi).trim();
+      return type.substring(0, semi).trim();
     }
-    return want.equals(type);
+    return type;
   }
 
   private static JSONValue parseJson(Response res)
@@ -459,4 +508,19 @@
       throw new JSONException("unsupported JSON type");
     }
   }
+
+  private static <T extends JavaScriptObject> HttpCallback<T> wrap(
+      final AsyncCallback<T> cb) {
+    return new HttpCallback<T>() {
+      @Override
+      public void onSuccess(HttpResponse<T> r) {
+        cb.onSuccess(r.getResult());
+      }
+
+      @Override
+      public void onFailure(Throwable e) {
+        cb.onFailure(e);
+      }
+    };
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/ScreenLoadCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/ScreenLoadCallback.java
index b8b9209..8128afe 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/ScreenLoadCallback.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/ScreenLoadCallback.java
@@ -28,6 +28,7 @@
     screen = s;
   }
 
+  @Override
   public final void onSuccess(final T result) {
     if (screen.isAttached()) {
       preDisplay(result);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/starFilled.gif b/gerrit-gwtui/src/main/java/com/google/gerrit/client/starFilled.gif
deleted file mode 100644
index 77619f0..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/starFilled.gif
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/starOpen.gif b/gerrit-gwtui/src/main/java/com/google/gerrit/client/starOpen.gif
deleted file mode 100644
index e8dc0a3..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/starOpen.gif
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
index 6046995..5f4081b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
@@ -35,10 +35,12 @@
   @Override
   public void _onRequestSuggestions(final Request req, final Callback callback) {
     RpcStatus.hide(new Runnable() {
+      @Override
       public void run() {
         SuggestUtil.SVC.suggestAccountGroupForProject(
             projectName, req.getQuery(), req.getLimit(),
             new GerritCallback<List<GroupReference>>() {
+              @Override
               public void onSuccess(final List<GroupReference> result) {
                 priorResults.clear();
                 final ArrayList<AccountGroupSuggestion> r =
@@ -66,10 +68,12 @@
       info = k;
     }
 
+    @Override
     public String getDisplayString() {
       return info.getName();
     }
 
+    @Override
     public String getReplacementString() {
       return info.getName();
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
index 4ffcd18..78350db 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
@@ -15,9 +15,11 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.RpcStatus;
+import com.google.gerrit.client.account.AccountApi;
+import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.common.data.AccountInfo;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.ui.SuggestOracle;
 
 import java.util.ArrayList;
@@ -26,23 +28,18 @@
 /** Suggestion Oracle for Account entities. */
 public class AccountSuggestOracle extends SuggestAfterTypingNCharsOracle {
   @Override
-  public void _onRequestSuggestions(final Request req, final Callback callback) {
-    RpcStatus.hide(new Runnable() {
-      public void run() {
-        SuggestUtil.SVC.suggestAccount(req.getQuery(), Boolean.TRUE,
-            req.getLimit(),
-            new GerritCallback<List<AccountInfo>>() {
-              public void onSuccess(final List<AccountInfo> result) {
-                final ArrayList<AccountSuggestion> r =
-                    new ArrayList<>(result.size());
-                for (final AccountInfo p : result) {
-                  r.add(new AccountSuggestion(p));
-                }
-                callback.onSuggestionsReady(req, new Response(r));
-              }
-            });
-      }
-    });
+  public void _onRequestSuggestions(final Request req, final Callback cb) {
+    AccountApi.suggest(req.getQuery(), req.getLimit(),
+        new GerritCallback<JsArray<AccountInfo>>() {
+          @Override
+          public void onSuccess(JsArray<AccountInfo> in) {
+            List<AccountSuggestion> r = new ArrayList<>(in.length());
+            for (AccountInfo p : Natives.asList(in)) {
+              r.add(new AccountSuggestion(p));
+            }
+            cb.onSuggestionsReady(req, new Response(r));
+          }
+        });
   }
 
   private static class AccountSuggestion implements SuggestOracle.Suggestion {
@@ -52,12 +49,14 @@
       info = k;
     }
 
+    @Override
     public String getDisplayString() {
-      return FormatUtil.nameEmail(FormatUtil.asInfo(info));
+      return FormatUtil.nameEmail(info);
     }
 
+    @Override
     public String getReplacementString() {
-      return FormatUtil.nameEmail(FormatUtil.asInfo(info));
+      return FormatUtil.nameEmail(info);
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ActionDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ActionDialog.java
deleted file mode 100644
index 295842c..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ActionDialog.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeDetailCache;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.ChangeDetail;
-import com.google.gwt.user.client.ui.FocusWidget;
-
-public abstract class ActionDialog extends CommentedActionDialog<ChangeDetail> {
-  public ActionDialog(final FocusWidget enableOnFailure, final boolean redirect,
-      String dialogTitle, String dialogHeading) {
-    super(dialogTitle, dialogHeading, new ChangeDetailCache.IgnoreErrorCallback() {
-        @Override
-        public void onSuccess(ChangeDetail result) {
-          if (redirect) {
-            Gerrit.display(PageLinks.toChange(result.getChange().getId()));
-          } else {
-            super.onSuccess(result);
-          }
-        }
-
-        @Override
-        public void onFailure(Throwable caught) {
-          enableOnFailure.setEnabled(true);
-        }
-      });
-  }
-}
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java
index d4aaa4c..72233f5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java
@@ -15,96 +15,56 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.admin.Util;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
 import com.google.gwt.event.logical.shared.SelectionEvent;
 import com.google.gwt.event.logical.shared.SelectionHandler;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
-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;
-import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
 
 public class AddMemberBox extends Composite {
   private final FlowPanel addPanel;
   private final Button addMember;
-  private final HintTextBox nameTxtBox;
-  private final SuggestBox nameTxt;
-  private boolean submitOnSelection;
-
-  public AddMemberBox() {
-    this(Util.C.buttonAddGroupMember(), Util.C.defaultAccountName(),
-        new AccountSuggestOracle());
-  }
+  private final RemoteSuggestBox suggestBox;
 
   public AddMemberBox(final String buttonLabel, final String hint,
       final SuggestOracle suggestOracle) {
     addPanel = new FlowPanel();
     addMember = new Button(buttonLabel);
-    nameTxtBox = new HintTextBox();
-    nameTxt = new SuggestBox(new RPCSuggestOracle(
-        suggestOracle), nameTxtBox);
-    nameTxt.setStyleName(Gerrit.RESOURCES.css().addMemberTextBox());
 
-    nameTxtBox.setVisibleLength(50);
-    nameTxtBox.setHintText(hint);
-    nameTxtBox.addKeyPressHandler(new KeyPressHandler() {
+    suggestBox = new RemoteSuggestBox(suggestOracle);
+    suggestBox.setStyleName(Gerrit.RESOURCES.css().addMemberTextBox());
+    suggestBox.setVisibleLength(50);
+    suggestBox.setHintText(hint);
+    suggestBox.addSelectionHandler(new SelectionHandler<String>() {
       @Override
-      public void onKeyPress(KeyPressEvent event) {
-        submitOnSelection = false;
-
-        if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-          if (((DefaultSuggestionDisplay) nameTxt.getSuggestionDisplay())
-              .isSuggestionListShowing()) {
-            submitOnSelection = true;
-          } else {
-            doAdd();
-          }
-        }
-      }
-    });
-    nameTxt.addSelectionHandler(new SelectionHandler<Suggestion>() {
-      @Override
-      public void onSelection(SelectionEvent<Suggestion> event) {
-        nameTxtBox.setFocus(true);
-        if (submitOnSelection) {
-          submitOnSelection = false;
-          doAdd();
-        }
+      public void onSelection(SelectionEvent<String> event) {
+        addMember.fireEvent(new ClickEvent() {});
       }
     });
 
-    addPanel.add(nameTxt);
+    addPanel.add(suggestBox);
     addPanel.add(addMember);
 
     initWidget(addPanel);
   }
 
-  public void addClickHandler(final ClickHandler handler) {
+  public void addClickHandler(ClickHandler handler) {
     addMember.addClickHandler(handler);
   }
 
   public String getText() {
-    String s = nameTxtBox.getText();
-    return s == null ? "" : s;
+    return suggestBox.getText();
   }
 
   public void setEnabled(boolean enabled) {
     addMember.setEnabled(enabled);
-    nameTxtBox.setEnabled(enabled);
+    suggestBox.setEnabled(enabled);
   }
 
   public void setText(String text) {
-    nameTxtBox.setText(text);
-  }
-
-  private void doAdd() {
-    addMember.fireEvent(new ClickEvent() {});
+    suggestBox.setText(text);
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
index 54c4c53..00269f9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.FocusWidget;
 import com.google.gwt.user.client.ui.SuggestBox;
 import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
@@ -32,12 +31,12 @@
 import java.util.LinkedList;
 import java.util.List;
 
-public abstract class CherryPickDialog extends ActionDialog {
+public abstract class CherryPickDialog extends TextAreaActionDialog {
   private SuggestBox newBranch;
   private List<BranchInfo> branches;
 
-  public CherryPickDialog(final FocusWidget enableOnFailure, Project.NameKey project) {
-    super(enableOnFailure, true, Util.C.cherryPickTitle(), Util.C
+  public CherryPickDialog(Project.NameKey project) {
+    super(Util.C.cherryPickTitle(), Util.C
         .cherryPickCommitMessage());
     ProjectApi.getBranches(project,
         new GerritCallback<JsArray<BranchInfo>>() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
index bb50b19..748cd3c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
@@ -257,6 +257,7 @@
 
   private static class DoubleClickHTML extends HTML implements
       HasDoubleClickHandlers {
+    @Override
     public HandlerRegistration addDoubleClickHandler(DoubleClickHandler handler) {
       return addDomHandler(handler, DoubleClickEvent.getType());
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
index b8fa373..60b5f93 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
@@ -15,47 +15,35 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.logical.shared.CloseEvent;
 import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.FocusWidget;
 import com.google.gwt.user.client.ui.PopupPanel;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.globalkey.client.NpTextArea;
 import com.google.gwtexpui.user.client.AutoCenterDialogBox;
 
-public abstract class CommentedActionDialog<T> extends AutoCenterDialogBox
+public abstract class CommentedActionDialog extends AutoCenterDialogBox
     implements CloseHandler<PopupPanel> {
   protected final FlowPanel panel;
-  protected final NpTextArea message;
   protected final Button sendButton;
   protected final Button cancelButton;
   protected final FlowPanel buttonPanel;
-  protected AsyncCallback<T> callback;
+  protected final FlowPanel contentPanel;
   protected FocusWidget focusOn;
 
   protected boolean sent = false;
 
-  public CommentedActionDialog(final String title, final String heading,
-      AsyncCallback<T> callback) {
+  public CommentedActionDialog(final String title, final String heading) {
     super(/* auto hide */false, /* modal */true);
-    this.callback = callback;
     setGlassEnabled(true);
     setText(title);
 
     addStyleName(Gerrit.RESOURCES.css().commentedActionDialog());
 
-    message = new NpTextArea();
-    message.setCharacterWidth(60);
-    message.setVisibleLines(10);
-    message.getElement().setPropertyBoolean("spellcheck", true);
-    setFocusOn(message);
     sendButton = new Button(Util.C.commentedActionButtonSend());
     sendButton.addClickHandler(new ClickHandler() {
       @Override
@@ -74,9 +62,8 @@
       }
     });
 
-    final FlowPanel mwrap = new FlowPanel();
-    mwrap.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
-    mwrap.add(message);
+    contentPanel = new FlowPanel();
+    contentPanel.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
 
     buttonPanel = new FlowPanel();
     buttonPanel.add(sendButton);
@@ -84,8 +71,10 @@
     buttonPanel.getElement().getStyle().setProperty("marginTop", "4px");
 
     panel = new FlowPanel();
-    panel.add(new SmallHeading(heading));
-    panel.add(mwrap);
+    if (heading != null) {
+      panel.add(new SmallHeading(heading));
+    }
+    panel.add(contentPanel);
     panel.add(buttonPanel);
     add(panel);
 
@@ -112,38 +101,8 @@
 
   @Override
   public void onClose(CloseEvent<PopupPanel> event) {
-    if (!sent) {
-      // the dialog was closed without the send button being pressed
-      // e.g. the user pressed Cancel or ESC to close the dialog
-      if (callback != null) {
-        callback.onFailure(null);
-      }
-    }
     sent = false;
   }
 
   public abstract void onSend();
-
-  public String getMessageText() {
-    return message.getText().trim();
-  }
-
-  public AsyncCallback<T> createCallback() {
-    return new GerritCallback<T>(){
-      @Override
-      public void onSuccess(T result) {
-        sent = true;
-        if (callback != null) {
-          callback.onSuccess(result);
-        }
-        hide();
-      }
-
-      @Override
-      public void onFailure(Throwable caught) {
-        enableButtons(true);
-        super.onFailure(caught);
-      }
-    };
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java
index eb62809..4cc6a16 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.client.ui;
 
-import com.google.gerrit.client.Gerrit;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.event.logical.shared.CloseHandler;
 import com.google.gwt.event.logical.shared.HasCloseHandlers;
@@ -56,7 +55,6 @@
       {
         setElement((Element)(DOM.createTD()));
         getElement().setInnerHTML("&nbsp;");
-        addStyleName(Gerrit.RESOURCES.css().complexHeader());
       }
 
       @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
new file mode 100644
index 0000000..021f39c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
@@ -0,0 +1,105 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.projects.BranchInfo;
+import com.google.gerrit.client.projects.ProjectApi;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.SuggestBox;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class CreateChangeDialog extends TextAreaActionDialog {
+  private SuggestBox newChange;
+  private List<BranchInfo> branches;
+
+  public CreateChangeDialog(Project.NameKey project) {
+    super(Util.C.dialogCreateChangeTitle(),
+        Util.C.dialogCreateChangeHeading());
+    ProjectApi.getBranches(project,
+        new GerritCallback<JsArray<BranchInfo>>() {
+          @Override
+          public void onSuccess(JsArray<BranchInfo> result) {
+            branches = Natives.asList(result);
+          }
+        });
+
+    newChange = new SuggestBox(new HighlightSuggestOracle() {
+      @Override
+      protected void onRequestSuggestions(Request request, Callback done) {
+        List<BranchSuggestion> suggestions = new ArrayList<>();
+        for (BranchInfo b : branches) {
+          if (b.ref().contains(request.getQuery())) {
+            suggestions.add(new BranchSuggestion(b));
+          }
+        }
+        done.onSuggestionsReady(request, new Response(suggestions));
+      }
+    });
+
+    newChange.setWidth("100%");
+    newChange.getElement().getStyle().setProperty("boxSizing", "border-box");
+    message.setCharacterWidth(70);
+
+    FlowPanel mwrap = new FlowPanel();
+    mwrap.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
+    mwrap.add(newChange);
+
+    panel.insert(mwrap, 0);
+    panel.insert(new SmallHeading(Util.C.newChangeBranchSuggestion()), 0);
+  }
+
+  @Override
+  public void center() {
+    super.center();
+    GlobalKey.dialog(this);
+    newChange.setFocus(true);
+  }
+
+  public String getDestinationBranch() {
+    return newChange.getText();
+  }
+
+  class BranchSuggestion implements Suggestion {
+    private BranchInfo branch;
+
+    public BranchSuggestion(BranchInfo branch) {
+      this.branch = branch;
+    }
+
+    @Override
+    public String getDisplayString() {
+      if (branch.ref().startsWith(Branch.R_HEADS)) {
+        return branch.ref().substring(Branch.R_HEADS.length());
+      }
+      return branch.ref();
+    }
+
+    @Override
+    public String getReplacementString() {
+      return branch.getShortName();
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ExpandAllCommand.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ExpandAllCommand.java
index 43721d1..9abf135 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ExpandAllCommand.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ExpandAllCommand.java
@@ -28,6 +28,7 @@
     open = isOpen;
   }
 
+  @Override
   public void execute() {
     for (final Widget w : panel) {
       if (w instanceof CommentPanel) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FilteredUserInterface.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FilteredUserInterface.java
deleted file mode 100644
index 3ea7acc..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FilteredUserInterface.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-public interface FilteredUserInterface {
-  /**
-   * Return the value by which the user interface is currently filtered.
-   *
-   * @return value by which the user interface is currently filtered,
-   *         {@code null} or empty String if currently no filter is applied
-   */
-  public String getCurrentFilter();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java
index 09550a6..2ec6cd93 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java
@@ -43,6 +43,7 @@
   private boolean isFocused;
 
 
+  @Override
   public String getText() {
     if (hintOn) {
       return "";
@@ -50,6 +51,7 @@
     return super.getText();
   }
 
+  @Override
   public void setText(String text) {
     focusHint();
 
@@ -196,6 +198,7 @@
     }
   }
 
+  @Override
   public void setFocus(boolean focus) {
     super.setFocus(focus);
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/IgnoreOutdatedFilterResultsCallbackWrapper.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/IgnoreOutdatedFilterResultsCallbackWrapper.java
deleted file mode 100644
index c9cadcc..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/IgnoreOutdatedFilterResultsCallbackWrapper.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gerrit.client.rpc.GerritCallback;
-
-/**
- * GerritCallback to be used on user interfaces that allow filtering to handle
- * RPC's that request filtering. The user may change the filter quickly so that
- * a response may be outdated when the client receives it. In this case the
- * response must be ignored because the responses to RCP's may come out-of-order
- * and an outdated response would overwrite the correct result which was
- * received before.
- */
-public class IgnoreOutdatedFilterResultsCallbackWrapper<T> extends GerritCallback<T> {
-  private final FilteredUserInterface filteredUI;
-  private final String myFilter;
-  private final GerritCallback<T> cb;
-
-  public IgnoreOutdatedFilterResultsCallbackWrapper(
-      final FilteredUserInterface filteredUI, final GerritCallback<T> cb) {
-    this.filteredUI = filteredUI;
-    this.myFilter = filteredUI.getCurrentFilter();
-    this.cb = cb;
-  }
-
-  @Override
-  public void onSuccess(final T result) {
-    if ((myFilter == null && filteredUI.getCurrentFilter() == null)
-        || (myFilter != null && myFilter.equals(filteredUI.getCurrentFilter()))) {
-      cb.onSuccess(result);
-    }
-    // Else ignore the result, the user has already changed the filter
-    // and the result is not relevant anymore. If multiple RPC's are
-    // fired the results may come back out-of-order and a non-relevant
-    // result could overwrite the correct result if not ignored.
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
index deca808..4f7d419 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
@@ -86,6 +86,7 @@
     return body.getWidgetIndex(i);
   }
 
+  @Override
   public void onScreenLoad(ScreenLoadEvent event) {
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuItem.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuItem.java
index 29c053b..9cc91a0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuItem.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuItem.java
@@ -38,6 +38,7 @@
     this.bar = bar;
   }
 
+  @Override
   public void onScreenLoad(ScreenLoadEvent event) {
     if (match(event.getScreen().getToken())) {
       Gerrit.selectMenu(bar);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableOldValue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableOldValue.java
index 47bca2b..758dff4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableOldValue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableOldValue.java
@@ -22,6 +22,7 @@
     return oldValue;
   }
 
+  @Override
   public void set(final T value) {
     try {
       oldValue = get();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableValue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableValue.java
index d834eb2..c8bb12e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableValue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableValue.java
@@ -37,10 +37,12 @@
     fireEvent(new ValueChangeEvent<T>(value) {});
   }
 
+  @Override
   public void fireEvent(GwtEvent<?> event) {
     manager.fireEvent(event);
   }
 
+  @Override
   public HandlerRegistration addValueChangeHandler(
       ValueChangeHandler<T> handler) {
     return manager.addHandler(ValueChangeEvent.getType(), handler);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
index 4a56558..91fedef 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
@@ -139,12 +139,23 @@
     }
   }
 
-  /** Invoked when the user double clicks on a table cell. */
+  /**
+   * Invoked when the user double clicks on a table cell.
+   *
+   * @param row row number.
+   * @param column column number.
+   */
   protected void onCellDoubleClick(int row, int column) {
     onOpenRow(row);
   }
 
-  /** Invoked when the user clicks on a table cell. */
+  /**
+   * Invoked when the user clicks on a table cell.
+   *
+   * @param event click event.
+   * @param row row number.
+   * @param column column number.
+   */
   protected void onCellSingleClick(Event event, int row, int column) {
     movePointerTo(row);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ParentProjectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ParentProjectBox.java
index 4bf8fef..e254f6e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ParentProjectBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ParentProjectBox.java
@@ -21,26 +21,22 @@
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.SuggestBox;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
 
 import java.util.HashSet;
 import java.util.Set;
 
 public class ParentProjectBox extends Composite {
-  private final NpTextBox textBox;
-  private final SuggestBox suggestBox;
+  private final RemoteSuggestBox suggestBox;
   private final ParentProjectNameSuggestOracle suggestOracle;
 
   public ParentProjectBox() {
-    textBox = new NpTextBox();
     suggestOracle = new ParentProjectNameSuggestOracle();
-    suggestBox = new SuggestBox(suggestOracle, textBox);
+    suggestBox = new RemoteSuggestBox(suggestOracle);
     initWidget(suggestBox);
   }
 
   public void setVisibleLength(int len) {
-    textBox.setVisibleLength(len);
+    suggestBox.setVisibleLength(len);
   }
 
   public void setProject(final Project.NameKey project) {
@@ -82,6 +78,7 @@
     @Override
     public void _onRequestSuggestions(Request req, final Callback callback) {
       super._onRequestSuggestions(req, new Callback() {
+        @Override
         public void onSuggestionsReady(Request request, Response response) {
           if (exclude.size() > 0) {
             Set<Suggestion> filteredSuggestions =
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PatchLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PatchLink.java
index f79dc51..09244c9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PatchLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PatchLink.java
@@ -15,90 +15,23 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.changes.PatchTable;
-import com.google.gerrit.client.patches.PatchScreen;
-import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 
 public class PatchLink extends InlineHyperlink {
-  protected PatchSet.Id base;
-  protected Patch.Key patchKey;
-  protected int patchIndex;
-  protected PatchSetDetail patchSetDetail;
-  protected PatchTable parentPatchTable;
-  protected PatchScreen.TopView topView;
-
-  /**
-   * @param text The text of this link
-   * @param base optional base to compare against.
-   * @param patchKey The key for this patch
-   * @param patchIndex The index of the current patch in the patch set
-   * @param historyToken The history token
-   * @param patchSetDetail Detailed information about the patch set.
-   * @param parentPatchTable The table used to display this link
-   */
-  protected PatchLink(String text, PatchSet.Id base, Patch.Key patchKey,
-      int patchIndex, String historyToken,
-      PatchSetDetail patchSetDetail, PatchTable parentPatchTable,
-      PatchScreen.TopView topView) {
+  private PatchLink(String text, String historyToken) {
     super(text, historyToken);
-    this.base = base;
-    this.patchKey = patchKey;
-    this.patchIndex = patchIndex;
-    this.patchSetDetail = patchSetDetail;
-    this.parentPatchTable = parentPatchTable;
-    this.parentPatchTable = parentPatchTable;
-    this.topView = topView;
-  }
-
-  /**
-   * @param text The text of this link
-   * @param type The type of the link to create (unified/side-by-side)
-   * @param patchScreen The patchScreen to grab contents to link to from
-   */
-  public PatchLink(String text, PatchScreen.Type type, PatchScreen patchScreen) {
-    this(text, //
-        patchScreen.getSideA(), //
-        patchScreen.getPatchKey(), //
-        patchScreen.getPatchIndex(), //
-        Dispatcher.toPatch(type, patchScreen.getPatchKey()), //
-        patchScreen.getPatchSetDetail(), //
-        patchScreen.getFileList(), //
-        patchScreen.getTopView() //
-        );
-  }
-
-  @Override
-  public void go() {
-    Dispatcher.patch( //
-        getTargetHistoryToken(), //
-        base, //
-        patchKey, //
-        patchIndex, //
-        patchSetDetail, //
-        parentPatchTable,
-        topView //
-        );
   }
 
   public static class SideBySide extends PatchLink {
-    public SideBySide(String text, PatchSet.Id base, Patch.Key patchKey,
-        int patchIndex, PatchSetDetail patchSetDetail,
-        PatchTable parentPatchTable) {
-      super(text, base, patchKey, patchIndex,
-          Dispatcher.toPatchSideBySide(base, patchKey),
-          patchSetDetail, parentPatchTable, null);
+    public SideBySide(String text, PatchSet.Id base, Patch.Key id) {
+      super(text, Dispatcher.toSideBySide(base, id));
     }
   }
 
   public static class Unified extends PatchLink {
-    public Unified(String text, PatchSet.Id base, final Patch.Key patchKey,
-        int patchIndex, PatchSetDetail patchSetDetail,
-        PatchTable parentPatchTable) {
-      super(text, base, patchKey, patchIndex,
-          Dispatcher.toPatchUnified(base, patchKey),
-          patchSetDetail, parentPatchTable, null);
+    public Unified(String text, PatchSet.Id base, Patch.Key id) {
+      super(text, Dispatcher.toUnified(base, id));
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLinkMenuItem.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLinkMenuItem.java
index 2917505..119f5ef 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLinkMenuItem.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLinkMenuItem.java
@@ -19,7 +19,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 
 public class ProjectLinkMenuItem extends LinkMenuItem {
-  private final String panel;
+  protected final String panel;
 
   public ProjectLinkMenuItem(String text, String panel) {
     super(text, "");
@@ -38,10 +38,14 @@
 
     if (projectKey != null) {
       setVisible(true);
-      setTargetHistoryToken(Dispatcher.toProjectAdmin(projectKey, panel));
+      onScreenLoad(projectKey);
     } else {
       setVisible(false);
     }
     super.onScreenLoad(event);
   }
+
+  protected void onScreenLoad(Project.NameKey project) {
+    setTargetHistoryToken(Dispatcher.toProjectAdmin(project, panel));
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
index cac7667..dc27790 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
@@ -35,13 +35,14 @@
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwtexpui.user.client.PluginSafeDialogBox;
 
-/** It creates a popup containing all the projects. */
-public class ProjectListPopup implements FilteredUserInterface {
+/** A popup containing all projects. */
+public class ProjectListPopup {
   private HighlightingProjectsTable projectsTab;
   private PluginSafeDialogBox popup;
   private NpTextBox filterTxt;
   private HorizontalPanel filterPanel;
-  private String subname;
+  private String match;
+  private Query query;
   private Button close;
   private ScrollPanel sp;
   private PopupPanel.PositionCallback popupPosition;
@@ -88,9 +89,19 @@
     };
   }
 
+  /**
+   * Invoked after moving pointer to a project.
+   *
+   * @param projectName project name.
+   */
   protected void onMovePointerTo(String projectName) {
   }
 
+  /**
+   * Invoked after opening a project row.
+   *
+   * @param projectName project name.
+   */
   protected void openRow(String projectName) {
   }
 
@@ -107,12 +118,16 @@
     filterLabel.setStyleName(Gerrit.RESOURCES.css().projectFilterLabel());
     filterPanel.add(filterLabel);
     filterTxt = new NpTextBox();
-    filterTxt.setValue(subname);
     filterTxt.addKeyUpHandler(new KeyUpHandler() {
       @Override
       public void onKeyUp(KeyUpEvent event) {
-        subname = filterTxt.getValue();
-        populateProjects();
+        Query q = new Query(filterTxt.getValue());
+        if (!match.equals(q.qMatch)) {
+          if (query == null) {
+            q.run();
+          }
+          query = q;
+        }
       }
     });
     filterPanel.add(filterTxt);
@@ -148,7 +163,8 @@
   public void displayPopup() {
     poppingUp = true;
     if (firstPopupLoad) { // For sizing/positioning, delay display until loaded
-      populateProjects();
+      match = "";
+      query = new Query(match).run();
     } else {
       popup.setPopupPositionAndShow(popupPosition);
       GlobalKey.dialog(popup);
@@ -173,23 +189,39 @@
     this.preferredLeft = left;
   }
 
-  protected void populateProjects() {
-    ProjectMap.match(subname,
-        new IgnoreOutdatedFilterResultsCallbackWrapper<ProjectMap>(this,
-            new GerritCallback<ProjectMap>() {
-              @Override
-              public void onSuccess(final ProjectMap result) {
-                projectsTab.display(result, subname);
-                if (firstPopupLoad) { // Display was delayed until table was loaded
-                  firstPopupLoad = false;
-                  displayPopup();
-                }
-              }
-            }));
-  }
+  private class Query {
+    private final String qMatch;
 
-  @Override
-  public String getCurrentFilter() {
-    return subname;
+    Query(String match) {
+      this.qMatch = match;
+    }
+
+    Query run() {
+      ProjectMap.match(qMatch, new GerritCallback<ProjectMap>() {
+          @Override
+          public void onSuccess(ProjectMap result) {
+            if (!firstPopupLoad && !popup.isShowing()) {
+              query = null;
+            } else if (query == Query.this) {
+              query = null;
+              showMap(result);
+            } else {
+              query.run();
+            }
+          }
+        });
+      return this;
+    }
+
+    private void showMap(ProjectMap result) {
+      ProjectListPopup.this.match = qMatch;
+      projectsTab.display(result, qMatch);
+
+      if (firstPopupLoad) {
+        // Display was delayed until table was loaded
+        firstPopupLoad = false;
+        displayPopup();
+      }
+    }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
index 80364cf..49120f6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.client.ui;
 
-import com.google.gerrit.client.RpcStatus;
 import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
@@ -23,17 +22,12 @@
 public class ProjectNameSuggestOracle extends SuggestAfterTypingNCharsOracle {
   @Override
   public void _onRequestSuggestions(final Request req, final Callback callback) {
-    RpcStatus.hide(new Runnable() {
-      @Override
-      public void run() {
-        ProjectMap.suggest(req.getQuery(), req.getLimit(),
-            new GerritCallback<ProjectMap>() {
-              @Override
-              public void onSuccess(ProjectMap map) {
-                callback.onSuggestionsReady(req, new Response(Natives.asList(map.values())));
-              }
-            });
-      }
-    });
+    ProjectMap.suggest(req.getQuery(), req.getLimit(),
+        new GerritCallback<ProjectMap>() {
+          @Override
+          public void onSuccess(ProjectMap map) {
+            callback.onSuggestionsReady(req, new Response(Natives.asList(map.values())));
+          }
+        });
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RPCSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RPCSuggestOracle.java
deleted file mode 100644
index 1068c3d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RPCSuggestOracle.java
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gwt.user.client.ui.SuggestOracle;
-
-/** This class will proxy SuggestOracle requests to another SuggestOracle
- *  while keeping track of the latest request.  Any repsonse that belongs
- *  to a request which is not the latest request will be dropped to prevent
- *  invalid deliveries.
- */
-
-public class RPCSuggestOracle extends SuggestOracle {
-
-  private SuggestOracle oracle;
-  private SuggestOracle.Request request;
-  private SuggestOracle.Callback callback;
-  private SuggestOracle.Callback myCallback = new SuggestOracle.Callback() {
-      public void onSuggestionsReady(SuggestOracle.Request req,
-            SuggestOracle.Response response) {
-          if (request == req) {
-            callback.onSuggestionsReady(req, response);
-            request = null;
-            callback = null;
-          }
-        }
-      };
-
-
-  public RPCSuggestOracle(SuggestOracle ora) {
-    oracle = ora;
-  }
-
-  public void requestSuggestions(SuggestOracle.Request req,
-      SuggestOracle.Callback cb) {
-    request = req;
-    callback = cb;
-    oracle.requestSuggestions(req, myCallback);
-  }
-
-  public boolean isDisplayStringHTML() {
-    return oracle.isDisplayStringHTML();
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java
new file mode 100644
index 0000000..5f47d98
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java
@@ -0,0 +1,147 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeList;
+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.reviewdb.client.Change;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.SuggestBox;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public abstract class RebaseDialog extends CommentedActionDialog {
+  private final SuggestBox base;
+  private final CheckBox cb;
+  private List<ChangeInfo> changes;
+  private final boolean sendEnabled;
+
+  public RebaseDialog(final String project, final String branch,
+      final Change.Id changeId, final boolean sendEnabled) {
+    super(Util.C.rebaseTitle(), null);
+    this.sendEnabled = sendEnabled;
+    sendButton.setText(Util.C.buttonRebaseChangeSend());
+
+    // create the suggestion box
+    base = new SuggestBox(new HighlightSuggestOracle() {
+      @Override
+      protected void onRequestSuggestions(Request request, Callback done) {
+        String query = request.getQuery().toLowerCase();
+        LinkedList<ChangeSuggestion> suggestions = new LinkedList<>();
+        for (final ChangeInfo ci : changes) {
+          if (changeId.equals(ci.legacy_id())) {
+            continue;  // do not suggest current change
+          }
+          String id = String.valueOf(ci.legacy_id().get());
+          if (id.contains(query) || ci.subject().toLowerCase().contains(query)) {
+            suggestions.add(new ChangeSuggestion(ci));
+            if (suggestions.size() >= 50) { // limit to 50 suggestions
+              break;
+            }
+          }
+        }
+        done.onSuggestionsReady(request, new Response(suggestions));
+      }
+    });
+    base.getElement().setAttribute("placeholder",
+        Util.C.rebasePlaceholderMessage());
+    base.setStyleName(Gerrit.RESOURCES.css().rebaseSuggestBox());
+
+    // the checkbox which must be clicked before the change list is populated
+    cb = new CheckBox(Util.C.rebaseConfirmMessage());
+    cb.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        boolean checked = ((CheckBox) event.getSource()).getValue();
+        if (checked) {
+          ChangeList.next("project:" + project + " AND branch:" + branch
+              + " AND is:open NOT age:90d", 0, 1000,
+              new GerritCallback<ChangeList>() {
+                @Override
+                public void onSuccess(ChangeList result) {
+                  changes = Natives.asList(result);
+                  updateControls(true);
+                }
+              });
+        } else {
+          updateControls(false);
+        }
+      }
+    });
+
+    // add the checkbox and suggestbox widgets to the content panel
+    contentPanel.add(cb);
+    contentPanel.add(base);
+    contentPanel.setStyleName(Gerrit.RESOURCES.css().rebaseContentPanel());
+  }
+
+  @Override
+  public void center() {
+    super.center();
+    GlobalKey.dialog(this);
+    updateControls(false);
+  }
+
+  private void updateControls(boolean changeParentEnabled) {
+    if (changeParentEnabled) {
+      sendButton.setTitle(null);
+      sendButton.setEnabled(true);
+      base.setEnabled(true);
+      base.setFocus(true);
+    } else {
+      base.setEnabled(false);
+      sendButton.setEnabled(sendEnabled);
+      if (sendEnabled) {
+        sendButton.setTitle(null);
+        sendButton.setFocus(true);
+      } else {
+        sendButton.setTitle(Util.C.rebaseNotPossibleMessage());
+        cancelButton.setFocus(true);
+      }
+    }
+  }
+
+  public String getBase() {
+    return cb.getValue() ? base.getText() : null;
+  }
+
+  private static class ChangeSuggestion implements Suggestion {
+    private ChangeInfo change;
+
+    public ChangeSuggestion(ChangeInfo change) {
+      this.change = change;
+    }
+
+    @Override
+    public String getDisplayString() {
+      return String.valueOf(change.legacy_id().get()) + ": " + change.subject();
+    }
+
+    @Override
+    public String getReplacementString() {
+      return String.valueOf(change.legacy_id().get());
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestBox.java
new file mode 100644
index 0000000..50f991c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestBox.java
@@ -0,0 +1,137 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.client.ui;
+
+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.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.event.logical.shared.HasCloseHandlers;
+import com.google.gwt.event.logical.shared.HasSelectionHandlers;
+import com.google.gwt.event.logical.shared.SelectionEvent;
+import com.google.gwt.event.logical.shared.SelectionHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Focusable;
+import com.google.gwt.user.client.ui.HasText;
+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;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+import com.google.gwt.user.client.ui.TextBoxBase;
+
+public class RemoteSuggestBox extends Composite implements Focusable, HasText,
+    HasSelectionHandlers<String>, HasCloseHandlers<RemoteSuggestBox> {
+  private final RemoteSuggestOracle remoteSuggestOracle;
+  private final DefaultSuggestionDisplay display;
+  private final HintTextBox textBox;
+  private final SuggestBox suggestBox;
+  private boolean submitOnSelection;
+
+  public RemoteSuggestBox(SuggestOracle oracle) {
+    remoteSuggestOracle = new RemoteSuggestOracle(oracle);
+    display = new DefaultSuggestionDisplay();
+
+    textBox = new HintTextBox();
+    textBox.addKeyDownHandler(new KeyDownHandler() {
+      @Override
+      public void onKeyDown(KeyDownEvent e) {
+        submitOnSelection = false;
+
+        if (e.getNativeEvent().getKeyCode() == KeyCodes.KEY_ESCAPE) {
+          CloseEvent.fire(RemoteSuggestBox.this, RemoteSuggestBox.this);
+        } else if (e.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
+          if (display.isSuggestionListShowing()) {
+            if (textBox.getValue().equals(remoteSuggestOracle.getLast())) {
+              submitOnSelection = true;
+            } else {
+              display.hideSuggestions();
+            }
+          } else {
+            SelectionEvent.fire(RemoteSuggestBox.this, getText());
+          }
+        }
+      }
+    });
+
+    suggestBox = new SuggestBox(remoteSuggestOracle, textBox, display);
+    suggestBox.addSelectionHandler(new SelectionHandler<Suggestion>() {
+      @Override
+      public void onSelection(SelectionEvent<Suggestion> event) {
+        textBox.setFocus(true);
+        if (submitOnSelection) {
+          SelectionEvent.fire(RemoteSuggestBox.this, getText());
+        }
+      }
+    });
+    initWidget(suggestBox);
+  }
+
+  public void setHintText(String hint) {
+    textBox.setHintText(hint);
+  }
+
+  public void setVisibleLength(int len) {
+    textBox.setVisibleLength(len);
+  }
+
+  public void setEnabled(boolean enabled) {
+    suggestBox.setEnabled(enabled);
+  }
+
+  public TextBoxBase getTextBox() {
+    return textBox;
+  }
+
+  @Override
+  public String getText() {
+    return suggestBox.getText();
+  }
+
+  @Override
+  public void setText(String value) {
+    suggestBox.setText(value);
+  }
+
+  @Override
+  public void setFocus(boolean focus) {
+    suggestBox.setFocus(focus);
+  }
+
+  @Override
+  public int getTabIndex() {
+    return suggestBox.getTabIndex();
+  }
+
+  @Override
+  public void setAccessKey(char key) {
+    suggestBox.setAccessKey(key);
+  }
+
+  @Override
+  public void setTabIndex(int index) {
+    suggestBox.setTabIndex(index);
+  }
+
+  @Override
+  public HandlerRegistration addSelectionHandler(SelectionHandler<String> h) {
+    return addHandler(h, SelectionEvent.getType());
+  }
+
+  @Override
+  public HandlerRegistration addCloseHandler(CloseHandler<RemoteSuggestBox> h) {
+    return addHandler(h, CloseEvent.getType());
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java
new file mode 100644
index 0000000..9554ac5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gwt.user.client.ui.SuggestOracle;
+
+/**
+ * Delegates to a slow SuggestOracle, such as a remote server API.
+ * <p>
+ * A response is only supplied to the UI if no requests were made after the
+ * oracle begin that request.
+ * <p>
+ * When a request is made while the delegate is still processing a prior request
+ * all intermediate requests are discarded and the most recent request is
+ * queued. The pending request's response is discarded and the most recent
+ * request is started.
+ */
+public class RemoteSuggestOracle extends SuggestOracle {
+  private final SuggestOracle oracle;
+  private Query query;
+  private String last;
+
+  public RemoteSuggestOracle(SuggestOracle src) {
+    oracle = src;
+  }
+
+  public String getLast() {
+    return last;
+  }
+
+  @Override
+  public void requestSuggestions(Request req, Callback cb) {
+    Query q = new Query(req, cb);
+    if (query == null) {
+      q.start();
+    }
+    query = q;
+  }
+
+  @Override
+  public boolean isDisplayStringHTML() {
+    return oracle.isDisplayStringHTML();
+  }
+
+  private class Query implements Callback {
+    final Request request;
+    final Callback callback;
+
+    Query(Request req, Callback cb) {
+      request = req;
+      callback = cb;
+    }
+
+    void start() {
+      oracle.requestSuggestions(request, this);
+    }
+
+    @Override
+    public void onSuggestionsReady(Request req, Response res) {
+      if (query == this) {
+        // No new request was started while this query was running.
+        // Propose this request's response as the suggestions.
+        query = null;
+        last = request.getQuery();
+        callback.onSuggestionsReady(req, res);
+      } else {
+        // Another query came in while this one was running. Skip
+        // this response and start the most recent query.
+        query.start();
+      }
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ReviewerSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ReviewerSuggestOracle.java
deleted file mode 100644
index 12506da..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ReviewerSuggestOracle.java
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.RpcStatus;
-import com.google.gerrit.client.admin.Util;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.common.data.AccountInfo;
-import com.google.gerrit.common.data.ReviewerInfo;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwt.user.client.ui.SuggestOracle;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/** Suggestion Oracle for reviewers. */
-public class ReviewerSuggestOracle extends SuggestAfterTypingNCharsOracle {
-
-  private Change.Id changeId;
-
-  @Override
-  protected void _onRequestSuggestions(final Request req, final Callback callback) {
-    RpcStatus.hide(new Runnable() {
-      public void run() {
-        SuggestUtil.SVC.suggestChangeReviewer(changeId, req.getQuery(),
-            req.getLimit(), new GerritCallback<List<ReviewerInfo>>() {
-              public void onSuccess(final List<ReviewerInfo> result) {
-                final List<ReviewerSuggestion> r =
-                    new ArrayList<>(result.size());
-                for (final ReviewerInfo reviewer : result) {
-                  r.add(new ReviewerSuggestion(reviewer));
-                }
-                callback.onSuggestionsReady(req, new Response(r));
-              }
-            });
-      }
-    });
-  }
-
-  public void setChange(Change.Id changeId) {
-    this.changeId = changeId;
-  }
-
-  private static class ReviewerSuggestion implements SuggestOracle.Suggestion {
-    private final ReviewerInfo reviewerInfo;
-
-    ReviewerSuggestion(final ReviewerInfo reviewerInfo) {
-      this.reviewerInfo = reviewerInfo;
-    }
-
-    public String getDisplayString() {
-      final AccountInfo accountInfo = reviewerInfo.getAccountInfo();
-      if (accountInfo != null) {
-        return FormatUtil.nameEmail(FormatUtil.asInfo(accountInfo));
-      }
-      return reviewerInfo.getGroup().getName() + " ("
-          + Util.C.suggestedGroupLabel() + ")";
-    }
-
-    public String getReplacementString() {
-      final AccountInfo accountInfo = reviewerInfo.getAccountInfo();
-      if (accountInfo != null) {
-        return FormatUtil.nameEmail(FormatUtil.asInfo(accountInfo));
-      }
-      return reviewerInfo.getGroup().getName();
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java
index 4f54ba5..26c0ce6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java
@@ -17,6 +17,9 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
 
+import java.util.Collections;
+import java.util.List;
+
 /**
  * Suggest oracle that only provides suggestions if the user has typed at least
  * as many characters as configured by 'suggest.from'. If 'suggest.from' is set
@@ -25,10 +28,12 @@
 public abstract class SuggestAfterTypingNCharsOracle extends HighlightSuggestOracle {
 
   @Override
-  protected void onRequestSuggestions(final Request request, final Callback done) {
-    final int suggestFrom = Gerrit.getConfig().getSuggestFrom();
-    if (suggestFrom == 0 || request.getQuery().length() >= suggestFrom) {
-      _onRequestSuggestions(request, done);
+  protected void onRequestSuggestions(Request req, Callback cb) {
+    if (req.getQuery().length() >= Gerrit.getConfig().getSuggestFrom()) {
+      _onRequestSuggestions(req, cb);
+    } else {
+      List<Suggestion> none = Collections.emptyList();
+      cb.onSuggestionsReady(req, new Response(none));
     }
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextAreaActionDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextAreaActionDialog.java
new file mode 100644
index 0000000..d7d5d6a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextAreaActionDialog.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+
+public abstract class TextAreaActionDialog extends CommentedActionDialog
+    implements CloseHandler<PopupPanel> {
+  protected final NpTextArea message;
+
+  public TextAreaActionDialog(String title, String heading) {
+    super(title, heading);
+
+    message = new NpTextArea();
+    message.setCharacterWidth(60);
+    message.setVisibleLines(10);
+    message.getElement().setPropertyBoolean("spellcheck", true);
+    setFocusOn(message);
+
+    contentPanel.add(message);
+  }
+
+  public String getMessageText() {
+    return message.getText().trim();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java
index bcfb394..0f5e12a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java
@@ -25,4 +25,8 @@
   String projectItemHelp();
   String projectStateAbbrev();
   String projectStateHelp();
+
+  String dialogCreateChangeTitle();
+  String dialogCreateChangeHeading();
+  String newChangeBranchSuggestion();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties
index 1e0e185..a0845d9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties
@@ -5,4 +5,8 @@
 projectDescription = Project Description
 projectItemHelp = project
 projectStateAbbrev = S
-projectStateHelp = State
\ No newline at end of file
+projectStateHelp = State
+
+dialogCreateChangeTitle = Create Change
+dialogCreateChangeHeading = Description
+newChangeBranchSuggestion = Select branch for new change
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/CodeMirror.gwt.xml b/gerrit-gwtui/src/main/java/net/codemirror/CodeMirror.gwt.xml
index 24a0f57..add033f 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/CodeMirror.gwt.xml
+++ b/gerrit-gwtui/src/main/java/net/codemirror/CodeMirror.gwt.xml
@@ -20,4 +20,5 @@
   <source path='lib'/>
   <source path='keymap'/>
   <source path='mode'/>
+  <source path='theme'/>
 </module>
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
index 85b1fa6..fc21532 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
@@ -14,10 +14,14 @@
 
 package net.codemirror.lib;
 
+import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.diff.DisplaySide;
+import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
 import net.codemirror.lib.TextMarker.FromTo;
@@ -28,316 +32,343 @@
  * @see <a href="http://codemirror.net/doc/manual.html#api">CodeMirror API</a>
  */
 public class CodeMirror extends JavaScriptObject {
+  public static void preload() {
+    initLibrary(CallbackGroup.<Void> emptyCallback());
+  }
+
   public static void initLibrary(AsyncCallback<Void> cb) {
     Loader.initLibrary(cb);
   }
 
-  public static native CodeMirror create(
-      DisplaySide side,
-      Element parent,
-      Configuration cfg) /*-{
-    var m = $wnd.CodeMirror(parent, cfg);
-    m._sbs2_side = side;
-    return m;
+  interface Style extends CssResource {
+    String activeLine();
+    String showTabs();
+    String margin();
+  }
+
+  static Style style() {
+    return Lib.I.style();
+  }
+
+  public static CodeMirror create(Element p, Configuration cfg) {
+    CodeMirror cm = newCM(p, cfg);
+    Extras.attach(cm);
+    return cm;
+  }
+
+  private static native CodeMirror newCM(Element p, Configuration cfg) /*-{
+    return $wnd.CodeMirror(p, cfg);
   }-*/;
 
   public final native void setOption(String option, boolean value) /*-{
-    this.setOption(option, value);
+    this.setOption(option, value)
   }-*/;
 
   public final native void setOption(String option, double value) /*-{
-    this.setOption(option, value);
+    this.setOption(option, value)
   }-*/;
 
   public final native void setOption(String option, String value) /*-{
-    this.setOption(option, value);
+    this.setOption(option, value)
   }-*/;
 
   public final native void setOption(String option, JavaScriptObject val) /*-{
-    this.setOption(option, val);
+    this.setOption(option, val)
   }-*/;
 
   public final native String getStringOption(String o) /*-{ return this.getOption(o) }-*/;
-  public final native void setValue(String v) /*-{ this.setValue(v); }-*/;
 
-  public final native void setWidth(double w) /*-{ this.setSize(w, null); }-*/;
-  public final native void setWidth(String w) /*-{ this.setSize(w, null); }-*/;
-  public final native void setHeight(double h) /*-{ this.setSize(null, h); }-*/;
-  public final native void setHeight(String h) /*-{ this.setSize(null, h); }-*/;
+  public final native String getValue() /*-{ return this.getValue() }-*/;
+  public final native void setValue(String v) /*-{ this.setValue(v) }-*/;
+
+  public final native int changeGeneration(boolean closeEvent)
+  /*-{ return this.changeGeneration(closeEvent) }-*/;
+  public final native boolean isClean(int generation)
+  /*-{ return this.isClean(generation) }-*/;
+
+  public final native void setWidth(double w) /*-{ this.setSize(w, null) }-*/;
+  public final native void setHeight(double h) /*-{ this.setSize(null, h) }-*/;
+
+  public final int getHeight() {
+    return getWrapperElement().getClientHeight();
+  }
+
+  public final void adjustHeight(int localHeader) {
+    int rest = Gerrit.getHeaderFooterHeight()
+        + localHeader
+        + 5; // Estimate
+    setHeight(Window.getClientHeight() - rest);
+  }
+
   public final native String getLine(int n) /*-{ return this.getLine(n) }-*/;
+  public final native double barHeight() /*-{ return this.display.barHeight }-*/;
+  public final native double barWidth() /*-{ return this.display.barWidth }-*/;
+  public final native int lastLine() /*-{ return this.lastLine() }-*/;
+  public final native void refresh() /*-{ this.refresh() }-*/;
 
-  public final native void refresh() /*-{ this.refresh(); }-*/;
-  public final native Element getWrapperElement() /*-{ return this.getWrapperElement(); }-*/;
-
-  public final native TextMarker markText(LineCharacter from, LineCharacter to,
+  public final native TextMarker markText(Pos from, Pos to,
       Configuration options) /*-{
-    return this.markText(from, to, options);
+    return this.markText(from, to, options)
   }-*/;
 
   public enum LineClassWhere {
-    TEXT, BACKGROUND, WRAP
+    TEXT { @Override String value() { return "text"; } },
+    BACKGROUND { @Override String value() { return "background"; } },
+    WRAP { @Override String value() { return "wrap"; } };
+    abstract String value();
   }
 
   public final void addLineClass(int line, LineClassWhere where,
       String className) {
-    addLineClassNative(line, where.name().toLowerCase(), className);
+    addLineClassNative(line, where.value(), className);
   }
 
   private final native void addLineClassNative(int line, String where,
       String lineClass) /*-{
-    this.addLineClass(line, where, lineClass);
+    this.addLineClass(line, where, lineClass)
   }-*/;
 
   public final void addLineClass(LineHandle line, LineClassWhere where,
       String className) {
-    addLineClassNative(line, where.name().toLowerCase(), className);
+    addLineClassNative(line, where.value(), className);
   }
 
   private final native void addLineClassNative(LineHandle line, String where,
       String lineClass) /*-{
-    this.addLineClass(line, where, lineClass);
+    this.addLineClass(line, where, lineClass)
   }-*/;
 
   public final void removeLineClass(int line, LineClassWhere where,
       String className) {
-    removeLineClassNative(line, where.name().toLowerCase(), className);
+    removeLineClassNative(line, where.value(), className);
   }
 
   private final native void removeLineClassNative(int line, String where,
       String lineClass) /*-{
-    this.removeLineClass(line, where, lineClass);
+    this.removeLineClass(line, where, lineClass)
   }-*/;
 
   public final void removeLineClass(LineHandle line, LineClassWhere where,
       String className) {
-    removeLineClassNative(line, where.name().toLowerCase(), className);
+    removeLineClassNative(line, where.value(), className);
   }
 
   private final native void removeLineClassNative(LineHandle line, String where,
       String lineClass) /*-{
-    this.removeLineClass(line, where, lineClass);
+    this.removeLineClass(line, where, lineClass)
   }-*/;
 
-  public final native void addWidget(LineCharacter pos, Element node,
-      boolean scrollIntoView) /*-{
-    this.addWidget(pos, node, scrollIntoView);
+  public final native void addWidget(Pos pos, Element node) /*-{
+    this.addWidget(pos, node, false)
   }-*/;
 
   public final native LineWidget addLineWidget(int line, Element node,
       Configuration options) /*-{
-    return this.addLineWidget(line, node, options);
+    return this.addLineWidget(line, node, options)
   }-*/;
 
   public final native int lineAtHeight(double height) /*-{
-    return this.lineAtHeight(height);
+    return this.lineAtHeight(height)
   }-*/;
 
   public final native int lineAtHeight(double height, String mode) /*-{
-    return this.lineAtHeight(height, mode);
+    return this.lineAtHeight(height, mode)
   }-*/;
 
   public final native double heightAtLine(int line) /*-{
-    return this.heightAtLine(line);
+    return this.heightAtLine(line)
   }-*/;
 
   public final native double heightAtLine(int line, String mode) /*-{
-    return this.heightAtLine(line, mode);
+    return this.heightAtLine(line, mode)
   }-*/;
 
-  public final native Rect charCoords(LineCharacter pos, String mode) /*-{
-    return this.charCoords(pos, mode);
+  public final native Rect charCoords(Pos pos, String mode) /*-{
+    return this.charCoords(pos, mode)
   }-*/;
 
   public final native CodeMirrorDoc getDoc() /*-{
-    return this.getDoc();
+    return this.getDoc()
   }-*/;
 
   public final native void scrollTo(double x, double y) /*-{
-    this.scrollTo(x, y);
+    this.scrollTo(x, y)
   }-*/;
 
   public final native void scrollToY(double y) /*-{
-    this.scrollTo(null, y);
+    this.scrollTo(null, y)
   }-*/;
 
+  public final void scrollToLine(int line) {
+    int height = getHeight();
+    if (lineAtHeight(height - 20) < line) {
+      scrollToY(heightAtLine(line, "local") - 0.5 * height);
+    }
+    setCursor(Pos.create(line));
+  }
+
   public final native ScrollInfo getScrollInfo() /*-{
-    return this.getScrollInfo();
+    return this.getScrollInfo()
   }-*/;
 
   public final native Viewport getViewport() /*-{
-    return this.getViewport();
+    return this.getViewport()
   }-*/;
 
   public final native void operation(Runnable thunk) /*-{
     this.operation(function() {
       thunk.@java.lang.Runnable::run()();
-    });
+    })
   }-*/;
 
-  public final native void on(String event, Runnable thunk) /*-{
-    this.on(event, $entry(function() {
-      thunk.@java.lang.Runnable::run()();
-    }));
+  public final native void off(String event, RegisteredHandler h) /*-{
+    this.off(event, h)
+  }-*/;
+
+  public final native RegisteredHandler on(String event, Runnable thunk) /*-{
+    var h = $entry(function() { thunk.@java.lang.Runnable::run()() });
+    this.on(event, h);
+    return h;
   }-*/;
 
   public final native void on(String event, EventHandler handler) /*-{
     this.on(event, $entry(function(cm, e) {
       handler.@net.codemirror.lib.CodeMirror.EventHandler::handle(
-        Lnet/codemirror/lib/CodeMirror;Lcom/google/gwt/dom/client/NativeEvent;)(cm, e);
-    }));
+        Lnet/codemirror/lib/CodeMirror;
+        Lcom/google/gwt/dom/client/NativeEvent;)(cm, e);
+    }))
   }-*/;
 
   public final native void on(String event, RenderLineHandler handler) /*-{
-    this.on(event, $entry(function(cm, h, ele) {
+    this.on(event, $entry(function(cm, h, e) {
       handler.@net.codemirror.lib.CodeMirror.RenderLineHandler::handle(
-        Lnet/codemirror/lib/CodeMirror;Lnet/codemirror/lib/CodeMirror$LineHandle;
-        Lcom/google/gwt/dom/client/Element;)(cm, h, ele);
-    }));
+        Lnet/codemirror/lib/CodeMirror;
+        Lnet/codemirror/lib/CodeMirror$LineHandle;
+        Lcom/google/gwt/dom/client/Element;)(cm, h, e);
+    }))
   }-*/;
 
   public final native void on(String event, GutterClickHandler handler) /*-{
     this.on(event, $entry(function(cm, l, g, e) {
       handler.@net.codemirror.lib.CodeMirror.GutterClickHandler::handle(
-        Lnet/codemirror/lib/CodeMirror;ILjava/lang/String;
+        Lnet/codemirror/lib/CodeMirror;
+        I
+        Ljava/lang/String;
         Lcom/google/gwt/dom/client/NativeEvent;)(cm, l, g, e);
-    }));
+    }))
   }-*/;
 
   public final native void on(String event, BeforeSelectionChangeHandler handler) /*-{
-    this.on(event, $entry(function(cm, e) {
+    this.on(event, $entry(function(cm, o) {
+      var e = o.ranges[o.ranges.length-1];
       handler.@net.codemirror.lib.CodeMirror.BeforeSelectionChangeHandler::handle(
-        Lnet/codemirror/lib/CodeMirror;Lnet/codemirror/lib/LineCharacter;
-        Lnet/codemirror/lib/LineCharacter;)(cm,e.anchor,e.head);
-    }));
+        Lnet/codemirror/lib/CodeMirror;
+        Lnet/codemirror/lib/Pos;
+        Lnet/codemirror/lib/Pos;)(cm, e.anchor, e.head);
+    }))
   }-*/;
 
-  public final native LineCharacter getCursor() /*-{
-    return this.getCursor();
+  public final native void on(ChangesHandler handler) /*-{
+    this.on('changes', $entry(function(cm, o) {
+      handler.@net.codemirror.lib.CodeMirror.ChangesHandler::handle(
+        Lnet/codemirror/lib/CodeMirror;)(cm);
+    }))
   }-*/;
 
-  public final native LineCharacter getCursor(String start) /*-{
-    return this.getCursor(start);
+  public final native void setCursor(Pos p) /*-{ this.setCursor(p) }-*/;
+  public final native Pos getCursor() /*-{ return this.getCursor() }-*/;
+  public final native Pos getCursor(String start) /*-{
+    return this.getCursor(start)
   }-*/;
 
   public final FromTo getSelectedRange() {
     return FromTo.create(getCursor("start"), getCursor("end"));
   }
 
-  public final native void setSelection(LineCharacter anchor) /*-{
-    this.setSelection(anchor);
-  }-*/;
-
-  public final native void setSelection(LineCharacter anchor, LineCharacter head) /*-{
-    this.setSelection(anchor, head);
-  }-*/;
-
-  public final native void setCursor(LineCharacter lineCh) /*-{
-    this.setCursor(lineCh);
+  public final native void setSelection(Pos p) /*-{ this.setSelection(p) }-*/;
+  public final native void setSelection(Pos anchor, Pos head) /*-{
+    this.setSelection(anchor, head)
   }-*/;
 
   public final native boolean somethingSelected() /*-{
-    return this.somethingSelected();
+    return this.somethingSelected()
   }-*/;
 
-  public final native boolean hasActiveLine() /*-{
-    return !!this.state.activeLine;
-  }-*/;
-
-  public final native LineHandle getActiveLine() /*-{
-    return this.state.activeLine;
-  }-*/;
-
-  public final native void setActiveLine(LineHandle line) /*-{
-    this.state.activeLine = line;
-  }-*/;
-
-  public final native void addKeyMap(KeyMap map) /*-{ this.addKeyMap(map); }-*/;
-  public final native void removeKeyMap(KeyMap map) /*-{ this.removeKeyMap(map); }-*/;
-
-  public static final native LineCharacter pos(int line, int ch) /*-{
-    return $wnd.CodeMirror.Pos(line, ch);
-  }-*/;
-
-  public static final native LineCharacter pos(int line) /*-{
-    return $wnd.CodeMirror.Pos(line);
-  }-*/;
+  public final native void addKeyMap(KeyMap map) /*-{ this.addKeyMap(map) }-*/;
+  public final native void removeKeyMap(KeyMap map) /*-{ this.removeKeyMap(map) }-*/;
 
   public final native LineHandle getLineHandle(int line) /*-{
-    return this.getLineHandle(line);
+    return this.getLineHandle(line)
   }-*/;
 
   public final native LineHandle getLineHandleVisualStart(int line) /*-{
-    return this.getLineHandleVisualStart(line);
+    return this.getLineHandleVisualStart(line)
   }-*/;
 
   public final native int getLineNumber(LineHandle handle) /*-{
-    return this.getLineNumber(handle);
+    return this.getLineNumber(handle)
   }-*/;
 
   public final native void focus() /*-{
-    this.focus();
+    this.focus()
+  }-*/;
+
+  public final native Element getWrapperElement() /*-{
+    return this.getWrapperElement()
   }-*/;
 
   public final native Element getGutterElement() /*-{
-    return this.getGutterElement();
+    return this.getGutterElement()
   }-*/;
 
-  public final native Element getSizer() /*-{
-    return this.display.sizer;
+  public final native Element sizer() /*-{
+    return this.display.sizer
   }-*/;
 
-  public final native Element getMoverElement() /*-{
-    return this.display.mover;
+  public final native Element mover() /*-{
+    return this.display.mover
   }-*/;
 
-  public final native Element getMeasureElement() /*-{
-    return this.display.measure;
+  public final native Element measure() /*-{
+    return this.display.measure
   }-*/;
 
-  public final native Element getScrollbarV() /*-{
-    return this.display.scrollbarV;
-  }-*/;
-
-  public static final native KeyMap cloneKeyMap(String name) /*-{
-    var i = $wnd.CodeMirror.keyMap[name];
-    var o = {};
-    for (n in i)
-      if (i.hasOwnProperty(n))
-        o[n] = i[n];
-    return o;
+  public final native Element scrollbarV() /*-{
+    return this.display.scrollbars.vert.node;
   }-*/;
 
   public final native void execCommand(String cmd) /*-{
-    this.execCommand(cmd);
+    this.execCommand(cmd)
+  }-*/;
+
+  public static final native KeyMap getKeyMap(String name) /*-{
+    return $wnd.CodeMirror.keyMap[name];
   }-*/;
 
   public static final native void addKeyMap(String name, KeyMap km) /*-{
-    $wnd.CodeMirror.keyMap[name] = km;
+    $wnd.CodeMirror.keyMap[name] = km
   }-*/;
 
-  public static final native void handleVimKey(CodeMirror cm, String key) /*-{
-    $wnd.CodeMirror.Vim.handleKey(cm, key);
+  public final native Vim vim() /*-{
+    return this;
   }-*/;
 
-  public static final native void mapVimKey(String alias, String actual) /*-{
-    $wnd.CodeMirror.Vim.map(alias, actual);
-  }-*/;
+  public final DisplaySide side() {
+    return extras().side();
+  }
 
-  public final native boolean hasVimSearchHighlight() /*-{
-    return this.state.vim && this.state.vim.searchState_ &&
-        !!this.state.vim.searchState_.getOverlay();
-  }-*/;
-
-  public final native DisplaySide side() /*-{ return this._sbs2_side }-*/;
+  public final Extras extras() {
+    return Extras.get(this);
+  }
 
   protected CodeMirror() {
   }
 
   public static class Viewport extends JavaScriptObject {
-    public final native int getFrom() /*-{ return this.from; }-*/;
-    public final native int getTo() /*-{ return this.to; }-*/;
+    public final native int from() /*-{ return this.from }-*/;
+    public final native int to() /*-{ return this.to }-*/;
     public final boolean contains(int line) {
-      return getFrom() <= line && line < getTo();
+      return from() <= line && line < to();
     }
 
     protected Viewport() {
@@ -349,6 +380,11 @@
     }
   }
 
+  public static class RegisteredHandler extends JavaScriptObject {
+    protected RegisteredHandler() {
+    }
+  }
+
   public interface EventHandler {
     public void handle(CodeMirror instance, NativeEvent event);
   }
@@ -363,6 +399,10 @@
   }
 
   public interface BeforeSelectionChangeHandler {
-    public void handle(CodeMirror instance, LineCharacter anchor, LineCharacter head);
+    public void handle(CodeMirror instance, Pos anchor, Pos head);
+  }
+
+  public interface ChangesHandler {
+    public void handle(CodeMirror instance);
   }
 }
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirrorDoc.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirrorDoc.java
index ff5d230..d04cc24 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirrorDoc.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirrorDoc.java
@@ -20,11 +20,11 @@
 public class CodeMirrorDoc extends JavaScriptObject {
 
   public final native void replaceRange(String replacement,
-      LineCharacter from, LineCharacter to) /*-{
+      Pos from, Pos to) /*-{
     this.replaceRange(replacement, from, to);
   }-*/;
 
-  public final native void insertText(String insertion, LineCharacter at) /*-{
+  public final native void insertText(String insertion, Pos at) /*-{
     this.replaceRange(insertion, at);
   }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Configuration.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Configuration.java
index 4ec3dea..7a0bbea 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/Configuration.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Configuration.java
@@ -20,7 +20,7 @@
  * Simple map-like structure to pass configuration to CodeMirror.
  *
  * @see <a href="http://codemirror.net/doc/manual.html#config">CodeMirror config</a>
- * @see CodeMirror#create(com.google.gerrit.client.diff.DisplaySide, com.google.gwt.dom.client.Element, Configuration)
+ * @see CodeMirror#create(com.google.gwt.dom.client.Element, Configuration)
  */
 public class Configuration extends JavaScriptObject {
   public static Configuration create() {
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Extras.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Extras.java
new file mode 100644
index 0000000..13186d1
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Extras.java
@@ -0,0 +1,143 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.lib;
+
+import static com.google.gwt.dom.client.Style.Display.INLINE_BLOCK;
+import static com.google.gwt.dom.client.Style.Unit.PX;
+import static net.codemirror.lib.CodeMirror.style;
+import static net.codemirror.lib.CodeMirror.LineClassWhere.WRAP;
+
+import com.google.gerrit.client.diff.DisplaySide;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.DOM;
+
+import net.codemirror.lib.CodeMirror.LineHandle;
+
+import java.util.Objects;
+
+/** Additional features added to CodeMirror by Gerrit Code Review. */
+public class Extras {
+  static final native Extras get(CodeMirror c) /*-{ return c.gerritExtras }-*/;
+  private static final native void set(CodeMirror c, Extras e)
+  /*-{ c.gerritExtras = e }-*/;
+
+  static void attach(CodeMirror c) {
+    set(c, new Extras(c));
+  }
+
+  private final CodeMirror cm;
+  private Element margin;
+  private DisplaySide side;
+  private double charWidthPx;
+  private double lineHeightPx;
+  private LineHandle activeLine;
+
+  private Extras(CodeMirror cm) {
+    this.cm = cm;
+  }
+
+  public DisplaySide side() {
+    return side;
+  }
+
+  public void side(DisplaySide s) {
+    side = s;
+  }
+
+  public double charWidthPx() {
+    if (charWidthPx <= 1) {
+      int len = 100;
+      StringBuilder s = new StringBuilder();
+      for (int i = 0; i < len; i++) {
+        s.append('m');
+      }
+
+      Element e = DOM.createSpan();
+      e.getStyle().setDisplay(INLINE_BLOCK);
+      e.setInnerText(s.toString());
+
+      cm.measure().appendChild(e);
+      charWidthPx = ((double) e.getOffsetWidth()) / len;
+      e.removeFromParent();
+    }
+    return charWidthPx;
+  }
+
+  public double lineHeightPx() {
+    if (lineHeightPx <= 1) {
+      Element p = DOM.createDiv();
+      int lines = 1;
+      for (int i = 0; i < lines; i++) {
+        Element e = DOM.createDiv();
+        p.appendChild(e);
+
+        Element pre = DOM.createElement("pre");
+        pre.setInnerText("gqyŚŻŹŃ");
+        e.appendChild(pre);
+      }
+
+      cm.measure().appendChild(p);
+      lineHeightPx = ((double) p.getOffsetHeight()) / lines;
+      p.removeFromParent();
+    }
+    return lineHeightPx;
+  }
+
+  public void lineLength(int columns) {
+    if (margin == null) {
+      margin = DOM.createDiv();
+      margin.setClassName(style().margin());
+      cm.mover().appendChild(margin);
+    }
+    margin.getStyle().setMarginLeft(columns * charWidthPx(), PX);
+  }
+
+  public void showTabs(boolean show) {
+    Element e = cm.getWrapperElement();
+    if (show) {
+      e.addClassName(style().showTabs());
+    } else {
+      e.removeClassName(style().showTabs());
+    }
+  }
+
+  public final boolean hasActiveLine() {
+    return activeLine != null;
+  }
+
+  public final LineHandle activeLine() {
+    return activeLine;
+  }
+
+  public final boolean activeLine(LineHandle line) {
+    if (Objects.equals(activeLine, line)) {
+      return false;
+    }
+
+    if (activeLine != null) {
+      cm.removeLineClass(activeLine, WRAP, style().activeLine());
+    }
+    activeLine = line;
+    cm.addLineClass(activeLine, WRAP, style().activeLine());
+    return true;
+  }
+
+  public final void clearActiveLine() {
+    if (activeLine != null) {
+      cm.removeLineClass(activeLine, WRAP, style().activeLine());
+      activeLine = null;
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/KeyMap.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/KeyMap.java
index fb9f770..946b44f 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/KeyMap.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/KeyMap.java
@@ -27,12 +27,17 @@
     return this;
   }-*/;
 
-  public final native KeyMap on(String key, boolean b) /*-{
-    this[key] = b;
+  /** Do not handle inside of CodeMirror; instead push up the DOM tree. */
+  public final native KeyMap propagate(String key) /*-{
+    this[key] = false;
     return this;
   }-*/;
 
-  public final native KeyMap remove(String key) /*-{ delete this[key]; }-*/;
+  /** Delegate undefined keys to another KeyMap implementation. */
+  public final native KeyMap fallthrough(KeyMap m) /*-{
+    this.fallthrough = m;
+    return this;
+  }-*/;
 
   protected KeyMap() {
   }
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Lib.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Lib.java
index bb60fe9..a3b1cf4 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/Lib.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Lib.java
@@ -23,10 +23,13 @@
 interface Lib extends ClientBundle {
   static final Lib I = GWT.create(Lib.class);
 
-  @Source("cm3.css")
+  @Source("cm.css")
   ExternalTextResource css();
 
-  @Source("cm3.js")
+  @Source("cm.js")
   @DoNotEmbed
   DataResource js();
+
+  @Source("style.css")
+  CodeMirror.Style style();
 }
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/LineCharacter.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/LineCharacter.java
deleted file mode 100644
index 5076aff..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/LineCharacter.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package net.codemirror.lib;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-/** {line, ch} objects used within CodeMirror. */
-public class LineCharacter extends JavaScriptObject {
-  public static LineCharacter create(int line, int ch) {
-    return createImpl(line, ch);
-  }
-
-  public static LineCharacter create(int line) {
-    return createImpl(line, 0);
-  }
-
-  private static LineCharacter createImpl(int line, int ch) {
-    LineCharacter lineCh = createObject().cast();
-    lineCh.setLine(line);
-    lineCh.setCh(ch);
-    return lineCh;
-  }
-
-  public final native void setLine(int line) /*-{ this.line = line; }-*/;
-  public final native void setCh(int ch) /*-{ this.ch = ch; }-*/;
-
-  public final native int getLine() /*-{ return this.line; }-*/;
-  public final native int getCh() /*-{ return this.ch; }-*/;
-
-  protected LineCharacter() {
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/LineWidget.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/LineWidget.java
index 91547fb..62c219b 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/LineWidget.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/LineWidget.java
@@ -18,17 +18,13 @@
 
 /** LineWidget objects used within CodeMirror. */
 public class LineWidget extends JavaScriptObject {
-  public static LineWidget create() {
-    return createObject().cast();
-  }
-
-  public final native void clear() /*-{ this.clear(); }-*/;
-  public final native void changed() /*-{ this.changed(); }-*/;
+  public final native void clear() /*-{ this.clear() }-*/;
+  public final native void changed() /*-{ this.changed() }-*/;
 
   public final native void onRedraw(Runnable thunk) /*-{
     this.on("redraw", $entry(function() {
       thunk.@java.lang.Runnable::run()();
-    }));
+    }))
   }-*/;
 
   public final native void onFirstRedraw(Runnable thunk) /*-{
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java
index 4d894c6..379cb3c 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java
@@ -14,7 +14,7 @@
 
 package net.codemirror.lib;
 
-import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gwt.core.client.Callback;
 import com.google.gwt.core.client.ScriptInjector;
 import com.google.gwt.dom.client.ScriptElement;
@@ -26,47 +26,54 @@
 import com.google.gwt.safehtml.shared.SafeUri;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-class Loader {
+public class Loader {
   private static native boolean isLibLoaded()
   /*-{ return $wnd.hasOwnProperty('CodeMirror'); }-*/;
 
   static void initLibrary(final AsyncCallback<Void> cb) {
     if (isLibLoaded()) {
       cb.onSuccess(null);
-    } else {
-      injectCss(Lib.I.css());
-      injectScript(Lib.I.js().getSafeUri(), new GerritCallback<Void>(){
-        @Override
-        public void onSuccess(Void result) {
-          initVimKeys();
-          cb.onSuccess(null);
-        }
-      });
+      return;
     }
+
+    CallbackGroup group = new CallbackGroup();
+    injectCss(Lib.I.css(), group.<Void> addEmpty());
+    injectScript(Lib.I.js().getSafeUri(), group.add(new AsyncCallback<Void>() {
+      @Override
+      public void onSuccess(Void result) {
+        Vim.initKeyMap();
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+      }
+    }));
+    group.addListener(cb);
+    group.done();
   }
 
-  private static void injectCss(ExternalTextResource css) {
+  private static void injectCss(ExternalTextResource css,
+      final AsyncCallback<Void> cb) {
     try {
       css.getText(new ResourceCallback<TextResource>() {
         @Override
         public void onSuccess(TextResource resource) {
           StyleInjector.inject(resource.getText());
+          Lib.I.style().ensureInjected();
+          cb.onSuccess(null);
         }
 
         @Override
         public void onError(ResourceException e) {
-          error(e);
+          cb.onFailure(e);
         }
       });
     } catch (ResourceException e) {
-      error(e);
+      cb.onFailure(e);
     }
   }
 
-  static void injectScript(SafeUri js, final AsyncCallback<Void> callback) {
+  public static void injectScript(SafeUri js, final AsyncCallback<Void> callback) {
     final ScriptElement[] script = new ScriptElement[1];
     script[0] = ScriptInjector.fromUrl(js.asString())
       .setWindow(ScriptInjector.TOP_WINDOW)
@@ -79,7 +86,6 @@
 
         @Override
         public void onFailure(Exception reason) {
-          error(reason);
           callback.onFailure(reason);
         }
        })
@@ -87,33 +93,6 @@
       .cast();
   }
 
-  private static void initVimKeys() {
-    // TODO: Better custom keybindings, remove temporary navigation hacks.
-    KeyMap km = CodeMirror.cloneKeyMap("vim");
-    for (String s : new String[] {
-        "A", "C", "I", "O", "R", "U",
-        "Ctrl-C", "Ctrl-O", "Ctrl-P", "Ctrl-S",
-        "Ctrl-F", "Ctrl-B", "Ctrl-R"}) {
-      km.remove(s);
-    }
-    for (int i = 0; i <= 9; i++) {
-      km.remove("Ctrl-" + i);
-    }
-    CodeMirror.addKeyMap("vim_ro", km);
-
-    CodeMirror.mapVimKey("j", "gj");
-    CodeMirror.mapVimKey("k", "gk");
-    CodeMirror.mapVimKey("Down", "gj");
-    CodeMirror.mapVimKey("Up", "gk");
-    CodeMirror.mapVimKey("<PageUp>", "<C-u>");
-    CodeMirror.mapVimKey("<PageDown>", "<C-d>");
-  }
-
-  private static void error(Exception e) {
-    Logger log = Logger.getLogger("net.codemirror");
-    log.log(Level.SEVERE, "Cannot load portions of CodeMirror", e);
-  }
-
   private Loader() {
   }
 }
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java
deleted file mode 100644
index 3098c43..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java
+++ /dev/null
@@ -1,208 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package net.codemirror.lib;
-
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.resources.client.DataResource;
-import com.google.gwt.safehtml.shared.SafeUri;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-import net.codemirror.mode.Modes;
-
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-public class ModeInjector {
-  /** Map of server content type to CodeMiror mode or content type. */
-  private static final Map<String, String> mimeAlias;
-
-  /** Map of content type "text/x-java" to mode name "clike". */
-  private static final Map<String, String> mimeModes;
-
-  /** Map of names such as "clike" to URI for code download. */
-  private static final Map<String, SafeUri> modeUris;
-
-  static {
-    DataResource[] all = {
-      Modes.I.clike(),
-      Modes.I.clojure(),
-      Modes.I.commonlisp(),
-      Modes.I.coffeescript(),
-      Modes.I.css(),
-      Modes.I.d(),
-      Modes.I.diff(),
-      Modes.I.dtd(),
-      Modes.I.erlang(),
-      Modes.I.gas(),
-      Modes.I.gerrit_commit(),
-      Modes.I.gfm(),
-      Modes.I.go(),
-      Modes.I.groovy(),
-      Modes.I.haskell(),
-      Modes.I.htmlmixed(),
-      Modes.I.javascript(),
-      Modes.I.lua(),
-      Modes.I.markdown(),
-      Modes.I.perl(),
-      Modes.I.php(),
-      Modes.I.pig(),
-      Modes.I.properties(),
-      Modes.I.python(),
-      Modes.I.r(),
-      Modes.I.ruby(),
-      Modes.I.scheme(),
-      Modes.I.shell(),
-      Modes.I.smalltalk(),
-      Modes.I.sql(),
-      Modes.I.velocity(),
-      Modes.I.verilog(),
-      Modes.I.xml(),
-      Modes.I.yaml(),
-    };
-
-    mimeAlias = new HashMap<>();
-    mimeModes = new HashMap<>();
-    modeUris = new HashMap<>();
-
-    for (DataResource m : all) {
-      modeUris.put(m.getName(), m.getSafeUri());
-    }
-    parseModeMap();
-  }
-
-  private static void parseModeMap() {
-    String mode = null;
-    for (String line : Modes.I.mode_map().getText().split("\n")) {
-      int eq = line.indexOf('=');
-      if (0 < eq) {
-        mimeAlias.put(
-          line.substring(0, eq).trim(),
-          line.substring(eq + 1).trim());
-      } else if (line.endsWith(":")) {
-        String n = line.substring(0, line.length() - 1);
-        if (modeUris.containsKey(n)) {
-          mode = n;
-        }
-      } else if (mode != null && line.contains("/")) {
-        mimeModes.put(line, mode);
-      } else {
-        mode = null;
-      }
-    }
-  }
-
-  public static String getContentType(String mode) {
-    String real = mode != null ? mimeAlias.get(mode) : null;
-    return real != null ? real : mode;
-  }
-
-  public static Collection<String> getKnownMimeTypes() {
-    return mimeModes.keySet();
-  }
-
-  private static native boolean isModeLoaded(String n)
-  /*-{ return $wnd.CodeMirror.modes.hasOwnProperty(n); }-*/;
-
-  private static native boolean isMimeLoaded(String n)
-  /*-{ return $wnd.CodeMirror.mimeModes.hasOwnProperty(n); }-*/;
-
-  private static native JsArrayString getDependencies(String n)
-  /*-{ return $wnd.CodeMirror.modes[n].dependencies || []; }-*/;
-
-  private final Set<String> loading = new HashSet<>(4);
-  private int pending;
-  private AsyncCallback<Void> appCallback;
-
-  public ModeInjector add(String name) {
-    if (name == null || isModeLoaded(name) || isMimeLoaded(name)) {
-      return this;
-    }
-
-    String mode = mimeModes.get(name);
-    if (mode == null) {
-      mode = name;
-    }
-
-    SafeUri uri = modeUris.get(mode);
-    if (uri == null) {
-      Logger.getLogger("net.codemirror").log(
-        Level.WARNING,
-        "CodeMirror mode " + mode + " not configured.");
-      return this;
-    }
-
-    loading.add(mode);
-    return this;
-  }
-
-  public void inject(AsyncCallback<Void> appCallback) {
-    this.appCallback = appCallback;
-    for (String mode : loading) {
-      beginLoading(mode);
-    }
-    if (pending == 0) {
-      appCallback.onSuccess(null);
-    }
-  }
-
-  private void beginLoading(final String mode) {
-    pending++;
-    Loader.injectScript(
-      modeUris.get(mode),
-      new AsyncCallback<Void>() {
-        @Override
-        public void onSuccess(Void result) {
-          pending--;
-          ensureDependenciesAreLoaded(mode);
-          if (pending == 0) {
-            appCallback.onSuccess(null);
-          }
-        }
-
-        @Override
-        public void onFailure(Throwable caught) {
-          if (--pending == 0) {
-            appCallback.onFailure(caught);
-          }
-        }
-      });
-  }
-
-  private void ensureDependenciesAreLoaded(String mode) {
-    JsArrayString deps = getDependencies(mode);
-    for (int i = 0; i < deps.length(); i++) {
-      String d = deps.get(i);
-      if (loading.contains(d) || isModeLoaded(d)) {
-        continue;
-      }
-
-      SafeUri uri = modeUris.get(d);
-      if (uri == null) {
-        Logger.getLogger("net.codemirror").log(
-          Level.SEVERE,
-          "CodeMirror mode " + mode + " needs " + d);
-        continue;
-      }
-
-      loading.add(d);
-      beginLoading(d);
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Pos.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Pos.java
new file mode 100644
index 0000000..07ead43
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Pos.java
@@ -0,0 +1,41 @@
+// 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 net.codemirror.lib;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/** Pos (or {line, ch}) objects used within CodeMirror. */
+public class Pos extends JavaScriptObject {
+  public static final native Pos create(int line) /*-{
+    return $wnd.CodeMirror.Pos(line)
+  }-*/;
+
+  public static final native Pos create(int line, int ch) /*-{
+    return $wnd.CodeMirror.Pos(line, ch)
+  }-*/;
+
+  public final native void line(int l) /*-{ this.line = l }-*/;
+  public final native void ch(int c) /*-{ this.ch = c }-*/;
+
+  public final native int line() /*-{ return this.line }-*/;
+  public final native int ch() /*-{ return this.ch || 0 }-*/;
+
+  public final boolean equals(Pos o) {
+    return this == o || (line() == o.line() && ch() == o.ch());
+  }
+
+  protected Pos() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/ScrollInfo.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/ScrollInfo.java
index 096f1ad..2623530 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/ScrollInfo.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/ScrollInfo.java
@@ -18,20 +18,20 @@
 
 /** Returned by {@link CodeMirror#getScrollInfo()}. */
 public class ScrollInfo extends JavaScriptObject {
-  public final native double getLeft() /*-{ return this.left; }-*/;
-  public final native double getTop() /*-{ return this.top; }-*/;
+  public final native double left() /*-{ return this.left }-*/;
+  public final native double top() /*-{ return this.top }-*/;
 
   /**
    * Pixel height of the full content being scrolled. This may only be an
    * estimate given by CodeMirror. Line widgets further down in the document may
    * not be measured, so line heights can be incorrect until drawn.
    */
-  public final native double getHeight() /*-{ return this.height; }-*/;
-  public final native double getWidth() /*-{ return this.width; }-*/;
+  public final native double height() /*-{ return this.height }-*/;
+  public final native double width() /*-{ return this.width }-*/;
 
   /** Visible height of the viewport, excluding scrollbars. */
-  public final native double getClientHeight() /*-{ return this.clientHeight; }-*/;
-  public final native double getClientWidth() /*-{ return this.clientWidth; }-*/;
+  public final native double clientHeight() /*-{ return this.clientHeight }-*/;
+  public final native double clientWidth() /*-{ return this.clientWidth }-*/;
 
   protected ScrollInfo() {
   }
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java
index 50db13c..2d69015 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java
@@ -19,10 +19,6 @@
 
 /** Object that represents a text marker within CodeMirror */
 public class TextMarker extends JavaScriptObject {
-  public static TextMarker create() {
-    return createObject().cast();
-  }
-
   public final native void clear() /*-{ this.clear(); }-*/;
   public final native void changed() /*-{ this.changed(); }-*/;
   public final native FromTo find() /*-{ return this.find(); }-*/;
@@ -33,24 +29,21 @@
   }
 
   public static class FromTo extends JavaScriptObject {
-    public static FromTo create(LineCharacter from, LineCharacter to) {
-      FromTo fromTo = createObject().cast();
-      fromTo.setFrom(from);
-      fromTo.setTo(to);
-      return fromTo;
-    }
+    public static final native FromTo create(Pos f, Pos t) /*-{
+      return {from: f, to: t}
+    }-*/;
 
     public static FromTo create(CommentRange range) {
       return create(
-          LineCharacter.create(range.start_line() - 1, range.start_character()),
-          LineCharacter.create(range.end_line() - 1, range.end_character()));
+          Pos.create(range.start_line() - 1, range.start_character()),
+          Pos.create(range.end_line() - 1, range.end_character()));
     }
 
-    public final native LineCharacter getFrom() /*-{ return this.from; }-*/;
-    public final native LineCharacter getTo() /*-{ return this.to; }-*/;
+    public final native Pos from() /*-{ return this.from }-*/;
+    public final native Pos to() /*-{ return this.to }-*/;
 
-    public final native void setFrom(LineCharacter from) /*-{ this.from = from; }-*/;
-    public final native void setTo(LineCharacter to) /*-{ this.to = to; }-*/;
+    public final native void from(Pos f) /*-{ this.from = f }-*/;
+    public final native void to(Pos t) /*-{ this.to = t }-*/;
 
     protected FromTo() {
     }
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Vim.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Vim.java
new file mode 100644
index 0000000..f1dee69
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Vim.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.lib;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/**
+ * Glue around the Vim emulation for {@link CodeMirror}.
+ *
+ * As an instance {@code this} is actually the {@link CodeMirror} object. Class
+ * Vim is providing a new namespace for Vim related methods that are associated
+ * with an editor.
+ */
+public class Vim extends JavaScriptObject {
+  static void initKeyMap() {
+    KeyMap km = KeyMap.create();
+    for (String key : new String[] {"A", "C", "I", "O", "R", "U"}) {
+      km.propagate(key);
+      km.propagate("'" + key.toLowerCase() + "'");
+    }
+    for (String key : new String[] {
+      "Ctrl-C", "Ctrl-O", "Ctrl-P", "Ctrl-S",
+      "Ctrl-F", "Ctrl-B", "Ctrl-R"}) {
+      km.propagate(key);
+    }
+    for (int i = 0; i <= 9; i++) {
+      km.propagate("Ctrl-" + i);
+    }
+    km.fallthrough(CodeMirror.getKeyMap("vim"));
+    CodeMirror.addKeyMap("vim_ro", km);
+
+    mapKey("j", "gj");
+    mapKey("k", "gk");
+    mapKey("Down", "gj");
+    mapKey("Up", "gk");
+    mapKey("<PageUp>", "<C-u>");
+    mapKey("<PageDown>", "<C-d>");
+  }
+
+  public static final native void mapKey(String alias, String actual) /*-{
+    $wnd.CodeMirror.Vim.map(alias, actual)
+  }-*/;
+
+  public final native void handleKey(String key) /*-{
+    $wnd.CodeMirror.Vim.handleKey(this, key)
+  }-*/;
+
+  public final native boolean hasSearchHighlight() /*-{
+    var v = this.state.vim;
+    return v && v.searchState_ && !!v.searchState_.getOverlay();
+  }-*/;
+
+  protected Vim() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css b/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css
new file mode 100644
index 0000000..c5f79d3
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css
@@ -0,0 +1,83 @@
+/* Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@external .CodeMirror;
+@external .CodeMirror-lines;
+@external .CodeMirror-linenumber;
+@external .CodeMirror-overlayscroll-horizontal;
+@external .CodeMirror-overlayscroll-vertical;
+@external .cm-tab;
+@external .cm-searching;
+@external .cm-trailingspace;
+
+/* Reduce margins around CodeMirror to save space. */
+.CodeMirror-lines {
+  padding: 0;
+}
+.CodeMirror pre {
+  padding: 0;
+  line-height: normal;
+}
+
+/* Minimum scrollbar bubble size even on large files. */
+.CodeMirror-overlayscroll-horizontal div {
+  min-width: 25px;
+}
+.CodeMirror-overlayscroll-vertical div {
+  min-height: 25px;
+}
+
+/* Stack the scrollbar so annotations can receive clicks. */
+.CodeMirror-overlayscroll-vertical {
+  z-index: inherit;
+}
+.CodeMirror-overlayscroll-horizontal div,
+.CodeMirror-overlayscroll-vertical div {
+  background-color: rgba(128, 128, 128, 0.50);
+  z-index: 8;
+}
+
+/* Highlight current line number in the line gutter. */
+.activeLine .CodeMirror-linenumber {
+  background-color: #bcf !important;
+  color: #000;
+}
+
+.showTabs .cm-tab:before {
+  position: absolute;
+  content: "\00bb";
+  color: #f00;
+}
+
+.cm-searching {
+  background-color: #ffa;
+}
+
+.cm-trailingspace {
+  background-color: red;
+}
+
+/* Line length margin displayed at NN columns to provide
+ * a visual guide for length of any single line of code.
+ */
+.margin {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  width: 0;
+  border-right: 1px dashed #ffa500;
+  z-index: 2;
+  cursor: text;
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
new file mode 100644
index 0000000..d6b194b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
@@ -0,0 +1,198 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.mode;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.resources.client.DataResource;
+import com.google.gwt.safehtml.shared.SafeUri;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Description of a CodeMirror language mode. */
+public class ModeInfo extends JavaScriptObject {
+  private static NativeMap<ModeInfo> byMime;
+  private static NativeMap<ModeInfo> byExt;
+
+  /** Map of names such as "clike" to URI for code download. */
+  private static final Map<String, SafeUri> modeUris = new HashMap<>();
+
+  static {
+    indexModes(new DataResource[] {
+      Modes.I.clike(),
+      Modes.I.clojure(),
+      Modes.I.coffeescript(),
+      Modes.I.commonlisp(),
+      Modes.I.css(),
+      Modes.I.d(),
+      Modes.I.dart(),
+      Modes.I.diff(),
+      Modes.I.dockerfile(),
+      Modes.I.dtd(),
+      Modes.I.erlang(),
+      Modes.I.gas(),
+      Modes.I.gerrit_commit(),
+      Modes.I.gfm(),
+      Modes.I.go(),
+      Modes.I.groovy(),
+      Modes.I.haskell(),
+      Modes.I.htmlmixed(),
+      Modes.I.javascript(),
+      Modes.I.lua(),
+      Modes.I.markdown(),
+      Modes.I.perl(),
+      Modes.I.php(),
+      Modes.I.pig(),
+      Modes.I.properties(),
+      Modes.I.python(),
+      Modes.I.r(),
+      Modes.I.rst(),
+      Modes.I.ruby(),
+      Modes.I.scheme(),
+      Modes.I.shell(),
+      Modes.I.smalltalk(),
+      Modes.I.soy(),
+      Modes.I.sql(),
+      Modes.I.stex(),
+      Modes.I.velocity(),
+      Modes.I.verilog(),
+      Modes.I.xml(),
+      Modes.I.yaml(),
+    });
+
+    alias("application/x-httpd-php-open", "application/x-httpd-php");
+    alias("application/x-javascript", "application/javascript");
+    alias("application/x-shellscript", "text/x-sh");
+    alias("application/x-tcl", "text/x-tcl");
+    alias("text/typescript", "application/typescript");
+    alias("text/x-c", "text/x-csrc");
+    alias("text/x-c++hdr", "text/x-c++src");
+    alias("text/x-chdr", "text/x-csrc");
+    alias("text/x-h", "text/x-csrc");
+    alias("text/x-ini", "text/x-properties");
+    alias("text/x-java-source", "text/x-java");
+    alias("text/x-php", "application/x-httpd-php");
+    alias("text/x-scripttcl", "text/x-tcl");
+  }
+
+  /** All supported modes. */
+  public static native JsArray<ModeInfo> all() /*-{
+    return $wnd.CodeMirror.modeInfo
+  }-*/;
+
+  private static native void setAll(JsArray<ModeInfo> m) /*-{
+    $wnd.CodeMirror.modeInfo = m
+  }-*/;
+
+  /** Look up mode by primary or alternate MIME types. */
+  public static ModeInfo findModeByMIME(String mime) {
+    return byMime.get(mime);
+  }
+
+  public static SafeUri getModeScriptUri(String mode) {
+    return modeUris.get(mode);
+  }
+
+  /** Look up mode by MIME type or file extension from a path. */
+  public static ModeInfo findMode(String mime, String path) {
+    ModeInfo m = byMime.get(mime);
+    if (m != null) {
+      return m;
+    }
+
+    int s = path.lastIndexOf('/');
+    int d = path.lastIndexOf('.');
+    if (d == -1 || s > d) {
+      return null; // punt on "foo.src/bar" type paths.
+    }
+
+    if (byExt == null) {
+      byExt = NativeMap.create();
+      for (ModeInfo mode : Natives.asList(all())) {
+        for (String ext : Natives.asList(mode.ext())) {
+          byExt.put(ext, mode);
+        }
+      }
+    }
+    return byExt.get(path.substring(d + 1));
+  }
+
+  private static void alias(String serverMime, String toMime) {
+    ModeInfo mode = byMime.get(toMime);
+    if (mode != null) {
+      byMime.put(serverMime, mode);
+    }
+  }
+
+  private static void indexModes(DataResource[] all) {
+    for (DataResource r : all) {
+      modeUris.put(r.getName(), r.getSafeUri());
+    }
+
+    JsArray<ModeInfo> modeList = all();
+    modeList.push(gerrit_commit());
+
+    byMime = NativeMap.create();
+    JsArray<ModeInfo> filtered = JsArray.createArray().cast();
+    for (ModeInfo m : Natives.asList(modeList)) {
+      if (modeUris.containsKey(m.mode())) {
+        filtered.push(m);
+
+        for (String mimeType : Natives.asList(m.mimes())) {
+          byMime.put(mimeType, m);
+        }
+        byMime.put(m.mode(), m);
+      }
+    }
+    Collections.sort(Natives.asList(filtered), new Comparator<ModeInfo>() {
+      @Override
+      public int compare(ModeInfo a, ModeInfo b) {
+        return a.name().toLowerCase().compareTo(b.name().toLowerCase());
+      }
+    });
+    setAll(filtered);
+  }
+
+  /** Human readable name of the mode, such as "C++". */
+  public final native String name() /*-{ return this.name }-*/;
+
+  /** Internal CodeMirror name for {@code mode.js} file to load. */
+  public final native String mode() /*-{ return this.mode }-*/;
+
+  /** Primary MIME type to activate this mode. */
+  public final native String mime() /*-{ return this.mime }-*/;
+
+  /** Primary and additional MIME types that activate this mode. */
+  public final native JsArrayString mimes()
+  /*-{ return this.mimes || [this.mime] }-*/;
+
+  private final native JsArrayString ext()
+  /*-{ return this.ext || [] }-*/;
+
+  protected ModeInfo() {
+  }
+
+  private static native ModeInfo gerrit_commit() /*-{
+    return {name: "Git Commit Message",
+            mime: "text/x-gerrit-commit-message",
+            mode: "gerrit_commit"}
+  }-*/;
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInjector.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInjector.java
new file mode 100644
index 0000000..2c171ac
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInjector.java
@@ -0,0 +1,118 @@
+// 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 net.codemirror.mode;
+
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import net.codemirror.lib.Loader;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class ModeInjector {
+  private static boolean canLoad(String mode) {
+    return ModeInfo.getModeScriptUri(mode) != null;
+  }
+
+  private static native boolean isModeLoaded(String n)
+  /*-{ return $wnd.CodeMirror.modes.hasOwnProperty(n); }-*/;
+
+  private static native boolean isMimeLoaded(String n)
+  /*-{ return $wnd.CodeMirror.mimeModes.hasOwnProperty(n); }-*/;
+
+  private static native JsArrayString getDependencies(String n)
+  /*-{ return $wnd.CodeMirror.modes[n].dependencies || []; }-*/;
+
+  private final Set<String> loading = new HashSet<>(4);
+  private int pending;
+  private AsyncCallback<Void> appCallback;
+
+  public ModeInjector add(String name) {
+    if (name == null || isModeLoaded(name) || isMimeLoaded(name)) {
+      return this;
+    }
+
+    ModeInfo m = ModeInfo.findModeByMIME(name);
+    if (m != null) {
+      name = m.mode();
+    }
+
+    if (!canLoad(name)) {
+      Logger.getLogger("net.codemirror").log(
+        Level.WARNING,
+        "CodeMirror mode " + name + " not configured.");
+      return this;
+    }
+
+    loading.add(name);
+    return this;
+  }
+
+  public void inject(AsyncCallback<Void> appCallback) {
+    this.appCallback = appCallback;
+    for (String mode : loading) {
+      beginLoading(mode);
+    }
+    if (pending == 0) {
+      appCallback.onSuccess(null);
+    }
+  }
+
+  private void beginLoading(final String mode) {
+    pending++;
+    Loader.injectScript(
+      ModeInfo.getModeScriptUri(mode),
+      new AsyncCallback<Void>() {
+        @Override
+        public void onSuccess(Void result) {
+          pending--;
+          ensureDependenciesAreLoaded(mode);
+          if (pending == 0) {
+            appCallback.onSuccess(null);
+          }
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+          if (--pending == 0) {
+            appCallback.onFailure(caught);
+          }
+        }
+      });
+  }
+
+  private void ensureDependenciesAreLoaded(String mode) {
+    JsArrayString deps = getDependencies(mode);
+    for (int i = 0; i < deps.length(); i++) {
+      String d = deps.get(i);
+      if (loading.contains(d) || isModeLoaded(d)) {
+        continue;
+      }
+
+      if (!canLoad(d)) {
+        Logger.getLogger("net.codemirror").log(
+          Level.SEVERE,
+          "CodeMirror mode " + d + " needs " + d);
+        continue;
+      }
+
+      loading.add(d);
+      beginLoading(d);
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
index d51479f..e511be5 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
@@ -18,47 +18,50 @@
 import com.google.gwt.resources.client.ClientBundle;
 import com.google.gwt.resources.client.DataResource;
 import com.google.gwt.resources.client.DataResource.DoNotEmbed;
-import com.google.gwt.resources.client.TextResource;
 
 public interface Modes extends ClientBundle {
   public static final Modes I = GWT.create(Modes.class);
 
-  @Source("mode_map") TextResource mode_map();
-  @Source("clike/clike.js") @DoNotEmbed DataResource clike();
-  @Source("clojure/clojure.js") @DoNotEmbed DataResource clojure();
-  @Source("commonlisp/commonlisp.js") @DoNotEmbed DataResource commonlisp();
-  @Source("coffeescript/coffeescript.js") @DoNotEmbed DataResource coffeescript();
-  @Source("css/css.js") @DoNotEmbed DataResource css();
-  @Source("d/d.js") @DoNotEmbed DataResource d();
-  @Source("diff/diff.js") @DoNotEmbed DataResource diff();
-  @Source("dtd/dtd.js") @DoNotEmbed DataResource dtd();
-  @Source("erlang/erlang.js") @DoNotEmbed DataResource erlang();
-  @Source("gas/gas.js") @DoNotEmbed DataResource gas();
+  @Source("clike.js") @DoNotEmbed DataResource clike();
+  @Source("clojure.js") @DoNotEmbed DataResource clojure();
+  @Source("coffeescript.js") @DoNotEmbed DataResource coffeescript();
+  @Source("commonlisp.js") @DoNotEmbed DataResource commonlisp();
+  @Source("css.js") @DoNotEmbed DataResource css();
+  @Source("d.js") @DoNotEmbed DataResource d();
+  @Source("dart.js") @DoNotEmbed DataResource dart();
+  @Source("diff.js") @DoNotEmbed DataResource diff();
+  @Source("dockerfile.js") @DoNotEmbed DataResource dockerfile();
+  @Source("dtd.js") @DoNotEmbed DataResource dtd();
+  @Source("erlang.js") @DoNotEmbed DataResource erlang();
+  @Source("gas.js") @DoNotEmbed DataResource gas();
   @Source("gerrit/commit.js") @DoNotEmbed DataResource gerrit_commit();
-  @Source("gfm/gfm.js") @DoNotEmbed DataResource gfm();
-  @Source("go/go.js") @DoNotEmbed DataResource go();
-  @Source("groovy/groovy.js") @DoNotEmbed DataResource groovy();
-  @Source("haskell/haskell.js") @DoNotEmbed DataResource haskell();
-  @Source("htmlmixed/htmlmixed.js") @DoNotEmbed DataResource htmlmixed();
-  @Source("javascript/javascript.js") @DoNotEmbed DataResource javascript();
-  @Source("lua/lua.js") @DoNotEmbed DataResource lua();
-  @Source("markdown/markdown.js") @DoNotEmbed DataResource markdown();
-  @Source("perl/perl.js") @DoNotEmbed DataResource perl();
-  @Source("php/php.js") @DoNotEmbed DataResource php();
-  @Source("pig/pig.js") @DoNotEmbed DataResource pig();
-  @Source("properties/properties.js") @DoNotEmbed DataResource properties();
-  @Source("python/python.js") @DoNotEmbed DataResource python();
-  @Source("r/r.js") @DoNotEmbed DataResource r();
-  @Source("ruby/ruby.js") @DoNotEmbed DataResource ruby();
-  @Source("scheme/scheme.js") @DoNotEmbed DataResource scheme();
-  @Source("shell/shell.js") @DoNotEmbed DataResource shell();
-  @Source("smalltalk/smalltalk.js") @DoNotEmbed DataResource smalltalk();
-  @Source("sql/sql.js") @DoNotEmbed DataResource sql();
-  @Source("tcl/tcl.js") @DoNotEmbed DataResource tcl();
-  @Source("velocity/velocity.js") @DoNotEmbed DataResource velocity();
-  @Source("verilog/verilog.js") @DoNotEmbed DataResource verilog();
-  @Source("xml/xml.js") @DoNotEmbed DataResource xml();
-  @Source("yaml/yaml.js") @DoNotEmbed DataResource yaml();
+  @Source("gfm.js") @DoNotEmbed DataResource gfm();
+  @Source("go.js") @DoNotEmbed DataResource go();
+  @Source("groovy.js") @DoNotEmbed DataResource groovy();
+  @Source("haskell.js") @DoNotEmbed DataResource haskell();
+  @Source("htmlmixed.js") @DoNotEmbed DataResource htmlmixed();
+  @Source("javascript.js") @DoNotEmbed DataResource javascript();
+  @Source("lua.js") @DoNotEmbed DataResource lua();
+  @Source("markdown.js") @DoNotEmbed DataResource markdown();
+  @Source("perl.js") @DoNotEmbed DataResource perl();
+  @Source("php.js") @DoNotEmbed DataResource php();
+  @Source("pig.js") @DoNotEmbed DataResource pig();
+  @Source("properties.js") @DoNotEmbed DataResource properties();
+  @Source("python.js") @DoNotEmbed DataResource python();
+  @Source("r.js") @DoNotEmbed DataResource r();
+  @Source("rst.js") @DoNotEmbed DataResource rst();
+  @Source("ruby.js") @DoNotEmbed DataResource ruby();
+  @Source("scheme.js") @DoNotEmbed DataResource scheme();
+  @Source("shell.js") @DoNotEmbed DataResource shell();
+  @Source("smalltalk.js") @DoNotEmbed DataResource smalltalk();
+  @Source("soy.js") @DoNotEmbed DataResource soy();
+  @Source("sql.js") @DoNotEmbed DataResource sql();
+  @Source("stex.js") @DoNotEmbed DataResource stex();
+  @Source("tcl.js") @DoNotEmbed DataResource tcl();
+  @Source("velocity.js") @DoNotEmbed DataResource velocity();
+  @Source("verilog.js") @DoNotEmbed DataResource verilog();
+  @Source("xml.js") @DoNotEmbed DataResource xml();
+  @Source("yaml.js") @DoNotEmbed DataResource yaml();
 
-  // When adding a resource, update static initializer in ModeInjector.
+  // When adding a resource, update static initializer in ModeInfo.
 }
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/gerrit/commit.js b/gerrit-gwtui/src/main/java/net/codemirror/mode/gerrit/commit.js
index a846e50..e1fe898 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/gerrit/commit.js
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/gerrit/commit.js
@@ -2,7 +2,7 @@
   var header = /^(Parent|Author|AuthorDate|Commit|CommitDate):/;
   var id = /^Change-Id: I[0-9a-f]{40}/;
   var footer = /^[A-Z][A-Za-z0-9-]+:/;
-  var sha1 = /[0-9a-f]{6,40}/;
+  var sha1 = /\b[0-9a-f]{6,40}/;
 
   return {
     token: function(stream) {
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map b/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map
deleted file mode 100644
index 2bff364..0000000
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map
+++ /dev/null
@@ -1,137 +0,0 @@
-clike:
-text/x-csrc
-text/x-c
-text/x-chdr
-text/x-c++src
-text/x-c++hdr
-text/x-java
-text/x-csharp
-text/x-scala
-
-clojure:
-text/x-clojure
-
-coffeescript:
-text/x-coffeescript
-
-commonlisp:
-text/x-common-lisp
-
-css:
-text/css
-text/x-scss
-
-d:
-text/x-d
-
-diff:
-text/x-diff
-
-dtd:
-application/xml-dtd
-
-erlang:
-text/x-erlang
-
-gas:
-text/x-gas
-
-gerrit_commit:
-text/x-gerrit-commit-message
-
-gfm:
-text/x-github-markdown
-
-go:
-text/x-go
-
-groovy:
-text/x-groovy
-
-haskell:
-text/x-haskell
-
-htmlmixed:
-text/html
-
-javascript:
-text/javascript
-text/ecmascript
-application/javascript
-application/ecmascript
-application/json
-application/x-json
-text/typescript
-application/typescript
-
-lua:
-text/x-lua
-
-markdown:
-text/x-markdown
-
-perl:
-text/x-perl
-
-properties:
-text/x-ini
-text/x-properties
-
-perl:
-text/x-perl
-
-php:
-application/x-httpd-php
-application/x-httpd-php-open
-text/x-php
-
-pig:
-text/x-pig
-
-python:
-text/x-python
-
-r:
-text/r-src
-
-ruby:
-text/x-ruby
-
-scheme:
-text/x-scheme
-
-shell:
-text/x-sh
-application/x-shellscript
-
-smalltalk:
-text/x-stsrc
-
-sql:
-text/x-sql
-text/x-mariadb
-text/x-mysql
-text/x-plsql
-
-tcl:
-text/x-tcl
-
-velocity:
-text/velocity
-
-verilog:
-text/x-verilog
-
-xml:
-text/xml
-application/xml
-
-yaml:
-text/x-yaml
-
-application/x-javascript = application/javascript
-application/x-shellscript = text/x-sh
-application/x-tcl = text/x-tcl
-text/x-h = text/x-c++hdr
-text/x-java-source = text/x-java
-text/x-scripttcl = text/x-tcl
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/theme/ThemeLoader.java b/gerrit-gwtui/src/main/java/net/codemirror/theme/ThemeLoader.java
new file mode 100644
index 0000000..c563c0a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/theme/ThemeLoader.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.theme;
+
+import com.google.gerrit.extensions.client.Theme;
+import com.google.gwt.dom.client.StyleInjector;
+import com.google.gwt.resources.client.ExternalTextResource;
+import com.google.gwt.resources.client.ResourceCallback;
+import com.google.gwt.resources.client.ResourceException;
+import com.google.gwt.resources.client.TextResource;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import java.util.EnumSet;
+
+/** Dynamically loads a known CodeMirror theme's CSS */
+public class ThemeLoader {
+  private static final ExternalTextResource[] THEMES = {
+      Themes.I.eclipse(),
+      Themes.I.elegant(),
+      Themes.I.midnight(),
+      Themes.I.neat(),
+      Themes.I.night(),
+      Themes.I.twilight(),
+  };
+
+  private static final EnumSet<Theme> loaded = EnumSet.of(Theme.DEFAULT);
+
+  public static final void loadTheme(final Theme theme,
+      final AsyncCallback<Void> cb) {
+    if (loaded.contains(theme)) {
+      cb.onSuccess(null);
+      return;
+    }
+
+    ExternalTextResource resource = findTheme(theme);
+    if (resource == null) {
+      cb.onFailure(new Exception("unknown theme " + theme));
+      return;
+    }
+
+    try {
+      resource.getText(new ResourceCallback<TextResource>() {
+        @Override
+        public void onSuccess(TextResource resource) {
+          StyleInjector.inject(resource.getText());
+          loaded.add(theme);
+          cb.onSuccess(null);
+        }
+
+        @Override
+        public void onError(ResourceException e) {
+          cb.onFailure(e);
+        }
+      });
+    } catch (ResourceException e) {
+      cb.onFailure(e);
+    }
+  }
+
+  private static final ExternalTextResource findTheme(Theme theme) {
+    for (ExternalTextResource r : THEMES) {
+      if (theme.name().toLowerCase().equals(r.getName())) {
+        return r;
+      }
+    }
+    return null;
+  }
+
+  private ThemeLoader() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/theme/Themes.java b/gerrit-gwtui/src/main/java/net/codemirror/theme/Themes.java
new file mode 100644
index 0000000..ed0ffca
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/theme/Themes.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.theme;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.ExternalTextResource;
+
+public interface Themes extends ClientBundle {
+  public static final Themes I = GWT.create(Themes.class);
+
+  @Source("eclipse.css") ExternalTextResource eclipse();
+  @Source("elegant.css") ExternalTextResource elegant();
+  @Source("midnight.css") ExternalTextResource midnight();
+  @Source("neat.css") ExternalTextResource neat();
+  @Source("night.css") ExternalTextResource night();
+  @Source("twilight.css") ExternalTextResource twilight();
+
+  // When adding a resource, update:
+  // - static initializer in ThemeLoader
+  // - enum value in com.google.gerrit.extensions.common.Theme
+}
diff --git a/gerrit-gwtui/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java b/gerrit-gwtui/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java
index 5be029c..6705e51 100644
--- a/gerrit-gwtui/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java
+++ b/gerrit-gwtui/src/test/java/com/google/gerrit/client/RelativeDateFormatterTest.java
@@ -14,18 +14,18 @@
 
 package com.google.gerrit.client;
 
-import static org.junit.Assert.assertEquals;
-import static com.google.gerrit.client.RelativeDateFormatter.YEAR_IN_MILLIS;
-import static com.google.gerrit.client.RelativeDateFormatter.SECOND_IN_MILLIS;
-import static com.google.gerrit.client.RelativeDateFormatter.MINUTE_IN_MILLIS;
-import static com.google.gerrit.client.RelativeDateFormatter.HOUR_IN_MILLIS;
 import static com.google.gerrit.client.RelativeDateFormatter.DAY_IN_MILLIS;
-
-import java.util.Date;
+import static com.google.gerrit.client.RelativeDateFormatter.HOUR_IN_MILLIS;
+import static com.google.gerrit.client.RelativeDateFormatter.MINUTE_IN_MILLIS;
+import static com.google.gerrit.client.RelativeDateFormatter.SECOND_IN_MILLIS;
+import static com.google.gerrit.client.RelativeDateFormatter.YEAR_IN_MILLIS;
+import static org.junit.Assert.assertEquals;
 
 import org.eclipse.jgit.util.RelativeDateFormatter;
 import org.junit.Test;
 
+import java.util.Date;
+
 public class RelativeDateFormatterTest {
 
   private static void assertFormat(long ageFromNow, long timeUnit,
diff --git a/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/EditIteratorTest.java b/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/EditIteratorTest.java
index 1ebf6bb..d751f34 100644
--- a/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/EditIteratorTest.java
+++ b/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/EditIteratorTest.java
@@ -22,18 +22,21 @@
 import com.googlecode.gwt.test.GwtModule;
 import com.googlecode.gwt.test.GwtTest;
 
-import net.codemirror.lib.LineCharacter;
+import net.codemirror.lib.Pos;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 
 /** Unit tests for EditIterator */
 @GwtModule("com.google.gerrit.GerritGwtUI")
+@Ignore
+// TODO(davido): Enable this again when gwt-test-utils lib is fixed.
 public class EditIteratorTest extends GwtTest {
   private JsArrayString lines;
 
-  private void assertLineChsEqual(LineCharacter a, LineCharacter b) {
-    assertEquals(a.getLine() + "," + a.getCh(), b.getLine() + "," + b.getCh());
+  private void assertLineChsEqual(Pos a, Pos b) {
+    assertEquals(a.line() + "," + a.ch(), b.line() + "," + b.ch());
   }
 
   @Before
@@ -47,57 +50,57 @@
   @Test
   public void testNegativeAdvance() {
     EditIterator i = new EditIterator(lines, 0);
-    assertLineChsEqual(LineCharacter.create(1, 1), i.advance(5));
-    assertLineChsEqual(LineCharacter.create(0, 3), i.advance(-2));
+    assertLineChsEqual(Pos.create(1, 1), i.advance(5));
+    assertLineChsEqual(Pos.create(0, 3), i.advance(-2));
   }
 
   @Test
   public void testNoAdvance() {
     EditIterator iter = new EditIterator(lines, 0);
-    assertLineChsEqual(LineCharacter.create(0), iter.advance(0));
+    assertLineChsEqual(Pos.create(0), iter.advance(0));
   }
 
   @Test
   public void testSimpleAdvance() {
     EditIterator iter = new EditIterator(lines, 0);
-    assertLineChsEqual(LineCharacter.create(0, 1), iter.advance(1));
+    assertLineChsEqual(Pos.create(0, 1), iter.advance(1));
   }
 
   @Test
   public void testEndsBeforeNewline() {
     EditIterator iter = new EditIterator(lines, 0);
-    assertLineChsEqual(LineCharacter.create(0, 3), iter.advance(3));
+    assertLineChsEqual(Pos.create(0, 3), iter.advance(3));
   }
 
   @Test
   public void testEndsOnNewline() {
     EditIterator iter = new EditIterator(lines, 0);
-    assertLineChsEqual(LineCharacter.create(1), iter.advance(4));
+    assertLineChsEqual(Pos.create(1), iter.advance(4));
   }
 
   @Test
   public void testAcrossNewline() {
     EditIterator iter = new EditIterator(lines, 0);
-    assertLineChsEqual(LineCharacter.create(1, 1), iter.advance(5));
+    assertLineChsEqual(Pos.create(1, 1), iter.advance(5));
   }
 
   @Test
   public void testContinueFromBeforeNewline() {
     EditIterator iter = new EditIterator(lines, 0);
     iter.advance(3);
-    assertLineChsEqual(LineCharacter.create(2, 2), iter.advance(7));
+    assertLineChsEqual(Pos.create(2, 2), iter.advance(7));
   }
 
   @Test
   public void testContinueFromAfterNewline() {
     EditIterator iter = new EditIterator(lines, 0);
     iter.advance(4);
-    assertLineChsEqual(LineCharacter.create(2, 2), iter.advance(6));
+    assertLineChsEqual(Pos.create(2, 2), iter.advance(6));
   }
 
   @Test
   public void testAcrossMultipleLines() {
     EditIterator iter = new EditIterator(lines, 0);
-    assertLineChsEqual(LineCharacter.create(2, 2), iter.advance(10));
+    assertLineChsEqual(Pos.create(2, 2), iter.advance(10));
   }
 }
diff --git a/gerrit-httpd/BUCK b/gerrit-httpd/BUCK
index 4ecc9e0..a4d127d 100644
--- a/gerrit-httpd/BUCK
+++ b/gerrit-httpd/BUCK
@@ -18,6 +18,7 @@
     '//gerrit-reviewdb:server',
     '//gerrit-server:server',
     '//gerrit-util-cli:cli',
+    '//gerrit-util-http:http',
     '//lib:args4j',
     '//lib:gson',
     '//lib:guava',
@@ -25,6 +26,7 @@
     '//lib:gwtorm',
     '//lib:jsch',
     '//lib:mime-util',
+    '//lib/auto:auto-value',
     '//lib/commons:codec',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
@@ -53,6 +55,7 @@
     '//gerrit-extension-api:api',
     '//gerrit-reviewdb:server',
     '//gerrit-server:server',
+    '//gerrit-util-http:http',
     '//lib:junit',
     '//lib:gson',
     '//lib:gwtorm',
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AdvertisedObjectsCacheKey.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AdvertisedObjectsCacheKey.java
index d594d77..4bf3102 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AdvertisedObjectsCacheKey.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AdvertisedObjectsCacheKey.java
@@ -14,28 +14,16 @@
 
 package com.google.gerrit.httpd;
 
+import com.google.auto.value.AutoValue;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
 
-class AdvertisedObjectsCacheKey {
-  private final Account.Id account;
-  private final Project.NameKey project;
-
-  AdvertisedObjectsCacheKey(Account.Id account, Project.NameKey project) {
-    this.account = account;
-    this.project = project;
+@AutoValue
+abstract class AdvertisedObjectsCacheKey {
+  static AdvertisedObjectsCacheKey create(Account.Id account, Project.NameKey project) {
+    return new AutoValue_AdvertisedObjectsCacheKey(account, project);
   }
 
-  @Override
-  public int hashCode() {
-    return account.hashCode();
-  }
-
-  public boolean equals(Object other) {
-    if (other instanceof AdvertisedObjectsCacheKey) {
-      AdvertisedObjectsCacheKey o = (AdvertisedObjectsCacheKey) other;
-      return account.equals(o.account) && project.equals(o.project);
-    }
-    return false;
-  }
+  public abstract Account.Id account();
+  public abstract Project.NameKey project();
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CanonicalWebUrl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CanonicalWebUrl.java
index 992c70a..901b180 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CanonicalWebUrl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CanonicalWebUrl.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.httpd;
 
-import javax.servlet.http.HttpServletRequest;
-
 import com.google.gerrit.common.Nullable;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import javax.servlet.http.HttpServletRequest;
+
 public class CanonicalWebUrl {
   private final Provider<String> configured;
 
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 f41f67c..4b0e46c 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
@@ -15,12 +15,12 @@
 package com.google.gerrit.httpd;
 
 import com.google.common.base.Function;
+import com.google.common.base.Optional;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.common.data.GitwebConfig;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.change.ArchiveFormat;
 import com.google.gerrit.server.change.GetArchive;
@@ -133,9 +133,6 @@
     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.CHANGE_SCREEN2));
     config.setLargeChangeSize(cfg.getInt("change", "largeChange", 500));
     config.setArchiveFormats(Lists.newArrayList(Iterables.transform(
         archiveFormats.getAllowed(),
@@ -146,11 +143,7 @@
           }
         })));
 
-    config.setNewFeatures(cfg.getBoolean("gerrit", "enableNewFeatures", true));
-
-    final String reportBugUrl = cfg.getString("gerrit", null, "reportBugUrl");
-    config.setReportBugUrl(reportBugUrl != null ?
-        reportBugUrl : "http://code.google.com/p/gerrit/issues/list");
+    config.setReportBugUrl(cfg.getString("gerrit", null, "reportBugUrl"));
     config.setReportBugText(cfg.getString("gerrit", null, "reportBugText"));
 
     final Set<Account.FieldName> fields = new HashSet<>();
@@ -174,6 +167,19 @@
       config.setSshdAddress(sshInfo.getHostKeys().get(0).getHost());
     }
 
+    String replyTitle =
+        Optional.fromNullable(cfg.getString("change", null, "replyTooltip"))
+        .or("Reply and score")
+        + " (Shortcut: a)";
+    String replyLabel =
+        Optional.fromNullable(cfg.getString("change", null, "replyLabel"))
+        .or("Reply")
+        + "\u2026";
+    config.setReplyTitle(replyTitle);
+    config.setReplyLabel(replyLabel);
+
+    config.setAllowDraftChanges(cfg.getBoolean("change", "allowDrafts", true));
+
     return config;
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritOptions.java
similarity index 78%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritOptions.java
index cd07320..adfe86c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritOptions.java
@@ -14,14 +14,20 @@
 
 package com.google.gerrit.httpd;
 
-public class GerritUiOptions {
+public class GerritOptions {
   private final boolean headless;
+  private final boolean slave;
 
-  public GerritUiOptions(boolean headless) {
+  public GerritOptions(boolean headless, boolean slave) {
     this.headless = headless;
+    this.slave = slave;
   }
 
   public boolean enableDefaultUi() {
     return !headless;
   }
+
+  public boolean enableMasterFeatures() {
+    return !slave;
+  }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/GetUserFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GetUserFilter.java
similarity index 72%
rename from gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/GetUserFilter.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/GetUserFilter.java
index 4f35f1c..94b8f29 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/GetUserFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GetUserFilter.java
@@ -12,9 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.http.jetty;
+package com.google.gerrit.httpd;
 
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -24,7 +25,6 @@
 import org.eclipse.jgit.lib.Config;
 
 import java.io.IOException;
-import java.net.URI;
 
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
@@ -34,28 +34,26 @@
 import javax.servlet.ServletResponse;
 
 /**
- * Stores as a request attribute, so the {@link HttpLog} can include the the
- * user for the request outside of the request scope.
+ * Stores user as a request attribute, so servlets can access it outside of the
+ * request scope.
  */
 @Singleton
 public class GetUserFilter implements Filter {
 
-  static final String REQ_ATTR_KEY = CurrentUser.class.toString();
+  public static final String REQ_ATTR_KEY = "User";
 
   public static class Module extends ServletModule {
 
-    private boolean loggingEnabled;
+    private final boolean enabled;
 
     @Inject
     Module(@GerritServerConfig final Config cfg) {
-      URI[] urls = JettyServer.listenURLs(cfg);
-      boolean reverseProxy = JettyServer.isReverseProxied(urls);
-      this.loggingEnabled = cfg.getBoolean("httpd", "requestLog", !reverseProxy);
+      enabled = cfg.getBoolean("http", "addUserAsRequestAttribute", true);
     }
 
     @Override
     protected void configureServlets() {
-      if (loggingEnabled) {
+      if (enabled) {
         filter("/*").through(GetUserFilter.class);
       }
     }
@@ -72,7 +70,15 @@
   public void doFilter(
       ServletRequest req, ServletResponse resp, FilterChain chain)
       throws IOException, ServletException {
-    req.setAttribute(REQ_ATTR_KEY, userProvider.get());
+    CurrentUser user = userProvider.get();
+    if (user != null && user.isIdentifiedUser()) {
+      IdentifiedUser who = (IdentifiedUser) user;
+      if (who.getUserName() != null && !who.getUserName().isEmpty()) {
+        req.setAttribute(REQ_ATTR_KEY, who.getUserName());
+      } else {
+        req.setAttribute(REQ_ATTR_KEY, "a/" + who.getAccountId());
+      }
+    }
     chain.doFilter(req, resp);
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 0c4f60c..e73f805 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -63,6 +63,8 @@
 import org.eclipse.jgit.transport.resolver.UploadPackFactory;
 
 import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
@@ -98,12 +100,20 @@
   }
 
   static class Module extends AbstractModule {
+
+    private final boolean enableReceive;
+
+    public Module(boolean enableReceive) {
+      this.enableReceive = enableReceive;
+    }
+
     @Override
     protected void configure() {
       bind(Resolver.class);
       bind(UploadFactory.class);
       bind(UploadFilter.class);
-      bind(ReceiveFactory.class);
+      bind(new TypeLiteral<ReceivePackFactory<HttpServletRequest>>() {}).to(
+          enableReceive ? ReceiveFactory.class : DisabledReceiveFactory.class);
       bind(ReceiveFilter.class);
       install(new CacheModule() {
         @Override
@@ -119,9 +129,10 @@
   }
 
   @Inject
-  GitOverHttpServlet(Resolver resolver,
-      UploadFactory upload, UploadFilter uploadFilter,
-      ReceiveFactory receive, ReceiveFilter receiveFilter) {
+  GitOverHttpServlet(Resolver resolver, UploadFactory upload,
+      UploadFilter uploadFilter,
+      ReceivePackFactory<HttpServletRequest> receive,
+      ReceiveFilter receiveFilter) {
     setRepositoryResolver(resolver);
     setAsIsFileService(AsIsFileService.DISABLED);
 
@@ -147,6 +158,13 @@
     public Repository open(HttpServletRequest req, String projectName)
         throws RepositoryNotFoundException, ServiceNotAuthorizedException,
         ServiceNotEnabledException {
+      try {
+        // TODO: remove this code when Guice fixes its issue 745
+        projectName = URLDecoder.decode(projectName, "UTF-8");
+      } catch (UnsupportedEncodingException e) {
+        // leave it encoded
+      }
+
       while (projectName.endsWith("/")) {
         projectName = projectName.substring(0, projectName.length() - 1);
       }
@@ -308,6 +326,15 @@
     }
   }
 
+  static class DisabledReceiveFactory implements
+      ReceivePackFactory<HttpServletRequest> {
+    @Override
+    public ReceivePack create(HttpServletRequest req, Repository db)
+        throws ServiceNotEnabledException {
+      throw new ServiceNotEnabledException();
+    }
+  }
+
   static class ReceiveFilter implements Filter {
     private final Cache<AdvertisedObjectsCacheKey, Set<ObjectId>> cache;
 
@@ -354,7 +381,7 @@
         return;
       }
 
-      AdvertisedObjectsCacheKey cacheKey = new AdvertisedObjectsCacheKey(
+      AdvertisedObjectsCacheKey cacheKey = AdvertisedObjectsCacheKey.create(
           ((IdentifiedUser) pc.getCurrentUser()).getAccountId(),
           projectName);
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
index b2228a9..ada3ebf 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
@@ -18,12 +18,11 @@
 import com.google.gerrit.audit.AuditEvent;
 import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -47,7 +46,6 @@
   protected HttpLogoutServlet(final AuthConfig authConfig,
       final DynamicItem<WebSession> webSession,
       @CanonicalWebUrl @Nullable final Provider<String> urlProvider,
-      final AccountManager accountManager,
       final AuditService audit) {
     this.webSession = webSession;
     this.urlProvider = urlProvider;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/LoginUrlToken.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/LoginUrlToken.java
index 044c18c..6c17c87 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/LoginUrlToken.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/LoginUrlToken.java
@@ -32,4 +32,7 @@
       return CharMatcher.is('/').trimLeadingFrom(Url.decode(encodedToken));
     }
   }
+
+  private LoginUrlToken() {
+  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
index b9b5731..8c469a9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
@@ -16,7 +16,7 @@
 
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.server.AccessPath;
@@ -183,7 +183,7 @@
   }
 
   private String encoding(HttpServletRequest req) {
-    return Objects.firstNonNull(req.getCharacterEncoding(), "UTF-8");
+    return MoreObjects.firstNonNull(req.getCharacterEncoding(), "UTF-8");
   }
 
   class Response extends HttpServletResponseWrapper {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProxyProperties.java
similarity index 78%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProxyProperties.java
index 407b7c7..67b97c4 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProxyProperties.java
@@ -12,10 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.httpd;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+import java.net.URL;
+
+public interface ProxyProperties {
+  URL getProxyUrl();
+  String getUsername();
+  String getPassword();
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProxyPropertiesProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProxyPropertiesProvider.java
new file mode 100644
index 0000000..0e51cc2
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProxyPropertiesProvider.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+@Singleton
+class ProxyPropertiesProvider implements Provider<ProxyProperties> {
+
+  private URL proxyUrl;
+  private String proxyUser;
+  private String proxyPassword;
+
+  @Inject
+  ProxyPropertiesProvider(@GerritServerConfig Config config)
+      throws MalformedURLException {
+    String proxyUrlStr = config.getString("http", null, "proxy");
+    if (!Strings.isNullOrEmpty(proxyUrlStr)) {
+      proxyUrl = new URL(proxyUrlStr);
+      proxyUser = config.getString("http", null, "proxyUsername");
+      proxyPassword = config.getString("http", null, "proxyPassword");
+      String userInfo = proxyUrl.getUserInfo();
+      if (userInfo != null) {
+        int c = userInfo.indexOf(':');
+        if (0 < c) {
+          proxyUser = userInfo.substring(0, c);
+          proxyPassword = userInfo.substring(c + 1);
+        } else {
+          proxyUser = userInfo;
+        }
+      }
+    }
+  }
+
+  @Override
+  public ProxyProperties get() {
+    return new ProxyProperties() {
+      @Override
+      public URL getProxyUrl() {
+        return proxyUrl;
+      }
+      @Override
+      public String getUsername() {
+        return proxyUser;
+      }
+      @Override
+      public String getPassword() {
+        return proxyPassword;
+      }
+    };
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
index 3418354..614184a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.httpd;
 
+import static com.google.gerrit.httpd.restapi.RestApiServlet.replyError;
 import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
 
 import com.google.gerrit.extensions.registration.DynamicItem;
-import com.google.gerrit.httpd.restapi.RestApiServlet;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountResolver;
@@ -77,17 +77,19 @@
     String runas = req.getHeader(RUN_AS);
     if (runas != null) {
       if (!enabled) {
-        RestApiServlet.replyError(req, res,
+        replyError(req, res,
             SC_FORBIDDEN,
-            RUN_AS + " disabled by auth.enableRunAs = false");
+            RUN_AS + " disabled by auth.enableRunAs = false",
+            null);
         return;
       }
 
       CurrentUser self = session.get().getCurrentUser();
       if (!self.getCapabilities().canRunAs()) {
-        RestApiServlet.replyError(req, res,
+        replyError(req, res,
             SC_FORBIDDEN,
-            "not permitted to use " + RUN_AS);
+            "not permitted to use " + RUN_AS,
+            null);
         return;
       }
 
@@ -96,15 +98,17 @@
         target = accountResolver.find(runas);
       } catch (OrmException e) {
         log.warn("cannot resolve account for " + RUN_AS, e);
-        RestApiServlet.replyError(req, res,
+        replyError(req, res,
             SC_INTERNAL_SERVER_ERROR,
-            "cannot resolve " + RUN_AS);
+            "cannot resolve " + RUN_AS,
+            e);
         return;
       }
       if (target == null) {
-        RestApiServlet.replyError(req, res,
+        replyError(req, res,
             SC_FORBIDDEN,
-            "no account matches " + RUN_AS);
+            "no account matches " + RUN_AS,
+            null);
         return;
       }
       session.get().setUserAccountId(target.getId());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index 031e3a2..86debdd 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.httpd.rpc.access.AccessRestApiServlet;
 import com.google.gerrit.httpd.rpc.account.AccountsRestApiServlet;
 import com.google.gerrit.httpd.rpc.change.ChangesRestApiServlet;
-import com.google.gerrit.httpd.rpc.change.DeprecatedChangeQueryServlet;
 import com.google.gerrit.httpd.rpc.config.ConfigRestApiServlet;
 import com.google.gerrit.httpd.rpc.doc.QueryDocumentationFilter;
 import com.google.gerrit.httpd.rpc.group.GroupsRestApiServlet;
@@ -37,15 +36,12 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AuthConfig;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gwtexpui.server.CacheControlFilter;
-import com.google.inject.Inject;
 import com.google.inject.Key;
 import com.google.inject.Provider;
 import com.google.inject.internal.UniqueAnnotations;
 import com.google.inject.servlet.ServletModule;
 
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
 
 import java.io.IOException;
@@ -55,22 +51,11 @@
 import javax.servlet.http.HttpServletResponse;
 
 class UrlModule extends ServletModule {
-  static class UrlConfig {
-    private final boolean deprecatedQuery;
-
-    @Inject
-    UrlConfig(@GerritServerConfig Config cfg) {
-      deprecatedQuery = cfg.getBoolean("site", "enableDeprecatedQuery", true);
-    }
-  }
-
-  private final UrlConfig cfg;
-  private GerritUiOptions uiOptions;
+  private GerritOptions options;
   private AuthConfig authConfig;
 
-  UrlModule(UrlConfig cfg, GerritUiOptions uiOptions, AuthConfig authConfig) {
-    this.cfg = cfg;
-    this.uiOptions = uiOptions;
+  UrlModule(GerritOptions options, AuthConfig authConfig) {
+    this.options = options;
     this.authConfig = authConfig;
   }
 
@@ -79,7 +64,7 @@
     filter("/*").through(Key.get(CacheControlFilter.class));
     bind(Key.get(CacheControlFilter.class)).in(SINGLETON);
 
-    if (uiOptions.enableDefaultUi()) {
+    if (options.enableDefaultUi()) {
       serve("/").with(HostPageServlet.class);
       serve("/Gerrit").with(LegacyGerritServlet.class);
       serve("/Gerrit/*").with(legacyGerritScreen());
@@ -121,10 +106,6 @@
 
     filter("/Documentation/").through(QueryDocumentationFilter.class);
 
-    if (cfg.deprecatedQuery) {
-      serve("/query").with(DeprecatedChangeQueryServlet.class);
-    }
-
     serve("/robots.txt").with(RobotsServlet.class);
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
index 76e1e41..7831161 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.httpd;
 
 import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
-import static com.google.inject.Scopes.SINGLETON;
 
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GerritConfig;
@@ -32,8 +31,6 @@
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritRequestModule;
-import com.google.gerrit.server.contact.ContactStore;
-import com.google.gerrit.server.contact.ContactStoreProvider;
 import com.google.gerrit.server.git.AsyncReceiveCommits;
 import com.google.gerrit.server.util.GuiceRequestScopePropagator;
 import com.google.gerrit.server.util.RequestScopePropagator;
@@ -47,21 +44,18 @@
 
 public class WebModule extends LifecycleModule {
   private final AuthConfig authConfig;
-  private final UrlModule.UrlConfig urlConfig;
   private final boolean wantSSL;
   private final GitWebConfig gitWebConfig;
-  private final GerritUiOptions uiOptions;
+  private final GerritOptions options;
 
   @Inject
   WebModule(final AuthConfig authConfig,
-      final UrlModule.UrlConfig urlConfig,
       @CanonicalWebUrl @Nullable final String canonicalUrl,
-      GerritUiOptions uiOptions,
+      GerritOptions options,
       final Injector creatingInjector) {
     this.authConfig = authConfig;
-    this.urlConfig = urlConfig;
     this.wantSSL = canonicalUrl != null && canonicalUrl.startsWith("https:");
-    this.uiOptions = uiOptions;
+    this.options = options;
 
     this.gitWebConfig =
         creatingInjector.createChildInjector(new AbstractModule() {
@@ -82,6 +76,34 @@
     }
     install(new RunAsFilter.Module());
 
+    installAuthModule();
+    if (options.enableMasterFeatures()) {
+      install(new UrlModule(options, authConfig));
+      install(new UiRpcModule());
+    }
+    install(new GerritRequestModule());
+    install(new GitOverHttpServlet.Module(options.enableMasterFeatures()));
+
+    bind(GitWebConfig.class).toInstance(gitWebConfig);
+    if (gitWebConfig.getGitwebCGI() != null) {
+      install(new GitWebModule());
+    }
+
+    bind(GerritConfigProvider.class);
+    bind(GerritConfig.class).toProvider(GerritConfigProvider.class);
+    DynamicSet.setOf(binder(), WebUiPlugin.class);
+
+    install(new AsyncReceiveCommits.Module());
+
+    bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
+        HttpRemotePeerProvider.class).in(RequestScoped.class);
+
+    bind(ProxyProperties.class).toProvider(ProxyPropertiesProvider.class);
+
+    listener().toInstance(registerInParentInjectors());
+  }
+
+  private void installAuthModule() {
     switch (authConfig.getAuthType()) {
       case HTTP:
       case HTTP_LDAP:
@@ -111,28 +133,5 @@
       default:
         throw new ProvisionException("Unsupported loginType: " + authConfig.getAuthType());
     }
-
-    install(new UrlModule(urlConfig, uiOptions, authConfig));
-    install(new UiRpcModule());
-    install(new GerritRequestModule());
-    install(new GitOverHttpServlet.Module());
-
-    bind(GitWebConfig.class).toInstance(gitWebConfig);
-    if (gitWebConfig.getGitwebCGI() != null) {
-      install(new GitWebModule());
-    }
-
-    bind(ContactStore.class).toProvider(ContactStoreProvider.class).in(
-        SINGLETON);
-    bind(GerritConfigProvider.class);
-    bind(GerritConfig.class).toProvider(GerritConfigProvider.class);
-    DynamicSet.setOf(binder(), WebUiPlugin.class);
-
-    install(new AsyncReceiveCommits.Module());
-
-    bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
-        HttpRemotePeerProvider.class).in(RequestScoped.class);
-
-    listener().toInstance(registerInParentInjectors());
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
index 24e2c56..5db620c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd;
 
+import static com.google.gerrit.common.TimeUtil.nowMs;
 import static com.google.gerrit.httpd.CacheBasedWebSession.MAX_AGE_MINUTES;
 import static com.google.gerrit.server.ioutil.BasicSerialization.readFixInt64;
 import static com.google.gerrit.server.ioutil.BasicSerialization.readString;
@@ -22,7 +23,6 @@
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeFixInt64;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeString;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
-import static com.google.gerrit.server.util.TimeUtil.nowMs;
 import static java.util.concurrent.TimeUnit.HOURS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index 8884b4d..b8a8092 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -46,7 +46,6 @@
 import java.util.List;
 import java.util.UUID;
 
-import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
@@ -66,7 +65,6 @@
   BecomeAnyAccountLoginServlet(final DynamicItem<WebSession> ws,
       final SchemaFactory<ReviewDb> sf,
       final AccountManager am,
-      final ServletContext servletContext,
       SiteHeaderFooter shf) {
     webSession = ws;
     schema = sf;
@@ -90,13 +88,13 @@
       res = create();
 
     } else if (req.getParameter("user_name") != null) {
-      res = byUserName(rsp, req.getParameter("user_name"));
+      res = byUserName(req.getParameter("user_name"));
 
     } else if (req.getParameter("preferred_email") != null) {
-      res = byPreferredEmail(rsp, req.getParameter("preferred_email"));
+      res = byPreferredEmail(req.getParameter("preferred_email"));
 
     } else if (req.getParameter("account_id") != null) {
-      res = byAccountId(rsp, req.getParameter("account_id"));
+      res = byAccountId(req.getParameter("account_id"));
 
     } else {
       byte[] raw;
@@ -207,8 +205,7 @@
     return null;
   }
 
-  private AuthResult byUserName(final HttpServletResponse rsp,
-      final String userName) {
+  private AuthResult byUserName(final String userName) {
     try {
       final ReviewDb db = schema.open();
       try {
@@ -224,8 +221,7 @@
     }
   }
 
-  private AuthResult byPreferredEmail(final HttpServletResponse rsp,
-      final String email) {
+  private AuthResult byPreferredEmail(final String email) {
     try {
       final ReviewDb db = schema.open();
       try {
@@ -240,8 +236,7 @@
     }
   }
 
-  private AuthResult byAccountId(final HttpServletResponse rsp,
-      final String idStr) {
+  private AuthResult byAccountId(final String idStr) {
     final Account.Id id;
     try {
       id = Account.Id.parse(idStr);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
index b737cd7..19c8342 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.httpd.auth.container;
 
-import static com.google.common.base.Objects.firstNonNull;
+import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Strings.emptyToNull;
 import static com.google.common.net.HttpHeaders.AUTHORIZATION;
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT;
@@ -63,6 +63,7 @@
   private final String loginHeader;
   private final String displaynameHeader;
   private final String emailHeader;
+  private final String externalIdHeader;
 
   @Inject
   HttpAuthFilter(final DynamicItem<WebSession> webSession,
@@ -82,6 +83,7 @@
         AUTHORIZATION);
     displaynameHeader = emptyToNull(authConfig.getHttpDisplaynameHeader());
     emailHeader = emptyToNull(authConfig.getHttpEmailHeader());
+    externalIdHeader = emptyToNull(authConfig.getHttpExternalIdHeader());
   }
 
   @Override
@@ -194,6 +196,14 @@
     }
   }
 
+  String getRemoteExternalIdToken(HttpServletRequest req) {
+    if(externalIdHeader != null) {
+      return emptyToNull(req.getHeader(externalIdHeader));
+    } else {
+      return null;
+    }
+  }
+
   String getLoginHeader() {
     return loginHeader;
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
index 734addb..6d8a0cda 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
@@ -14,18 +14,22 @@
 
 package com.google.gerrit.httpd.auth.container;
 
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_EXTERNAL;
+
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.CanonicalWebUrl;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.httpd.LoginUrlToken;
 import com.google.gerrit.httpd.WebSession;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gwtexpui.server.CacheHeaders;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -122,6 +126,20 @@
       return;
     }
 
+    String remoteExternalId = authFilter.getRemoteExternalIdToken(req);
+    if (remoteExternalId != null) {
+      try {
+        log.debug("Associating external identity \"{}\" to user \"{}\"",
+            remoteExternalId, user);
+        updateRemoteExternalId(arsp, remoteExternalId);
+      } catch (AccountException | OrmException e) {
+        log.error("Unable to associate external identity \"" + remoteExternalId
+            + "\" to user \"" + user + "\"", e);
+        rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
+        return;
+      }
+    }
+
     final StringBuilder rdr = new StringBuilder();
     if (arsp.isNew() && authConfig.getRegisterPageUrl() != null) {
       rdr.append(authConfig.getRegisterPageUrl());
@@ -137,6 +155,15 @@
     rsp.sendRedirect(rdr.toString());
   }
 
+  private void updateRemoteExternalId(AuthResult arsp, String remoteAuthToken)
+      throws AccountException, OrmException {
+    AccountExternalId remoteAuthExtId =
+        new AccountExternalId(arsp.getAccountId(), new AccountExternalId.Key(
+            SCHEME_EXTERNAL, remoteAuthToken));
+    accountManager.updateLink(arsp.getAccountId(),
+        new AuthRequest(remoteAuthExtId.getExternalId()));
+  }
+
   private void replace(Document doc, String name, String value) {
     Element e = HtmlDomUtil.find(doc, name);
     if (e != null) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
index 2deb2eb..f58a719 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.httpd.auth.ldap;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.registration.DynamicItem;
@@ -72,7 +72,7 @@
   private void sendForm(HttpServletRequest req, HttpServletResponse res,
       @Nullable String errorMessage) throws IOException {
     String self = req.getRequestURI();
-    String cancel = Objects.firstNonNull(urlProvider.get(req), "/");
+    String cancel = MoreObjects.firstNonNull(urlProvider.get(req), "/");
     cancel += LoginUrlToken.getToken(req);
 
     Document doc = headers.parse(LdapLoginServlet.class, "LoginForm.html");
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebCssServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebCssServlet.java
index dcca106..4a39b97 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebCssServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebCssServlet.java
@@ -36,8 +36,8 @@
   @Singleton
   static class Site extends GitWebCssServlet {
     @Inject
-    Site(SitePaths paths, GitWebConfig gwc) throws IOException {
-      super(paths.site_css, gwc);
+    Site(SitePaths paths) throws IOException {
+      super(paths.site_css);
     }
   }
 
@@ -45,7 +45,7 @@
   static class Default extends GitWebCssServlet {
     @Inject
     Default(GitWebConfig gwc) throws IOException {
-      super(gwc.getGitwebCSS(), gwc);
+      super(gwc.getGitwebCSS());
     }
   }
 
@@ -55,7 +55,7 @@
   private final byte[] raw_css;
   private final byte[] gz_css;
 
-  GitWebCssServlet(final File src, final GitWebConfig gitWebConfig)
+  GitWebCssServlet(final File src)
       throws IOException {
     if (src != null) {
       final File dir = src.getParentFile();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
index 3b4c998..39b8249 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
@@ -413,7 +413,7 @@
     }
     try {
       CacheHeaders.setNotCacheable(rsp);
-      exec(req, rsp, project, repo);
+      exec(req, rsp, project);
     } finally {
       repo.close();
     }
@@ -451,8 +451,7 @@
   }
 
   private void exec(final HttpServletRequest req,
-      final HttpServletResponse rsp, final ProjectControl project,
-      final Repository repo) throws IOException {
+      final HttpServletResponse rsp, final ProjectControl project) throws IOException {
     final Process proc =
         Runtime.getRuntime().exec(new String[] {gitwebCgi.getAbsolutePath()},
             makeEnv(req, project),
@@ -611,6 +610,7 @@
     final int contentLength = req.getContentLength();
     final InputStream src = req.getInputStream();
     new Thread(new Runnable() {
+      @Override
       public void run() {
         try {
           try {
@@ -637,6 +637,7 @@
 
   private void copyStderrToLog(final InputStream in) {
     new Thread(new Runnable() {
+      @Override
       public void run() {
         try {
           final BufferedReader br =
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
index d1c617f..0e81a0d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
@@ -14,14 +14,18 @@
 
 package com.google.gerrit.httpd.plugins;
 
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.plugins.AutoRegisterUtil.calculateBindAnnotation;
 
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
 import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.webui.JavaScriptPlugin;
+import com.google.gerrit.extensions.webui.WebUiPlugin;
+import com.google.gerrit.server.plugins.HttpModuleGenerator;
 import com.google.gerrit.server.plugins.InvalidPluginException;
-import com.google.gerrit.server.plugins.ModuleGenerator;
 import com.google.inject.Module;
 import com.google.inject.Scopes;
 import com.google.inject.TypeLiteral;
@@ -33,9 +37,10 @@
 import javax.servlet.http.HttpServlet;
 
 class HttpAutoRegisterModuleGenerator extends ServletModule
-    implements ModuleGenerator {
+    implements HttpModuleGenerator {
   private final Map<String, Class<HttpServlet>> serve = Maps.newHashMap();
   private final Multimap<TypeLiteral<?>, Class<?>> listeners = LinkedListMultimap.create();
+  private String javascript;
 
   @Override
   protected void configureServlets() {
@@ -53,6 +58,10 @@
       Annotation n = calculateBindAnnotation(impl);
       bind(type).annotatedWith(n).to(impl);
     }
+    if (javascript != null) {
+      DynamicSet.bind(binder(), WebUiPlugin.class).toInstance(
+          new JavaScriptPlugin(javascript));
+    }
   }
 
   @Override
@@ -80,6 +89,14 @@
   }
 
   @Override
+  public void export(String javascript) {
+    checkState(this.javascript == null,
+        "Multiple JavaScript plugins detected: %s, %s", this.javascript,
+        javascript);
+    this.javascript = javascript;
+  }
+
+  @Override
   public void listen(TypeLiteral<?> tl, Class<?> clazz) {
     listeners.put(tl, clazz);
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
index 5dc7e2e..2016942 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
@@ -14,8 +14,11 @@
 
 package com.google.gerrit.httpd.plugins;
 
+import com.google.gerrit.httpd.resources.Resource;
+import com.google.gerrit.httpd.resources.ResourceKey;
+import com.google.gerrit.httpd.resources.ResourceWeigher;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.plugins.ModuleGenerator;
+import com.google.gerrit.server.plugins.HttpModuleGenerator;
 import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.plugins.StartPluginListener;
 import com.google.inject.internal.UniqueAnnotations;
@@ -37,7 +40,7 @@
       .annotatedWith(UniqueAnnotations.create())
       .to(HttpPluginServlet.class);
 
-    bind(ModuleGenerator.class)
+    bind(HttpModuleGenerator.class)
       .to(HttpAutoRegisterModuleGenerator.class);
 
     install(new CacheModule() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index 5886636..64b754d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -26,11 +26,13 @@
 import com.google.common.collect.Maps;
 import com.google.common.net.HttpHeaders;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.httpd.resources.Resource;
+import com.google.gerrit.httpd.resources.ResourceKey;
+import com.google.gerrit.httpd.resources.SmallResource;
 import com.google.gerrit.httpd.restapi.RestApiServlet;
-import com.google.gerrit.server.MimeUtilFileTypeRegistry;
 import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.documentation.MarkdownFormatter;
+import com.google.gerrit.server.mime.MimeUtilFileTypeRegistry;
 import com.google.gerrit.server.plugins.Plugin;
 import com.google.gerrit.server.plugins.Plugin.ApiType;
 import com.google.gerrit.server.plugins.PluginContentScanner;
@@ -39,6 +41,7 @@
 import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.plugins.StartPluginListener;
 import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gerrit.util.http.RequestUtil;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -46,7 +49,6 @@
 import com.google.inject.name.Named;
 import com.google.inject.servlet.GuiceFilter;
 
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.RawParseUtils;
 import org.slf4j.Logger;
@@ -105,7 +107,6 @@
       MimeUtilFileTypeRegistry mimeUtil,
       @CanonicalWebUrl Provider<String> webUrl,
       @Named(HttpPluginModule.PLUGIN_RESOURCES) Cache<ResourceKey, Resource> cache,
-      @GerritServerConfig Config cfg,
       SshInfo sshInfo,
       RestApiServlet.Globals globals,
       PluginsCollection plugins) {
@@ -205,7 +206,7 @@
       throws IOException, ServletException {
     List<String> parts = Lists.newArrayList(
       Splitter.on('/').limit(3).omitEmptyStrings()
-        .split(Strings.nullToEmpty(req.getPathInfo())));
+        .split(Strings.nullToEmpty(RequestUtil.getEncodedPathInfo(req))));
 
     if (isApiCall(req, parts)) {
       managerApi.service(req, res);
@@ -252,14 +253,14 @@
       return;
     }
 
-    String pathInfo = req.getPathInfo();
+    String pathInfo = RequestUtil.getEncodedPathInfo(req);
     if (pathInfo.length() < 1) {
       Resource.NOT_FOUND.send(req, res);
       return;
     }
 
     String file = pathInfo.substring(1);
-    ResourceKey key = new ResourceKey(holder.plugin, file);
+    PluginResourceKey key = PluginResourceKey.create(holder.plugin, file);
     Resource rsc = resourceCache.getIfPresent(key);
     if (rsc != null && req.getHeader(HttpHeaders.IF_MODIFIED_SINCE) == null) {
       rsc.send(req, res);
@@ -364,7 +365,7 @@
 
   private void sendAutoIndex(PluginContentScanner scanner,
       String prefix, String pluginName,
-      ResourceKey cacheKey, HttpServletResponse res,long lastModifiedTime)
+      PluginResourceKey cacheKey, HttpServletResponse res,long lastModifiedTime)
       throws IOException {
     List<PluginEntry> cmds = Lists.newArrayList();
     List<PluginEntry> servlets = Lists.newArrayList();
@@ -437,7 +438,7 @@
   }
 
   private void sendMarkdownAsHtml(String md, String pluginName,
-      ResourceKey cacheKey, HttpServletResponse res, long lastModifiedTime)
+      PluginResourceKey cacheKey, HttpServletResponse res, long lastModifiedTime)
       throws UnsupportedEncodingException, IOException {
     Map<String, String> macros = Maps.newHashMap();
     macros.put("PLUGIN", pluginName);
@@ -540,7 +541,7 @@
   }
 
   private void sendMarkdownAsHtml(PluginContentScanner scanner, PluginEntry entry,
-      String pluginName, ResourceKey key, HttpServletResponse res)
+      String pluginName, PluginResourceKey key, HttpServletResponse res)
       throws IOException {
     byte[] rawmd = readWholeEntry(scanner, entry);
     String encoding = null;
@@ -560,7 +561,7 @@
   }
 
   private void sendResource(PluginContentScanner scanner, PluginEntry entry,
-      ResourceKey key, HttpServletResponse res)
+      PluginResourceKey key, HttpServletResponse res)
       throws IOException {
     byte[] data = null;
     Optional<Long> size = entry.getSize();
@@ -608,7 +609,7 @@
     }
   }
 
-  private void sendJsPlugin(Plugin plugin, ResourceKey key,
+  private void sendJsPlugin(Plugin plugin, PluginResourceKey key,
       HttpServletRequest req, HttpServletResponse res) throws IOException {
     File pluginFile = plugin.getSrcFile();
     if (req.getRequestURI().endsWith(getJsPluginPath(plugin))
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginResourceKey.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginResourceKey.java
new file mode 100644
index 0000000..0a15a4f
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginResourceKey.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.plugins;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.httpd.resources.ResourceKey;
+import com.google.gerrit.server.plugins.Plugin;
+
+@AutoValue
+abstract class PluginResourceKey implements ResourceKey {
+  static PluginResourceKey create(Plugin p, String r) {
+    return new AutoValue_PluginResourceKey(p.getCacheKey(), r);
+  }
+
+  public abstract Plugin.CacheKey plugin();
+  public abstract String resource();
+
+  @Override
+  public int weigh() {
+    return resource().length() * 2;
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceKey.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceKey.java
deleted file mode 100644
index 068d6b4..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceKey.java
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.plugins;
-
-import com.google.gerrit.server.plugins.Plugin;
-
-final class ResourceKey {
-  private final Plugin.CacheKey plugin;
-  private final String resource;
-
-  ResourceKey(Plugin p, String r) {
-    this.plugin = p.getCacheKey();
-    this.resource = r;
-  }
-
-  int weigh() {
-    return resource.length() * 2;
-  }
-
-  @Override
-  public int hashCode() {
-    return plugin.hashCode() * 31 + resource.hashCode();
-  }
-
-  @Override
-  public boolean equals(Object other) {
-    if (other instanceof ResourceKey) {
-      ResourceKey rk = (ResourceKey) other;
-      return plugin == rk.plugin && resource.equals(rk.resource);
-    }
-    return false;
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
index 9aec609..72fc008 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
@@ -14,17 +14,22 @@
 
 package com.google.gerrit.httpd.raw;
 
+import com.google.common.base.Optional;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.FileTypeRegistry;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.edit.ChangeEdit;
+import com.google.gerrit.server.edit.ChangeEditUtil;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.mime.FileTypeRegistry;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -73,16 +78,24 @@
   private final GitRepositoryManager repoManager;
   private final SecureRandom rng;
   private final FileTypeRegistry registry;
-  private final ChangeControl.Factory changeControl;
+  private final Provider<CurrentUser> userProvider;
+  private final ChangeControl.GenericFactory changeControl;
+  private final ChangeEditUtil changeEditUtil;
 
   @Inject
-  CatServlet(final GitRepositoryManager grm, final Provider<ReviewDb> sf,
-      final FileTypeRegistry ftr, final ChangeControl.Factory ccf) {
+  CatServlet(GitRepositoryManager grm,
+      Provider<ReviewDb> sf,
+      FileTypeRegistry ftr,
+      ChangeControl.GenericFactory ccf,
+      Provider<CurrentUser> usrprv,
+      ChangeEditUtil ceu) {
     requestDb = sf;
     repoManager = grm;
     rng = new SecureRandom();
     registry = ftr;
     changeControl = ccf;
+    userProvider = usrprv;
+    changeEditUtil = ceu;
   }
 
   @Override
@@ -137,16 +150,35 @@
 
     final Change.Id changeId = patchKey.getParentKey().getParentKey();
     final Project project;
-    final PatchSet patchSet;
+    final String revision;
     try {
       final ReviewDb db = requestDb.get();
-      final ChangeControl control = changeControl.validateFor(changeId);
+      final ChangeControl control = changeControl.validateFor(changeId,
+          userProvider.get());
 
       project = control.getProject();
-      patchSet = db.patchSets().get(patchKey.getParentKey());
-      if (patchSet == null) {
-        rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
-        return;
+
+      if (patchKey.getParentKey().get() == 0) {
+        // change edit
+        try {
+          Optional<ChangeEdit> edit = changeEditUtil.byChange(control.getChange());
+          if (edit.isPresent()) {
+            revision = edit.get().getRevision().get();
+          } else {
+            rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return;
+          }
+        } catch (AuthException e) {
+          rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
+          return;
+        }
+      } else {
+        PatchSet patchSet = db.patchSets().get(patchKey.getParentKey());
+        if (patchSet == null) {
+          rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
+          return;
+        }
+        revision = patchSet.getRevision().get();
       }
     } catch (NoSuchChangeException e) {
       rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
@@ -170,32 +202,29 @@
     final RevCommit fromCommit;
     final String suffix;
     final String path = patchKey.getFileName();
-    try {
-      final ObjectReader reader = repo.newObjectReader();
-      try {
-        final RevWalk rw = new RevWalk(reader);
-        final RevCommit c;
-        final TreeWalk tw;
+    try (ObjectReader reader = repo.newObjectReader();
+        RevWalk rw = new RevWalk(reader)) {
+      final RevCommit c;
 
-        c = rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get()));
-        if (side == 0) {
-          fromCommit = c;
-          suffix = "new";
+      c = rw.parseCommit(ObjectId.fromString(revision));
+      if (side == 0) {
+        fromCommit = c;
+        suffix = "new";
 
-        } else if (1 <= side && side - 1 < c.getParentCount()) {
-          fromCommit = rw.parseCommit(c.getParent(side - 1));
-          if (c.getParentCount() == 1) {
-            suffix = "old";
-          } else {
-            suffix = "old" + side;
-          }
-
+      } else if (1 <= side && side - 1 < c.getParentCount()) {
+        fromCommit = rw.parseCommit(c.getParent(side - 1));
+        if (c.getParentCount() == 1) {
+          suffix = "old";
         } else {
-          rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
-          return;
+          suffix = "old" + side;
         }
 
-        tw = TreeWalk.forPath(reader, path, fromCommit.getTree());
+      } else {
+        rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
+        return;
+      }
+
+      try (TreeWalk tw = TreeWalk.forPath(reader, path, fromCommit.getTree())) {
         if (tw == null) {
           rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
           return;
@@ -208,8 +237,6 @@
           rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
           return;
         }
-      } finally {
-        reader.release();
       }
     } catch (IOException e) {
       getServletContext().log("Cannot read repository", e);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index 18a2ae5..90c5ff4 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.gwtjsonrpc.server.JsonServlet;
 import com.google.gwtjsonrpc.server.RPCServletUtils;
@@ -82,6 +83,7 @@
   private final String noCacheName;
   private final boolean refreshHeaderFooter;
   private final StaticServlet staticServlet;
+  private final boolean isNoteDbEnabled;
   private volatile Page page;
 
   @Inject
@@ -91,7 +93,8 @@
       final DynamicSet<WebUiPlugin> webUiPlugins,
       final DynamicSet<MessageOfTheDay> motd,
       @GerritServerConfig final Config cfg,
-      final StaticServlet ss)
+      final StaticServlet ss,
+      final NotesMigration migration)
       throws IOException, ServletException {
     currentUser = cu;
     session = w;
@@ -103,6 +106,7 @@
     site = sp;
     refreshHeaderFooter = cfg.getBoolean("site", "refreshHeaderFooter", true);
     staticServlet = ss;
+    isNoteDbEnabled = migration.enabled();
 
     final String pageName = "HostPage.html";
     template = HtmlDomUtil.parseFile(getClass(), pageName);
@@ -314,6 +318,7 @@
       final HostPageData pageData = new HostPageData();
       pageData.version = Version.getVersion();
       pageData.config = config;
+      pageData.isNoteDbEnabled = isNoteDbEnabled;
 
       final StringWriter w = new StringWriter();
       w.write("var " + HPD_ID + "=");
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java
index 79c2aeb..9c267a8 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java
@@ -24,7 +24,6 @@
 import java.io.IOException;
 import java.io.OutputStream;
 
-import javax.servlet.ServletContext;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -45,7 +44,7 @@
   private final byte[] compressed;
 
   @Inject
-  LegacyGerritServlet(final ServletContext servletContext) throws IOException {
+  LegacyGerritServlet() throws IOException {
     final String pageName = "LegacyGerrit.html";
     final String doc = HtmlDomUtil.readFile(getClass(), pageName);
     if (doc == null) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ToolServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ToolServlet.java
index 810cc2a..16509ed 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ToolServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ToolServlet.java
@@ -61,7 +61,7 @@
 
     switch (ent.getType()) {
       case FILE:
-        doGetFile(ent, req, rsp);
+        doGetFile(ent, rsp);
         break;
 
       case DIR:
@@ -74,8 +74,7 @@
     }
   }
 
-  private void doGetFile(Entry ent, HttpServletRequest req,
-      HttpServletResponse rsp) throws IOException {
+  private void doGetFile(Entry ent, HttpServletResponse rsp) throws IOException {
     byte[] tosend = ent.getBytes();
 
     rsp.setDateHeader(HDR_EXPIRES, 0L);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/Resource.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/resources/Resource.java
similarity index 60%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/Resource.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/resources/Resource.java
index b361fdc..b6d9a75 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/Resource.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/resources/Resource.java
@@ -12,39 +12,48 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd.plugins;
+package com.google.gerrit.httpd.resources;
 
 import com.google.gwtexpui.server.CacheHeaders;
 
 import java.io.IOException;
+import java.io.Serializable;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-abstract class Resource {
-  static final Resource NOT_FOUND = new Resource() {
+public abstract class Resource implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  public static final Resource NOT_FOUND = new Resource() {
+    private static final long serialVersionUID = 1L;
+
     @Override
-    int weigh() {
+    public int weigh() {
       return 0;
     }
 
     @Override
-    void send(HttpServletRequest req, HttpServletResponse res)
+    public void send(HttpServletRequest req, HttpServletResponse res)
         throws IOException {
       CacheHeaders.setNotCacheable(res);
       res.sendError(HttpServletResponse.SC_NOT_FOUND);
     }
 
     @Override
-    boolean isUnchanged(long latestModifiedDate) {
+    public boolean isUnchanged(long latestModifiedDate) {
       return false;
     }
+
+    protected Object readResolve() {
+      return NOT_FOUND;
+    }
   };
 
-  abstract boolean isUnchanged(long latestModifiedDate);
+  public abstract boolean isUnchanged(long latestModifiedDate);
 
-  abstract int weigh();
+  public abstract int weigh();
 
-  abstract void send(HttpServletRequest req, HttpServletResponse res)
+  public abstract void send(HttpServletRequest req, HttpServletResponse res)
       throws IOException;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/resources/ResourceKey.java
similarity index 84%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-httpd/src/main/java/com/google/gerrit/httpd/resources/ResourceKey.java
index 407b7c7..ef5a1df 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/resources/ResourceKey.java
@@ -12,10 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.httpd.resources;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+public interface ResourceKey {
+  public int weigh();
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/resources/ResourceWeigher.java
similarity index 86%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/resources/ResourceWeigher.java
index 2514272..8e997c7 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/resources/ResourceWeigher.java
@@ -12,11 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd.plugins;
+package com.google.gerrit.httpd.resources;
 
 import com.google.common.cache.Weigher;
 
-class ResourceWeigher implements Weigher<ResourceKey, Resource> {
+public class ResourceWeigher implements Weigher<ResourceKey, Resource> {
   @Override
   public int weigh(ResourceKey key, Resource value) {
     return key.weigh() + value.weigh();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/resources/SmallResource.java
similarity index 78%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/resources/SmallResource.java
index 2a3da57..d057269 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/resources/SmallResource.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd.plugins;
+package com.google.gerrit.httpd.resources;
 
 import com.google.common.net.HttpHeaders;
 import com.google.gerrit.common.Nullable;
@@ -22,38 +22,39 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-final class SmallResource extends Resource {
+public final class SmallResource extends Resource {
+  private static final long serialVersionUID = 1L;
   private final byte[] data;
   private String contentType;
   private String characterEncoding;
   private long lastModified;
 
-  SmallResource(byte[] data) {
+  public SmallResource(byte[] data) {
     this.data = data;
   }
 
-  SmallResource setLastModified(long when) {
+  public SmallResource setLastModified(long when) {
     this.lastModified = when;
     return this;
   }
 
-  SmallResource setContentType(String contentType) {
+  public SmallResource setContentType(String contentType) {
     this.contentType = contentType;
     return this;
   }
 
-  SmallResource setCharacterEncoding(@Nullable String enc) {
+  public SmallResource setCharacterEncoding(@Nullable String enc) {
     this.characterEncoding = enc;
     return this;
   }
 
   @Override
-  int weigh() {
+  public int weigh() {
     return contentType.length() * 2 + data.length;
   }
 
   @Override
-  void send(HttpServletRequest req, HttpServletResponse res)
+  public void send(HttpServletRequest req, HttpServletResponse res)
       throws IOException {
     if (0 < lastModified) {
       long ifModifiedSince = req.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE);
@@ -73,7 +74,7 @@
   }
 
   @Override
-  boolean isUnchanged(long lastModified) {
+  public boolean isUnchanged(long lastModified) {
     return this.lastModified == lastModified;
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
index 9183d5c..46843fc 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
@@ -68,7 +68,7 @@
       clp.parseOptionMap(in);
     } catch (CmdLineException e) {
       if (!clp.wasHelpRequestedByOption()) {
-        replyError(req, res, SC_BAD_REQUEST, e.getMessage());
+        replyError(req, res, SC_BAD_REQUEST, e.getMessage(), e);
         return false;
       }
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index b7528b5..6f4cc08 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -25,13 +25,14 @@
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
 import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_IMPLEMENTED;
 import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
+import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
 
 import com.google.common.base.Function;
 import com.google.common.base.Joiner;
-import com.google.common.base.Objects;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMultimap;
@@ -47,17 +48,21 @@
 import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.audit.HttpAuditEvent;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AcceptsCreate;
+import com.google.gerrit.extensions.restapi.AcceptsDelete;
 import com.google.gerrit.extensions.restapi.AcceptsPost;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.CacheControl;
 import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.ETagView;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.PreconditionFailedException;
 import com.google.gerrit.extensions.restapi.RawInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -78,7 +83,7 @@
 import com.google.gerrit.server.OptionUtil;
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.account.CapabilityUtils;
-import com.google.gerrit.server.util.TimeUtil;
+import com.google.gerrit.util.http.RequestUtil;
 import com.google.gson.ExclusionStrategy;
 import com.google.gson.FieldAttributes;
 import com.google.gson.FieldNamingPolicy;
@@ -89,6 +94,7 @@
 import com.google.gson.JsonPrimitive;
 import com.google.gson.stream.JsonReader;
 import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
 import com.google.gson.stream.MalformedJsonException;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
@@ -103,6 +109,7 @@
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
 import java.io.EOFException;
+import java.io.FilterOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -221,7 +228,7 @@
         try {
           rsrc = rc.parse(rsrc, id);
           if (path.isEmpty()) {
-            checkPreconditions(req, rsrc);
+            checkPreconditions(req);
           }
         } catch (ResourceNotFoundException e) {
           if (rc instanceof AcceptsCreate
@@ -254,6 +261,10 @@
             @SuppressWarnings("unchecked")
             AcceptsPost<RestResource> ac = (AcceptsPost<RestResource>) c;
             viewData = new ViewData(null, ac.post(rsrc));
+          } else if (c instanceof AcceptsDelete && "DELETE".equals(req.getMethod())) {
+            @SuppressWarnings("unchecked")
+            AcceptsDelete<RestResource> ac = (AcceptsDelete<RestResource>) c;
+            viewData = new ViewData(null, ac.delete(rsrc, null));
           } else {
             throw new MethodNotAllowedException();
           }
@@ -262,7 +273,7 @@
           IdString id = path.remove(0);
           try {
             rsrc = c.parse(rsrc, id);
-            checkPreconditions(req, rsrc);
+            checkPreconditions(req);
             viewData = new ViewData(null, null);
           } catch (ResourceNotFoundException e) {
             if (c instanceof AcceptsCreate
@@ -273,6 +284,13 @@
               AcceptsCreate<RestResource> ac = (AcceptsCreate<RestResource>) c;
               viewData = new ViewData(viewData.pluginName, ac.create(rsrc, id));
               status = SC_CREATED;
+            } else if (c instanceof AcceptsDelete
+                && path.isEmpty()
+                && "DELETE".equals(req.getMethod())) {
+              @SuppressWarnings("unchecked")
+              AcceptsDelete<RestResource> ac = (AcceptsDelete<RestResource>) c;
+              viewData = new ViewData(viewData.pluginName, ac.delete(rsrc, id));
+              status = SC_NO_CONTENT;
             } else {
               throw e;
             }
@@ -284,7 +302,7 @@
         checkRequiresCapability(viewData);
       }
 
-      if (notModified(req, rsrc)) {
+      if (notModified(req, rsrc, viewData.view)) {
         res.sendError(SC_NOT_MODIFIED);
         return;
       }
@@ -330,28 +348,38 @@
           replyJson(req, res, config, result);
         }
       }
-    } catch (AuthException e) {
-      replyError(req, res, status = SC_FORBIDDEN, e.getMessage(), e.caching());
+    } catch (MalformedJsonException e) {
+      replyError(req, res, status = SC_BAD_REQUEST,
+          "Invalid " + JSON_TYPE + " in request", e);
+    } catch (JsonParseException e) {
+      replyError(req, res, status = SC_BAD_REQUEST,
+          "Invalid " + JSON_TYPE + " in request", e);
     } catch (BadRequestException e) {
-      replyError(req, res, status = SC_BAD_REQUEST, e.getMessage(), e.caching());
+      replyError(req, res, status = SC_BAD_REQUEST, messageOr(e, "Bad Request"),
+          e.caching(), e);
+    } catch (AuthException e) {
+      replyError(req, res, status = SC_FORBIDDEN, messageOr(e, "Forbidden"),
+          e.caching(), e);
+    } catch (AmbiguousViewException e) {
+      replyError(req, res, status = SC_NOT_FOUND, messageOr(e, "Ambiguous"), e);
+    } catch (ResourceNotFoundException e) {
+      replyError(req, res, status = SC_NOT_FOUND, messageOr(e, "Not Found"),
+          e.caching(), e);
     } catch (MethodNotAllowedException e) {
-      replyError(req, res, status = SC_METHOD_NOT_ALLOWED, "Method not allowed", e.caching());
+      replyError(req, res, status = SC_METHOD_NOT_ALLOWED,
+          messageOr(e, "Method Not Allowed"), e.caching(), e);
     } catch (ResourceConflictException e) {
-      replyError(req, res, status = SC_CONFLICT, e.getMessage(), e.caching());
+      replyError(req, res, status = SC_CONFLICT, messageOr(e, "Conflict"),
+          e.caching(), e);
     } catch (PreconditionFailedException e) {
       replyError(req, res, status = SC_PRECONDITION_FAILED,
-          Objects.firstNonNull(e.getMessage(), "Precondition failed"), e.caching());
-    } catch (ResourceNotFoundException e) {
-      replyError(req, res, status = SC_NOT_FOUND, "Not found", e.caching());
+          messageOr(e, "Precondition Failed"), e.caching(), e);
     } catch (UnprocessableEntityException e) {
-      replyError(req, res, status = 422,
-          Objects.firstNonNull(e.getMessage(), "Unprocessable Entity"), e.caching());
-    } catch (AmbiguousViewException e) {
-      replyError(req, res, status = SC_NOT_FOUND, e.getMessage());
-    } catch (MalformedJsonException e) {
-      replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
-    } catch (JsonParseException e) {
-      replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
+      replyError(req, res, status = 422, messageOr(e, "Unprocessable Entity"),
+          e.caching(), e);
+    } catch (NotImplementedException e) {
+      replyError(req, res, status = SC_NOT_IMPLEMENTED,
+          messageOr(e, "Not Implemented"), e);
     } catch (Exception e) {
       status = SC_INTERNAL_SERVER_ERROR;
       handleException(e, req, res);
@@ -363,11 +391,27 @@
     }
   }
 
-  private static boolean notModified(HttpServletRequest req, RestResource rsrc) {
+  private static String messageOr(Throwable t, String defaultMessage) {
+    if (!Strings.isNullOrEmpty(t.getMessage())) {
+      return t.getMessage();
+    }
+    return defaultMessage;
+  }
+
+  @SuppressWarnings({"unchecked", "rawtypes"})
+  private static boolean notModified(HttpServletRequest req, RestResource rsrc,
+      RestView<RestResource> view) {
     if (!isGetOrHead(req)) {
       return false;
     }
 
+    if (view instanceof ETagView) {
+      String have = req.getHeader(HttpHeaders.IF_NONE_MATCH);
+      if (have != null) {
+        return have.equals(((ETagView) view).getETag(rsrc));
+      }
+    }
+
     if (rsrc instanceof RestResource.HasETag) {
       String have = req.getHeader(HttpHeaders.IF_NONE_MATCH);
       if (have != null) {
@@ -425,7 +469,7 @@
     }
   }
 
-  private void checkPreconditions(HttpServletRequest req, RestResource rsrc)
+  private void checkPreconditions(HttpServletRequest req)
       throws PreconditionFailedException {
     if ("*".equals(req.getHeader("If-None-Match"))) {
       throw new PreconditionFailedException("Resource already exists");
@@ -691,6 +735,7 @@
     }
   }
 
+  @SuppressWarnings("resource")
   static void replyBinaryResult(
       @Nullable HttpServletRequest req,
       HttpServletResponse res,
@@ -703,7 +748,11 @@
             "attachment; filename=\"" + bin.getAttachmentName() + "\"");
       }
       if (bin.isBase64()) {
-        bin = stackBase64(res, bin);
+        if (req != null && JSON_TYPE.equals(req.getHeader(HttpHeaders.ACCEPT))) {
+          bin = stackJsonString(res, bin);
+        } else {
+          bin = stackBase64(res, bin);
+        }
       }
       if (bin.canGzip() && acceptsGzip(req)) {
         bin = stackGzip(res, bin);
@@ -730,6 +779,24 @@
     }
   }
 
+  private static BinaryResult stackJsonString(HttpServletResponse res,
+      final BinaryResult src) throws IOException {
+    TemporaryBuffer.Heap buf = heap(Integer.MAX_VALUE);
+    buf.write(JSON_MAGIC);
+    try(Writer w = new BufferedWriter(new OutputStreamWriter(buf, UTF_8));
+        JsonWriter json = new JsonWriter(w)) {
+      json.setLenient(true);
+      json.setHtmlSafe(true);
+      json.value(src.asString());
+      w.write('\n');
+    }
+    res.setHeader("X-FYI-Content-Encoding", "json");
+    res.setHeader("X-FYI-Content-Type", src.getContentType());
+    return asBinaryResult(buf)
+      .setContentType(JSON_TYPE)
+      .setCharacterEncoding(UTF_8.name());
+  }
+
   private static BinaryResult stackBase64(HttpServletResponse res,
       final BinaryResult src) throws IOException {
     BinaryResult b64;
@@ -740,10 +807,16 @@
       b64 = new BinaryResult() {
         @Override
         public void writeTo(OutputStream out) throws IOException {
-          OutputStream e = BaseEncoding.base64().encodingStream(
-              new OutputStreamWriter(out, ISO_8859_1));
-          src.writeTo(e);
-          e.flush();
+          try (OutputStreamWriter w = new OutputStreamWriter(
+                new FilterOutputStream(out) {
+                  @Override
+                  public void close() {
+                    // Do not close out, but only w and e.
+                  }
+                }, ISO_8859_1);
+              OutputStream e = BaseEncoding.base64().encodingStream(w)) {
+            src.writeTo(e);
+          }
         }
       };
     }
@@ -858,7 +931,7 @@
   }
 
   private static List<IdString> splitPath(HttpServletRequest req) {
-    String path = req.getPathInfo();
+    String path = RequestUtil.getEncodedPathInfo(req);
     if (Strings.isNullOrEmpty(path)) {
       return Collections.emptyList();
     }
@@ -914,20 +987,23 @@
 
     if (!res.isCommitted()) {
       res.reset();
-      replyError(req, res, SC_INTERNAL_SERVER_ERROR, "Internal server error");
+      replyError(req, res, SC_INTERNAL_SERVER_ERROR, "Internal server error", err);
     }
   }
 
-  public static void replyError(HttpServletRequest req,
-      HttpServletResponse res, int statusCode, String msg) throws IOException {
-    replyError(req, res, statusCode, msg, CacheControl.NONE);
+  public static void replyError(HttpServletRequest req, HttpServletResponse res,
+      int statusCode, String msg, @Nullable Throwable err) throws IOException {
+    replyError(req, res, statusCode, msg, CacheControl.NONE, err);
   }
 
   public static void replyError(HttpServletRequest req,
       HttpServletResponse res, int statusCode, String msg,
-      CacheControl c) throws IOException {
-    res.setStatus(statusCode);
+      CacheControl c, @Nullable Throwable err) throws IOException {
+    if (err != null) {
+      RequestUtil.setErrorTraceAttribute(req, err);
+    }
     configureCaching(req, res, null, c);
+    res.setStatus(statusCode);
     replyText(req, res, msg);
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/AuditedHttpServletResponse.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/AuditedHttpServletResponse.java
index c0c4535..4696e8d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/AuditedHttpServletResponse.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/AuditedHttpServletResponse.java
@@ -28,6 +28,7 @@
     super(response);
   }
 
+  @SuppressWarnings("all") // @Override for servlet API 3.0+ only.
   public int getStatus() {
     return status;
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
index 32b4958..01f2df3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.Multimap;
 import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.audit.RpcAuditEvent;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gerrit.common.errors.NotSignedInException;
@@ -25,7 +26,6 @@
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gson.GsonBuilder;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
 import com.google.gwtjsonrpc.server.ActiveCall;
@@ -131,7 +131,7 @@
       if (method == null) {
         return;
       }
-      Audit note = (Audit) method.getAnnotation(Audit.class);
+      Audit note = method.getAnnotation(Audit.class);
       if (note != null) {
         final String sid = call.getWebSession().getSessionId();
         final CurrentUser username = call.getWebSession().getCurrentUser();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java
index 911d1cc..19a02a5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java
@@ -103,5 +103,6 @@
    * @throws Exception the operation failed. The caller will log the exception
    *         and the stack trace, if it is worth logging on the server side.
    */
+  @Override
   public abstract T call() throws Exception;
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
index dcd181c..69db233 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
@@ -17,173 +17,42 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.data.ReviewerInfo;
 import com.google.gerrit.common.data.SuggestService;
-import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.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.AccountVisibility;
 import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.account.GroupMembers;
-import com.google.gerrit.server.change.PostReviewers;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.eclipse.jgit.lib.Config;
-
-import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collections;
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
 class SuggestServiceImpl extends BaseServiceImplementation implements
     SuggestService {
-  private static final String MAX_SUFFIX = "\u9fa5";
-
-  private final Provider<ReviewDb> reviewDbProvider;
-  private final AccountCache accountCache;
-  private final GroupMembers.Factory groupMembersFactory;
-  private final IdentifiedUser.GenericFactory identifiedUserFactory;
-  private final AccountControl.Factory accountControlFactory;
-  private final ChangeControl.Factory changeControlFactory;
   private final ProjectControl.Factory projectControlFactory;
-  private final Config cfg;
   private final GroupBackend groupBackend;
-  private final boolean suggestAccounts;
 
   @Inject
   SuggestServiceImpl(final Provider<ReviewDb> schema,
-      final AccountCache accountCache,
-      final GroupMembers.Factory groupMembersFactory,
       final Provider<CurrentUser> currentUser,
-      final IdentifiedUser.GenericFactory identifiedUserFactory,
-      final AccountControl.Factory accountControlFactory,
-      final ChangeControl.Factory changeControlFactory,
       final ProjectControl.Factory projectControlFactory,
-      @GerritServerConfig final Config cfg, final GroupBackend groupBackend) {
+      final GroupBackend groupBackend) {
     super(schema, currentUser);
-    this.reviewDbProvider = schema;
-    this.accountCache = accountCache;
-    this.groupMembersFactory = groupMembersFactory;
-    this.identifiedUserFactory = identifiedUserFactory;
-    this.accountControlFactory = accountControlFactory;
-    this.changeControlFactory = changeControlFactory;
     this.projectControlFactory = projectControlFactory;
-    this.cfg = cfg;
     this.groupBackend = groupBackend;
-
-    if ("OFF".equals(cfg.getString("suggest", null, "accounts"))) {
-      this.suggestAccounts = false;
-    } else {
-      boolean suggestAccounts;
-      try {
-        AccountVisibility av =
-            cfg.getEnum("suggest", null, "accounts", AccountVisibility.ALL);
-        suggestAccounts = (av != AccountVisibility.NONE);
-      } catch (IllegalArgumentException err) {
-        suggestAccounts = cfg.getBoolean("suggest", null, "accounts", true);
-      }
-      this.suggestAccounts = suggestAccounts;
-    }
   }
 
-  private interface VisibilityControl {
-    boolean isVisible(Account account) throws OrmException;
-  }
-
-  public void suggestAccount(final String query, final Boolean active,
-      final int limit, final AsyncCallback<List<AccountInfo>> callback) {
-    run(callback, new Action<List<AccountInfo>>() {
-      public List<AccountInfo> run(final ReviewDb db) throws OrmException {
-        return suggestAccount(db, query, active, limit, new VisibilityControl() {
-          @Override
-          public boolean isVisible(Account account) throws OrmException {
-            return accountControlFactory.get().canSee(account);
-          }
-        });
-      }
-    });
-  }
-
-  private List<AccountInfo> suggestAccount(final ReviewDb db,
-      final String query, final Boolean active, final int limit,
-      VisibilityControl visibilityControl)
-      throws OrmException {
-    if (!suggestAccounts) {
-      return Collections.emptyList();
-    }
-
-    final String a = query;
-    final String b = a + MAX_SUFFIX;
-    final int max = 10;
-    final int n = limit <= 0 ? max : Math.min(limit, max);
-
-    LinkedHashMap<Account.Id, AccountInfo> r = new LinkedHashMap<>();
-    for (final Account p : db.accounts().suggestByFullName(a, b, n)) {
-      addSuggestion(r, p, new AccountInfo(p), active, visibilityControl);
-    }
-    if (r.size() < n) {
-      for (final Account p : db.accounts().suggestByPreferredEmail(a, b,
-          n - r.size())) {
-        addSuggestion(r, p, new AccountInfo(p), active, visibilityControl);
-      }
-    }
-    if (r.size() < n) {
-      for (final AccountExternalId e : db.accountExternalIds()
-          .suggestByEmailAddress(a, b, n - r.size())) {
-        if (!r.containsKey(e.getAccountId())) {
-          final Account p = accountCache.get(e.getAccountId()).getAccount();
-          final AccountInfo info = new AccountInfo(p);
-          info.setPreferredEmail(e.getEmailAddress());
-          addSuggestion(r, p, info, active, visibilityControl);
-        }
-      }
-    }
-    return new ArrayList<>(r.values());
-  }
-
-  private void addSuggestion(Map<Account.Id, AccountInfo> map, Account account,
-      AccountInfo info, Boolean active, VisibilityControl visibilityControl)
-      throws OrmException {
-    if (map.containsKey(account.getId())) {
-      return;
-    }
-    if (active != null && active != account.isActive()) {
-      return;
-    }
-    if (visibilityControl.isVisible(account)) {
-      map.put(account.getId(), info);
-    }
-  }
-
-  public void suggestAccountGroup(final String query, final int limit,
-      final AsyncCallback<List<GroupReference>> callback) {
-    suggestAccountGroupForProject(null, query, limit, callback);
-  }
-
+  @Override
   public void suggestAccountGroupForProject(final Project.NameKey project,
       final String query, final int limit,
       final AsyncCallback<List<GroupReference>> callback) {
     run(callback, new Action<List<GroupReference>>() {
+      @Override
       public List<GroupReference> run(final ReviewDb db) {
         ProjectControl projectControl = null;
         if (project != null) {
@@ -204,102 +73,4 @@
         groupBackend.suggest(query, projectControl),
         limit <= 0 ? 10 : Math.min(limit, 10)));
   }
-
-  @Override
-  public void suggestReviewer(Project.NameKey project, String query, int limit,
-      AsyncCallback<List<ReviewerInfo>> callback) {
-    // The RPC is deprecated, but return an empty list for RPC API compatibility.
-    callback.onSuccess(Collections.<ReviewerInfo>emptyList());
-  }
-
-  @Override
-  public void suggestChangeReviewer(final Change.Id change,
-      final String query, final int limit,
-      final AsyncCallback<List<ReviewerInfo>> callback) {
-    run(callback, new Action<List<ReviewerInfo>>() {
-      public List<ReviewerInfo> run(final ReviewDb db)
-          throws OrmException, Failure {
-        final ChangeControl changeControl;
-        try {
-          changeControl = changeControlFactory.controlFor(change);
-        } catch (NoSuchChangeException e) {
-          return Collections.emptyList();
-        }
-
-        VisibilityControl visibilityControl;
-        if (changeControl.getRefControl().isVisibleByRegisteredUsers()) {
-          visibilityControl = new VisibilityControl() {
-            @Override
-            public boolean isVisible(Account account) throws OrmException {
-              return true;
-            }
-          };
-        } else {
-          visibilityControl = new VisibilityControl() {
-            @Override
-            public boolean isVisible(Account account) throws OrmException {
-              IdentifiedUser who =
-                  identifiedUserFactory.create(reviewDbProvider, account.getId());
-              // we can't use changeControl directly as it won't suggest reviewers
-              // to drafts
-              return changeControl.forUser(who).isRefVisible();
-            }
-          };
-        }
-
-        final List<AccountInfo> suggestedAccounts =
-            suggestAccount(db, query, Boolean.TRUE, limit, visibilityControl);
-        final List<ReviewerInfo> reviewer =
-            new ArrayList<>(suggestedAccounts.size());
-        for (final AccountInfo a : suggestedAccounts) {
-          reviewer.add(new ReviewerInfo(a));
-        }
-        final List<GroupReference> suggestedAccountGroups =
-            suggestAccountGroup(changeControl.getProjectControl(), query, limit);
-        for (final GroupReference g : suggestedAccountGroups) {
-          if (suggestGroupAsReviewer(changeControl.getProject().getNameKey(), g)) {
-            reviewer.add(new ReviewerInfo(g));
-          }
-        }
-
-        Collections.sort(reviewer);
-        if (reviewer.size() <= limit) {
-          return reviewer;
-        } else {
-          return reviewer.subList(0, limit);
-        }
-      }
-    });
-  }
-
-  private boolean suggestGroupAsReviewer(final Project.NameKey project,
-      final GroupReference group) throws OrmException, Failure {
-    if (!PostReviewers.isLegalReviewerGroup(group.getUUID())) {
-      return false;
-    }
-
-    try {
-      final Set<Account> members = groupMembersFactory.create(getCurrentUser())
-          .listAccounts(group.getUUID(), project);
-
-      if (members.isEmpty()) {
-        return false;
-      }
-
-      final int maxAllowed =
-          cfg.getInt("addreviewer", "maxAllowed",
-              PostReviewers.DEFAULT_MAX_REVIEWERS);
-      if (maxAllowed > 0 && members.size() > maxAllowed) {
-        return false;
-      }
-    } catch (NoSuchGroupException e) {
-      return false;
-    } catch (NoSuchProjectException e) {
-      return false;
-    } catch (IOException e) {
-      throw new Failure(e);
-    }
-
-    return true;
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
index 56a6a50..5b28a61 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
@@ -59,6 +59,7 @@
     projectCache = pc;
   }
 
+  @Override
   public void contributorAgreements(
       final AsyncCallback<List<ContributorAgreement>> callback) {
     Collection<ContributorAgreement> agreements =
@@ -71,6 +72,7 @@
     callback.onSuccess(cas);
   }
 
+  @Override
   public void daemonHostKeys(final AsyncCallback<List<SshHostKey>> callback) {
     final ArrayList<SshHostKey> r = new ArrayList<>(hostKeys.size());
     for (final HostKey hk : hostKeys) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
index b6549ea..b2a6afc 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
@@ -15,10 +15,13 @@
 package com.google.gerrit.httpd.rpc.account;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.AccountSecurity;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.errors.ContactInformationStoreException;
+import com.google.gerrit.common.errors.InvalidUserNameException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
@@ -27,7 +30,6 @@
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
 import com.google.gerrit.reviewdb.client.ContactInformation;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
@@ -42,7 +44,6 @@
 import com.google.gerrit.server.contact.ContactStore;
 import com.google.gerrit.server.mail.EmailTokenVerifier;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.VoidResult;
 import com.google.gwtorm.server.OrmException;
@@ -71,6 +72,7 @@
 
   private final ChangeHooks hooks;
   private final GroupCache groupCache;
+  private final AuditService auditService;
 
   @Inject
   AccountSecurityImpl(final Provider<ReviewDb> schema,
@@ -82,7 +84,8 @@
       final ChangeUserName.CurrentUser changeUserNameFactory,
       final DeleteExternalIds.Factory deleteExternalIdsFactory,
       final ExternalIdDetailFactory.Factory externalIdDetailFactory,
-      final ChangeHooks hooks, final GroupCache groupCache) {
+      final ChangeHooks hooks, final GroupCache groupCache,
+      final AuditService auditService) {
     super(schema, currentUser);
     contactStore = cs;
     realm = r;
@@ -92,6 +95,7 @@
     byEmailCache = abec;
     accountCache = uac;
     accountManager = am;
+    this.auditService = auditService;
 
     useContactInfo = contactStore != null && contactStore.isEnabled();
 
@@ -106,25 +110,32 @@
   public void changeUserName(final String newName,
       final AsyncCallback<VoidResult> callback) {
     if (realm.allowsEdit(Account.FieldName.USER_NAME)) {
+      if (newName == null || !newName.matches(Account.USER_NAME_PATTERN)) {
+        callback.onFailure(new InvalidUserNameException());
+      }
       Handler.wrap(changeUserNameFactory.create(newName)).to(callback);
     } else {
-      callback.onFailure(new PermissionDeniedException("Not allowed to change"
-          + " username"));
+      callback.onFailure(
+          new PermissionDeniedException("Not allowed to change username"));
     }
   }
 
+  @Override
   public void myExternalIds(AsyncCallback<List<AccountExternalId>> callback) {
     externalIdDetailFactory.create().to(callback);
   }
 
+  @Override
   public void deleteExternalIds(final Set<AccountExternalId.Key> keys,
       final AsyncCallback<Set<AccountExternalId.Key>> callback) {
     deleteExternalIdsFactory.create(keys).to(callback);
   }
 
+  @Override
   public void updateContact(final String name, final String emailAddr,
       final ContactInformation info, final AsyncCallback<Account> callback) {
     run(callback, new Action<Account>() {
+      @Override
       public Account run(ReviewDb db) throws OrmException, Failure {
         IdentifiedUser self = user.get();
         final Account me = db.accounts().get(self.getAccountId());
@@ -133,7 +144,7 @@
           me.setFullName(Strings.emptyToNull(name));
         }
         if (!Strings.isNullOrEmpty(emailAddr)
-            && !self.getEmailAddresses().contains(emailAddr)) {
+            && !self.hasEmailAddress(emailAddr)) {
           throw new Failure(new PermissionDeniedException("Email address must be verified"));
         }
         me.setPreferredEmail(Strings.emptyToNull(emailAddr));
@@ -168,9 +179,11 @@
     return a != null && a.equals(b);
   }
 
+  @Override
   public void enterAgreement(final String agreementName,
       final AsyncCallback<VoidResult> callback) {
     run(callback, new Action<VoidResult>() {
+      @Override
       public VoidResult run(final ReviewDb db) throws OrmException, Failure {
         ContributorAgreement ca = projectCache.getAllProjects().getConfig()
             .getContributorAgreement(agreementName);
@@ -198,9 +211,8 @@
         AccountGroupMember m = db.accountGroupMembers().get(key);
         if (m == null) {
           m = new AccountGroupMember(key);
-          db.accountGroupMembersAudit().insert(
-              Collections.singleton(new AccountGroupMemberAudit(
-                  m, account.getId(), TimeUtil.nowTs())));
+          auditService.dispatchAddAccountsToGroup(account.getId(), Collections
+              .singleton(m));
           db.accountGroupMembers().insert(Collections.singleton(m));
           accountCache.evict(m.getAccountId());
         }
@@ -210,6 +222,7 @@
     });
   }
 
+  @Override
   public void validateEmail(final String tokenString,
       final AsyncCallback<VoidResult> callback) {
     try {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
index bdb0028..e1c9e3c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
@@ -51,7 +51,7 @@
   private final AccountCache accountCache;
   private final ProjectControl.Factory projectControlFactory;
   private final AgreementInfoFactory.Factory agreementInfoFactory;
-  private final ChangeQueryBuilder.Factory queryBuilder;
+  private final ChangeQueryBuilder queryBuilder;
 
   @Inject
   AccountServiceImpl(final Provider<ReviewDb> schema,
@@ -59,7 +59,7 @@
       final AccountCache accountCache,
       final ProjectControl.Factory projectControlFactory,
       final AgreementInfoFactory.Factory agreementInfoFactory,
-      final ChangeQueryBuilder.Factory queryBuilder) {
+      final ChangeQueryBuilder queryBuilder) {
     super(schema, identifiedUser);
     this.currentUser = identifiedUser;
     this.accountCache = accountCache;
@@ -68,6 +68,7 @@
     this.queryBuilder = queryBuilder;
   }
 
+  @Override
   public void myAccount(final AsyncCallback<Account> callback) {
     run(callback, new Action<Account>() {
       @Override
@@ -77,9 +78,11 @@
     });
   }
 
+  @Override
   public void changePreferences(final AccountGeneralPreferences pref,
       final AsyncCallback<VoidResult> callback) {
     run(callback, new Action<VoidResult>() {
+      @Override
       public VoidResult run(final ReviewDb db) throws OrmException, Failure {
         final Account a = db.accounts().get(getAccountId());
         if (a == null) {
@@ -97,6 +100,7 @@
   public void changeDiffPreferences(final AccountDiffPreference diffPref,
       AsyncCallback<VoidResult> callback) {
     run(callback, new Action<VoidResult>(){
+      @Override
       public VoidResult run(ReviewDb db) throws OrmException {
         if (!diffPref.getAccountId().equals(getAccountId())) {
           throw new IllegalArgumentException("diffPref.getAccountId() "
@@ -109,9 +113,11 @@
     });
   }
 
+  @Override
   public void myProjectWatch(
       final AsyncCallback<List<AccountProjectWatchInfo>> callback) {
     run(callback, new Action<List<AccountProjectWatchInfo>>() {
+      @Override
       public List<AccountProjectWatchInfo> run(ReviewDb db) throws OrmException {
         List<AccountProjectWatchInfo> r = new ArrayList<>();
 
@@ -127,6 +133,7 @@
           r.add(new AccountProjectWatchInfo(w, ctl.getProject()));
         }
         Collections.sort(r, new Comparator<AccountProjectWatchInfo>() {
+          @Override
           public int compare(final AccountProjectWatchInfo a,
               final AccountProjectWatchInfo b) {
             return a.getProject().getName().compareTo(b.getProject().getName());
@@ -137,9 +144,11 @@
     });
   }
 
+  @Override
   public void addProjectWatch(final String projectName, final String filter,
       final AsyncCallback<AccountProjectWatchInfo> callback) {
     run(callback, new Action<AccountProjectWatchInfo>() {
+      @Override
       public AccountProjectWatchInfo run(ReviewDb db) throws OrmException,
           NoSuchProjectException, InvalidQueryException {
         final Project.NameKey nameKey = new Project.NameKey(projectName);
@@ -147,7 +156,7 @@
 
         if (filter != null) {
           try {
-            queryBuilder.create(currentUser.get()).parse(filter);
+            queryBuilder.parse(filter);
           } catch (QueryParseException badFilter) {
             throw new InvalidQueryException(badFilter.getMessage(), filter);
           }
@@ -167,6 +176,7 @@
     });
   }
 
+  @Override
   public void updateProjectWatch(final AccountProjectWatch watch,
       final AsyncCallback<VoidResult> callback) {
     if (!getAccountId().equals(watch.getAccountId())) {
@@ -175,6 +185,7 @@
     }
 
     run(callback, new Action<VoidResult>() {
+      @Override
       public VoidResult run(ReviewDb db) throws OrmException {
         db.accountProjectWatches().update(Collections.singleton(watch));
         return VoidResult.INSTANCE;
@@ -182,9 +193,11 @@
     });
   }
 
+  @Override
   public void deleteProjectWatches(final Set<AccountProjectWatch.Key> keys,
       final AsyncCallback<VoidResult> callback) {
     run(callback, new Action<VoidResult>() {
+      @Override
       public VoidResult run(final ReviewDb db) throws OrmException, Failure {
         final Account.Id me = getAccountId();
         for (final AccountProjectWatch.Key keyId : keys) {
@@ -198,6 +211,7 @@
     });
   }
 
+  @Override
   public void myAgreements(final AsyncCallback<AgreementInfo> callback) {
     agreementInfoFactory.create().to(callback);
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/DeprecatedChangeQueryServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/DeprecatedChangeQueryServlet.java
deleted file mode 100644
index da21c51..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/DeprecatedChangeQueryServlet.java
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.change;
-
-import com.google.gerrit.server.query.change.QueryProcessor;
-import com.google.gerrit.server.query.change.QueryProcessor.OutputFormat;
-import com.google.gson.Gson;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-import java.io.IOException;
-
-import javax.servlet.ServletOutputStream;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-@Singleton
-public class DeprecatedChangeQueryServlet extends HttpServlet {
-  private static final long serialVersionUID = 1L;
-  private final Provider<QueryProcessor> processor;
-
-  @Inject
-  DeprecatedChangeQueryServlet(Provider<QueryProcessor> processor) {
-    this.processor = processor;
-  }
-
-  @Override
-  protected void doGet(HttpServletRequest req, HttpServletResponse rsp)
-      throws IOException {
-    rsp.setContentType("text/json");
-    rsp.setCharacterEncoding("UTF-8");
-
-    QueryProcessor p = processor.get();
-    OutputFormat format = OutputFormat.JSON;
-    try {
-      format = OutputFormat.valueOf(get(req, "format", format.toString()));
-    } catch (IllegalArgumentException err) {
-      error(rsp, "invalid format");
-      return;
-    }
-
-    switch (format) {
-      case JSON:
-        rsp.setContentType("text/json");
-        rsp.setCharacterEncoding("UTF-8");
-        break;
-
-      case TEXT:
-        rsp.setContentType("text/plain");
-        rsp.setCharacterEncoding("UTF-8");
-        break;
-
-      default:
-        error(rsp, "invalid format");
-        return;
-    }
-
-    p.setIncludeComments(get(req, "comments", false));
-    p.setIncludeCurrentPatchSet(get(req, "current-patch-set", false));
-    p.setIncludePatchSets(get(req, "patch-sets", false));
-    p.setIncludeApprovals(get(req, "all-approvals", false));
-    p.setIncludeFiles(get(req, "files", false));
-    p.setOutput(rsp.getOutputStream(), format);
-    p.query(get(req, "q", "status:open"));
-  }
-
-  private static void error(HttpServletResponse rsp, String message)
-      throws IOException {
-    ErrorMessage em = new ErrorMessage();
-    em.message = message;
-
-    ServletOutputStream out = rsp.getOutputStream();
-    try {
-      out.write(new Gson().toJson(em).getBytes("UTF-8"));
-      out.write('\n');
-      out.flush();
-    } finally {
-      out.close();
-    }
-  }
-
-  private static String get(HttpServletRequest req, String name, String val) {
-    String v = req.getParameter(name);
-    if (v == null || v.isEmpty()) {
-      return val;
-    }
-    return v;
-  }
-
-  private static boolean get(HttpServletRequest req, String name, boolean val) {
-    String v = req.getParameter(name);
-    if (v == null || v.isEmpty()) {
-      return val;
-    }
-    return "true".equalsIgnoreCase(v);
-  }
-
-  public static class ErrorMessage {
-    public final String type = "error";
-    public String message;
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java
index 538275f..d0c042c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java
@@ -30,11 +30,13 @@
     this.patchSetDetail = patchSetDetail;
   }
 
+  @Override
   public void patchSetDetail(PatchSet.Id id,
       AsyncCallback<PatchSetDetail> callback) {
     patchSetDetail2(null, id, null, callback);
   }
 
+  @Override
   public void patchSetDetail2(PatchSet.Id baseId, PatchSet.Id id,
       AccountDiffPreference diffPrefs, AsyncCallback<PatchSetDetail> callback) {
     patchSetDetail.create(baseId, id, diffPrefs).to(callback);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
index 73cc83a..9fc95b1 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
@@ -15,18 +15,21 @@
 package com.google.gerrit.httpd.rpc.changedetail;
 
 import com.google.common.base.Function;
+import com.google.common.base.Optional;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.common.data.UiCommandDetail;
 import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.AccountPatchReview;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -38,6 +41,8 @@
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.Revisions;
+import com.google.gerrit.server.edit.ChangeEdit;
+import com.google.gerrit.server.edit.ChangeEditUtil;
 import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchList;
@@ -50,6 +55,7 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.util.Providers;
 
@@ -57,6 +63,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -77,10 +84,12 @@
   private final PatchSetInfoFactory infoFactory;
   private final ReviewDb db;
   private final PatchListCache patchListCache;
-  private final ChangeControl.Factory changeControlFactory;
+  private final Provider<CurrentUser> userProvider;
+  private final ChangeControl.GenericFactory changeControlFactory;
   private final ChangesCollection changes;
   private final Revisions revisions;
   private final PatchLineCommentsUtil plcUtil;
+  private final ChangeEditUtil editUtil;
 
   private Project.NameKey projectKey;
   private final PatchSet.Id psIdBase;
@@ -96,20 +105,24 @@
   @Inject
   PatchSetDetailFactory(final PatchSetInfoFactory psif, final ReviewDb db,
       final PatchListCache patchListCache,
-      final ChangeControl.Factory changeControlFactory,
+      final Provider<CurrentUser> userProvider,
+      final ChangeControl.GenericFactory changeControlFactory,
       final ChangesCollection changes,
       final Revisions revisions,
       final PatchLineCommentsUtil plcUtil,
+      ChangeEditUtil editUtil,
       @Assisted("psIdBase") @Nullable final PatchSet.Id psIdBase,
       @Assisted("psIdNew") final PatchSet.Id psIdNew,
       @Assisted @Nullable final AccountDiffPreference diffPrefs) {
     this.infoFactory = psif;
     this.db = db;
     this.patchListCache = patchListCache;
+    this.userProvider = userProvider;
     this.changeControlFactory = changeControlFactory;
     this.changes = changes;
     this.revisions = revisions;
     this.plcUtil = plcUtil;
+    this.editUtil = editUtil;
 
     this.psIdBase = psIdBase;
     this.psIdNew = psIdNew;
@@ -118,10 +131,21 @@
 
   @Override
   public PatchSetDetail call() throws OrmException, NoSuchEntityException,
-      PatchSetInfoNotAvailableException, NoSuchChangeException {
+      PatchSetInfoNotAvailableException, NoSuchChangeException, AuthException,
+      IOException {
+    Optional<ChangeEdit> edit = null;
     if (control == null || patchSet == null) {
-      control = changeControlFactory.validateFor(psIdNew.getParentKey());
-      patchSet = db.patchSets().get(psIdNew);
+      control = changeControlFactory.validateFor(psIdNew.getParentKey(),
+          userProvider.get());
+      if (psIdNew.get() == 0) {
+        Change change = db.changes().get(psIdNew.getParentKey());
+        edit = editUtil.byChange(change);
+        if (edit.isPresent()) {
+          patchSet = edit.get().getBasePatchSet();
+        }
+      } else {
+        patchSet = db.patchSets().get(psIdNew);
+      }
       if (patchSet == null) {
         throw new NoSuchEntityException();
       }
@@ -132,7 +156,11 @@
     try {
       if (psIdBase != null) {
         oldId = toObjectId(psIdBase);
-        newId = toObjectId(psIdNew);
+        if (edit != null && edit.isPresent()) {
+          newId = edit.get().getEditCommit().toObjectId();
+        } else {
+          newId = toObjectId(psIdNew);
+        }
 
         list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
       } else { // OK, means use base to compare
@@ -149,10 +177,12 @@
     }
 
     ChangeNotes notes = control.getNotes();
-    for (PatchLineComment c : plcUtil.publishedByPatchSet(db, notes, psIdNew)) {
-      final Patch p = byKey.get(c.getKey().getParentKey());
-      if (p != null) {
-        p.setCommentCount(p.getCommentCount() + 1);
+    if (edit == null) {
+      for (PatchLineComment c : plcUtil.publishedByPatchSet(db, notes, psIdNew)) {
+        final Patch p = byKey.get(c.getKey().getParentKey());
+        if (p != null) {
+          p.setCommentCount(p.getCommentCount() + 1);
+        }
       }
     }
 
@@ -160,17 +190,18 @@
     detail.setPatchSet(patchSet);
     detail.setProject(projectKey);
 
-    detail.setInfo(infoFactory.get(db, psIdNew));
+    detail.setInfo(infoFactory.get(db, patchSet.getId()));
     detail.setPatches(patches);
 
     final CurrentUser user = control.getCurrentUser();
-    if (user.isIdentifiedUser()) {
+    if (user.isIdentifiedUser() && edit == null) {
       // If we are signed in, compute the number of draft comments by the
       // current user on each of these patch files. This way they can more
       // quickly locate where they have pending drafts, and review them.
       //
       final Account.Id me = ((IdentifiedUser) user).getAccountId();
-      for (final PatchLineComment c : db.patchComments().draftByPatchSetAuthor(psIdNew, me)) {
+      for (PatchLineComment c
+          : plcUtil.draftByPatchSetAuthor(db, psIdNew, me, notes)) {
         final Patch p = byKey.get(c.getKey().getParentKey());
         if (p != null) {
           p.setDraftCount(p.getDraftCount() + 1);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
index 64d5838..9257271 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -34,19 +33,20 @@
 class PatchDetailServiceImpl extends BaseServiceImplementation implements
     PatchDetailService {
   private final PatchScriptFactory.Factory patchScriptFactoryFactory;
-  private final ChangeControl.Factory changeControlFactory;
+  private final ChangeControl.GenericFactory changeControlFactory;
 
   @Inject
   PatchDetailServiceImpl(final Provider<ReviewDb> schema,
       final Provider<CurrentUser> currentUser,
       final PatchScriptFactory.Factory patchScriptFactoryFactory,
-      final ChangeControl.Factory changeControlFactory) {
+      final ChangeControl.GenericFactory changeControlFactory) {
     super(schema, currentUser);
 
     this.patchScriptFactoryFactory = patchScriptFactoryFactory;
     this.changeControlFactory = changeControlFactory;
   }
 
+  @Override
   public void patchScript(final Patch.Key patchKey, final PatchSet.Id psa,
       final PatchSet.Id psb, final AccountDiffPreference dp,
       final AsyncCallback<PatchScript> callback) {
@@ -58,8 +58,9 @@
     new Handler<PatchScript>() {
       @Override
       public PatchScript call() throws Exception {
-        Change.Id changeId = patchKey.getParentKey().getParentKey();
-        ChangeControl control = changeControlFactory.validateFor(changeId);
+        ChangeControl control = changeControlFactory.validateFor(
+            patchKey.getParentKey().getParentKey(),
+            getCurrentUser());
         return patchScriptFactoryFactory.create(
             control, patchKey.getFileName(), psa, psb, dp).call();
       }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
index d18c16a..e877d1e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
@@ -79,9 +79,9 @@
   }
 
   @Override
-  protected ProjectAccess updateProjectConfig(ProjectConfig config,
-      MetaDataUpdate md, boolean parentProjectUpdate) throws IOException,
-      NoSuchProjectException, ConfigInvalidException {
+  protected ProjectAccess updateProjectConfig(ProjectControl ctl,
+      ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate)
+      throws IOException, NoSuchProjectException, ConfigInvalidException {
     RevCommit commit = config.commit(md);
 
     hooks.doRefUpdatedHook(
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
index 52283b2..829035b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
@@ -16,7 +16,7 @@
 
 import static com.google.gerrit.common.ProjectAccessUtil.mergeSections;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
@@ -139,7 +139,7 @@
         parentProjectUpdate = true;
         try {
           setParent.get().validateParentUpdate(projectControl,
-              Objects.firstNonNull(parentProjectName, allProjects.get()).get(),
+              MoreObjects.firstNonNull(parentProjectName, allProjects.get()).get(),
               checkIfOwner);
         } catch (AuthException e) {
           throw new UpdateParentFailedException(
@@ -163,15 +163,17 @@
         md.setMessage("Modify access rules\n");
       }
 
-      return updateProjectConfig(config, md, parentProjectUpdate);
+      return updateProjectConfig(projectControl, config, md,
+          parentProjectUpdate);
     } finally {
       md.close();
     }
   }
 
-  protected abstract T updateProjectConfig(ProjectConfig config,
-      MetaDataUpdate md, boolean parentProjectUpdate) throws IOException,
-      NoSuchProjectException, ConfigInvalidException, OrmException;
+  protected abstract T updateProjectConfig(ProjectControl ctl,
+      ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate)
+      throws IOException, NoSuchProjectException, ConfigInvalidException,
+      OrmException;
 
   private void replace(ProjectConfig config, Set<String> toDelete,
       AccessSection section) throws NoSuchGroupException {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
index 51aa2c0..9437bbe 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
-import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.PermissionRule;
@@ -24,29 +24,22 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetAncestor;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ChangesCollection;
-import com.google.gerrit.server.change.MergeabilityChecker;
 import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.mail.CreateChangeSender;
-import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.SetParent;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -54,18 +47,11 @@
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 public class ReviewProjectAccess extends ProjectAccessHandler<Change.Id> {
-  private static final Logger log =
-      LoggerFactory.getLogger(ReviewProjectAccess.class);
-
   interface Factory {
     ReviewProjectAccess create(
         @Assisted("projectName") Project.NameKey projectName,
@@ -77,25 +63,21 @@
 
   private final ReviewDb db;
   private final IdentifiedUser user;
-  private final PatchSetInfoFactory patchSetInfoFactory;
   private final Provider<PostReviewers> reviewersProvider;
-  private final MergeabilityChecker mergeabilityChecker;
-  private final ChangeHooks hooks;
-  private final CreateChangeSender.Factory createChangeSenderFactory;
   private final ProjectCache projectCache;
   private final ChangesCollection changes;
+  private final ChangeInserter.Factory changeInserterFactory;
 
   @Inject
   ReviewProjectAccess(final ProjectControl.Factory projectControlFactory,
       GroupBackend groupBackend,
       MetaDataUpdate.User metaDataUpdateFactory, ReviewDb db,
-      IdentifiedUser user, PatchSetInfoFactory patchSetInfoFactory,
+      IdentifiedUser user,
       Provider<PostReviewers> reviewersProvider,
-      MergeabilityChecker mergeabilityChecker, ChangeHooks hooks,
-      CreateChangeSender.Factory createChangeSenderFactory,
       ProjectCache projectCache,
       AllProjectsNameProvider allProjects,
       ChangesCollection changes,
+      ChangeInserter.Factory changeInserterFactory,
       Provider<SetParent> setParent,
 
       @Assisted("projectName") Project.NameKey projectName,
@@ -108,23 +90,20 @@
         parentProjectName, message, false);
     this.db = db;
     this.user = user;
-    this.patchSetInfoFactory = patchSetInfoFactory;
     this.reviewersProvider = reviewersProvider;
-    this.mergeabilityChecker = mergeabilityChecker;
-    this.hooks = hooks;
-    this.createChangeSenderFactory = createChangeSenderFactory;
     this.projectCache = projectCache;
     this.changes = changes;
+    this.changeInserterFactory = changeInserterFactory;
   }
 
   @Override
-  protected Change.Id updateProjectConfig(ProjectConfig config,
-      MetaDataUpdate md, boolean parentProjectUpdate) throws IOException,
-      OrmException {
+  protected Change.Id updateProjectConfig(ProjectControl ctl,
+      ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate)
+      throws IOException, OrmException {
     Change.Id changeId = new Change.Id(db.nextChangeId());
-    PatchSet ps =
-        new PatchSet(new PatchSet.Id(changeId, Change.INITIAL_PATCH_SET_ID));
-    RevCommit commit = config.commitToNewRef(md, ps.getRefName());
+    RevCommit commit =
+        config.commitToNewRef(md, new PatchSet.Id(changeId,
+            Change.INITIAL_PATCH_SET_ID).toRefName());
     if (commit.getId().equals(base)) {
       return null;
     }
@@ -137,35 +116,10 @@
             config.getProject().getNameKey(),
             RefNames.REFS_CONFIG),
         TimeUtil.nowTs());
+    ChangeInserter ins =
+        changeInserterFactory.create(ctl, change, commit);
+    ins.insert();
 
-    ps.setCreatedOn(change.getCreatedOn());
-    ps.setUploader(change.getOwner());
-    ps.setRevision(new RevId(commit.name()));
-
-    PatchSetInfo info = patchSetInfoFactory.get(commit, ps.getId());
-    change.setCurrentPatchSet(info);
-    ChangeUtil.updated(change);
-
-    db.changes().beginTransaction(changeId);
-    try {
-      insertAncestors(ps.getId(), commit);
-      db.patchSets().insert(Collections.singleton(ps));
-      db.changes().insert(Collections.singleton(change));
-      db.commit();
-    } finally {
-      db.rollback();
-    }
-    mergeabilityChecker.newCheck().addChange(change).reindex().run();
-    hooks.doPatchsetCreatedHook(change, ps, db);
-    try {
-      CreateChangeSender cm =
-          createChangeSenderFactory.create(change);
-      cm.setFrom(change.getOwner());
-      cm.setPatchSet(ps, info);
-      cm.send();
-    } catch (Exception err) {
-      log.error("Cannot send email for new change " + change.getId(), err);
-    }
     ChangeResource rsrc;
     try {
       rsrc = changes.parse(changeId);
@@ -179,20 +133,6 @@
     return changeId;
   }
 
-  private void insertAncestors(PatchSet.Id id, RevCommit src)
-      throws OrmException {
-    final int cnt = src.getParentCount();
-    List<PatchSetAncestor> toInsert = new ArrayList<>(cnt);
-    for (int p = 0; p < cnt; p++) {
-      PatchSetAncestor a;
-
-      a = new PatchSetAncestor(new PatchSetAncestor.Id(id, p + 1));
-      a.setAncestorRevision(new RevId(src.getParent(p).name()));
-      toInsert.add(a);
-    }
-    db.patchSetAncestors().insert(toInsert);
-  }
-
   private void addProjectOwnersAsReviewers(ChangeResource rsrc) {
     final String projectOwners =
         groupBackend.get(SystemGroupBackend.PROJECT_OWNERS).getName();
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/GitWebConfigTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/GitWebConfigTest.java
index 26cdd8a..57b089a 100644
--- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/GitWebConfigTest.java
+++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/GitWebConfigTest.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.httpd;
 
-import org.junit.Test;
-
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import org.junit.Test;
+
 public class GitWebConfigTest {
 
   private static final String VALID_CHARACTERS = "*()";
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/restapi/ParameterParserTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/restapi/ParameterParserTest.java
index 8533a9c..af90585 100644
--- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/restapi/ParameterParserTest.java
+++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/restapi/ParameterParserTest.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.httpd.restapi;
 
+import static org.junit.Assert.assertEquals;
+
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -22,7 +24,6 @@
 import com.google.gson.JsonPrimitive;
 
 import org.junit.Test;
-import static org.junit.Assert.assertEquals;
 
 public class ParameterParserTest {
   @Test
diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
index 474b284..4ee9676 100644
--- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -294,8 +294,8 @@
     return name;
   }
 
-  private volatile static File myArchive;
-  private volatile static File myHome;
+  private static volatile File myArchive;
+  private static volatile File myHome;
 
   /**
    * Locate the JAR/WAR file we were launched from.
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AutoCommitWriter.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AutoCommitWriter.java
index fd14cd9..e0c13ae 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AutoCommitWriter.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AutoCommitWriter.java
@@ -82,7 +82,7 @@
   }
 
   @Override
-  public void deleteDocuments(Term term) throws IOException {
+  public void deleteDocuments(Term... term) throws IOException {
     super.deleteDocuments(term);
     autoFlush();
   }
@@ -98,18 +98,6 @@
   }
 
   @Override
-  public void deleteDocuments(Term... terms) throws IOException {
-    super.deleteDocuments(terms);
-    autoFlush();
-  }
-
-  @Override
-  public void deleteDocuments(Query query) throws IOException {
-    super.deleteDocuments(query);
-    autoFlush();
-  }
-
-  @Override
   public void deleteDocuments(Query... queries) throws IOException {
     super.deleteDocuments(queries);
     autoFlush();
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/CustomMappingAnalyzer.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/CustomMappingAnalyzer.java
new file mode 100644
index 0000000..3d7faeb
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/CustomMappingAnalyzer.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.lucene;
+
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.AnalyzerWrapper;
+import org.apache.lucene.analysis.charfilter.MappingCharFilter;
+import org.apache.lucene.analysis.charfilter.NormalizeCharMap;
+
+import java.io.Reader;
+import java.util.Map;
+
+/**
+ * This analyzer can be used to provide custom char mappings.
+ *
+ * <p>Example usage:
+ *
+ * <pre class="prettyprint">
+ * {@code
+ * Map<String,String> customMapping = new HashMap<>();
+ * customMapping.put("_", " ");
+ * customMapping.put(".", " ");
+ *
+ * CustomMappingAnalyzer analyzer =
+ *   new CustomMappingAnalyzer(new StandardAnalyzer(version), customMapping);
+ * }
+ * </pre>
+ */
+public class CustomMappingAnalyzer extends AnalyzerWrapper {
+  private Analyzer delegate;
+  private Map<String, String> customMappings;
+
+  public CustomMappingAnalyzer(Analyzer delegate,
+      Map<String, String> customMappings) {
+    super(delegate.getReuseStrategy());
+    this.delegate = delegate;
+    this.customMappings = customMappings;
+  }
+
+  @Override
+  protected Analyzer getWrappedAnalyzer(String fieldName) {
+    return delegate;
+  }
+
+  @Override
+  protected Reader wrapReader(String fieldName, Reader reader) {
+    NormalizeCharMap.Builder builder = new NormalizeCharMap.Builder();
+    for (Map.Entry<String, String> e : customMappings.entrySet()) {
+      builder.add(e.getKey(), e.getValue());
+    }
+    return new MappingCharFilter(builder.build(), reader);
+  }
+}
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 3e3a7fa..e8825c5 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.lucene;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
 import static com.google.gerrit.server.index.IndexRewriteImpl.CLOSED_STATUSES;
 import static com.google.gerrit.server.index.IndexRewriteImpl.OPEN_STATUSES;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -50,15 +51,12 @@
 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.gerrit.server.query.change.SortKeyPredicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
-import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.analysis.standard.StandardAnalyzer;
 import org.apache.lucene.analysis.util.CharArraySet;
 import org.apache.lucene.document.Document;
@@ -121,8 +119,12 @@
   private static final String CHANGE_FIELD = ChangeField.CHANGE.getName();
   private static final String DELETED_FIELD = ChangeField.DELETED.getName();
   private static final String ID_FIELD = ChangeField.LEGACY_ID.getName();
+  private static final String MERGEABLE_FIELD = ChangeField.MERGEABLE.getName();
   private static final ImmutableSet<String> FIELDS = ImmutableSet.of(
-      ADDED_FIELD, APPROVAL_FIELD, CHANGE_FIELD, DELETED_FIELD, ID_FIELD);
+      ADDED_FIELD, APPROVAL_FIELD, CHANGE_FIELD, DELETED_FIELD, ID_FIELD,
+      MERGEABLE_FIELD);
+  private static final Map<String, String> CUSTOM_CHAR_MAPPING = ImmutableMap.of(
+      "_", " ", ".", " ");
 
   private static final Map<Schema<ChangeData>, Version> LUCENE_VERSIONS;
   static {
@@ -136,6 +138,14 @@
     Version lucene46 = Version.LUCENE_46;
     @SuppressWarnings("deprecation")
     Version lucene47 = Version.LUCENE_47;
+    @SuppressWarnings("deprecation")
+    Version lucene48 = Version.LUCENE_48;
+    @SuppressWarnings("deprecation")
+    Version lucene410 = Version.LUCENE_4_10_0;
+    // We are using 4.10.2 but there is no difference in the index
+    // format since 4.10.1, so we reuse the version here.
+    @SuppressWarnings("deprecation")
+    Version lucene4101 = Version.LUCENE_4_10_1;
     for (Map.Entry<Integer, Schema<ChangeData>> e
         : ChangeSchemas.ALL.entrySet()) {
       if (e.getKey() <= 3) {
@@ -146,8 +156,12 @@
         versions.put(e.getValue(), lucene46);
       } else if (e.getKey() <= 10) {
         versions.put(e.getValue(), lucene47);
+      } else if (e.getKey() <= 11) {
+        versions.put(e.getValue(), lucene48);
+      } else if (e.getKey() <= 13) {
+        versions.put(e.getValue(), lucene410);
       } else {
-        versions.put(e.getValue(), Version.LUCENE_48);
+        versions.put(e.getValue(), lucene4101);
       }
     }
     LUCENE_VERSIONS = versions.build();
@@ -174,8 +188,10 @@
     private long commitWithinMs;
 
     private GerritIndexWriterConfig(Version version, Config cfg, String name) {
-      luceneConfig = new IndexWriterConfig(version,
-          new StandardAnalyzer(version, CharArraySet.EMPTY_SET));
+      CustomMappingAnalyzer analyzer =
+          new CustomMappingAnalyzer(new StandardAnalyzer(
+              CharArraySet.EMPTY_SET), CUSTOM_CHAR_MAPPING);
+      luceneConfig = new IndexWriterConfig(version, analyzer);
       luceneConfig.setOpenMode(OpenMode.CREATE_OR_APPEND);
       double m = 1 << 20;
       luceneConfig.setRAMBufferSizeMB(cfg.getLong(
@@ -217,7 +233,7 @@
   LuceneChangeIndex(
       @GerritServerConfig Config cfg,
       SitePaths sitePaths,
-      @IndexExecutor ListeningExecutorService executor,
+      @IndexExecutor(INTERACTIVE)  ListeningExecutorService executor,
       Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       FillArgs fillArgs,
@@ -238,10 +254,10 @@
     Version luceneVersion = checkNotNull(
         LUCENE_VERSIONS.get(schema),
         "unknown Lucene version for index schema: %s", schema);
-
-    Analyzer analyzer =
-        new StandardAnalyzer(luceneVersion, CharArraySet.EMPTY_SET);
-    queryBuilder = new QueryBuilder(schema, analyzer);
+    CustomMappingAnalyzer analyzer =
+        new CustomMappingAnalyzer(new StandardAnalyzer(CharArraySet.EMPTY_SET),
+            CUSTOM_CHAR_MAPPING);
+    queryBuilder = new QueryBuilder(analyzer);
 
     BooleanQuery.setMaxClauseCount(cfg.getInt("index", "defaultMaxClauseCount",
         BooleanQuery.getMaxClauseCount()));
@@ -285,26 +301,6 @@
 
   @SuppressWarnings("unchecked")
   @Override
-  public void insert(ChangeData cd) throws IOException {
-    Term id = QueryBuilder.idTerm(cd);
-    Document doc = toDocument(cd);
-    try {
-      if (cd.change().getStatus().isOpen()) {
-        Futures.allAsList(
-            closedIndex.delete(id),
-            openIndex.insert(doc)).get();
-      } else {
-        Futures.allAsList(
-            openIndex.delete(id),
-            closedIndex.insert(doc)).get();
-      }
-    } catch (OrmException | ExecutionException | InterruptedException e) {
-      throw new IOException(e);
-    }
-  }
-
-  @SuppressWarnings("unchecked")
-  @Override
   public void replace(ChangeData cd) throws IOException {
     Term id = QueryBuilder.idTerm(cd);
     Document doc = toDocument(cd);
@@ -325,20 +321,7 @@
 
   @SuppressWarnings("unchecked")
   @Override
-  public void delete(ChangeData cd) throws IOException {
-    Term id = QueryBuilder.idTerm(cd);
-    try {
-      Futures.allAsList(
-          openIndex.delete(id),
-          closedIndex.delete(id)).get();
-    } catch (ExecutionException | InterruptedException e) {
-      throw new IOException(e);
-    }
-  }
-
-  @SuppressWarnings("unchecked")
-  @Override
-  public void delete(int id) throws IOException {
+  public void delete(Change.Id id) throws IOException {
     Term idTerm = QueryBuilder.idTerm(id);
     try {
       Futures.allAsList(
@@ -367,7 +350,7 @@
       indexes.add(closedIndex);
     }
     return new QuerySource(indexes, queryBuilder.toQuery(p), start, limit,
-        getSort(schema, p));
+        getSort());
   }
 
   @Override
@@ -375,22 +358,12 @@
     setReady(sitePaths, schema.getVersion(), ready);
   }
 
-  @SuppressWarnings("deprecation")
-  private static Sort getSort(Schema<ChangeData> schema,
-      Predicate<ChangeData> p) {
-    // Standard order is descending by sort key, unless reversed due to a
-    // sortkey_before predicate.
-    if (SortKeyPredicate.hasSortKeyField(schema)) {
-      boolean reverse = ChangeQueryBuilder.hasNonTrivialSortKeyAfter(schema, p);
-      return new Sort(new SortField(
-          ChangeField.SORTKEY.getName(), SortField.Type.LONG, !reverse));
-    } else {
-      return new Sort(
-          new SortField(
-            ChangeField.UPDATED.getName(), SortField.Type.LONG, true),
-          new SortField(
-            ChangeField.LEGACY_ID.getName(), SortField.Type.INT, true));
-    }
+  private static Sort getSort() {
+    return new Sort(
+        new SortField(
+          ChangeField.UPDATED.getName(), SortField.Type.LONG, true),
+        new SortField(
+          ChangeField.LEGACY_ID.getName(), SortField.Type.INT, true));
   }
 
   private class QuerySource implements ChangeDataSource {
@@ -510,25 +483,28 @@
           deleted.numericValue().intValue());
     }
 
+    // Mergeable.
+    String mergeable = doc.get(MERGEABLE_FIELD);
+    if ("1".equals(mergeable)) {
+      cd.setMergeable(true);
+    } else if ("0".equals(mergeable)) {
+      cd.setMergeable(false);
+    }
+
     return cd;
   }
 
-  private Document toDocument(ChangeData cd) throws IOException {
-    try {
-      Document result = new Document();
-      for (Values<ChangeData> vs : schema.buildFields(cd, fillArgs)) {
-        if (vs.getValues() != null) {
-          add(result, vs);
-        }
+  private Document toDocument(ChangeData cd) {
+    Document result = new Document();
+    for (Values<ChangeData> vs : schema.buildFields(cd, fillArgs)) {
+      if (vs.getValues() != null) {
+        add(result, vs);
       }
-      return result;
-    } catch (OrmException e) {
-      throw new IOException(e);
     }
+    return result;
   }
 
-  private void add(Document doc, Values<ChangeData> values)
-      throws OrmException {
+  private void add(Document doc, Values<ChangeData> values) {
     String name = values.getField().getName();
     FieldType<?> type = values.getField().getType();
     Store store = store(values.getField());
@@ -542,17 +518,8 @@
         doc.add(new LongField(name, (Long) value, store));
       }
     } else if (type == FieldType.TIMESTAMP) {
-      @SuppressWarnings("deprecation")
-      boolean legacy = values.getField() == ChangeField.LEGACY_UPDATED;
-      if (legacy) {
-        for (Object value : values.getValues()) {
-          int t = queryBuilder.toIndexTimeInMinutes((Timestamp) value);
-          doc.add(new IntField(name, (int) t, store));
-        }
-      } else {
-        for (Object value : values.getValues()) {
-          doc.add(new LongField(name, ((Timestamp) value).getTime(), store));
-        }
+      for (Object value : values.getValues()) {
+        doc.add(new LongField(name, ((Timestamp) value).getTime(), store));
       }
     } else if (type == FieldType.EXACT
         || type == FieldType.PREFIX) {
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
index 583e54f..672a7c9 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -16,9 +16,9 @@
 
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.ChangeSchemas;
 import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.IndexConfig;
 import com.google.gerrit.server.index.IndexModule;
 import com.google.gerrit.server.index.Schema;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -44,6 +44,7 @@
 
   @Override
   protected void configure() {
+    bind(IndexConfig.class).toInstance(IndexConfig.createDefault());
     factory(LuceneChangeIndex.Factory.class);
     install(new IndexModule(threads));
     if (singleVersion == null && base == null) {
@@ -69,8 +70,7 @@
 
     @Provides
     @Singleton
-    LuceneChangeIndex getIndex(LuceneChangeIndex.Factory factory,
-        SitePaths sitePaths) {
+    LuceneChangeIndex getIndex(LuceneChangeIndex.Factory factory) {
       Schema<ChangeData> schema = singleVersion != null
           ? ChangeSchemas.get(singleVersion)
           : ChangeSchemas.getLatest();
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java
index 41c83eb..edded44 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java
@@ -17,9 +17,9 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.common.collect.Lists;
-import com.google.gerrit.server.index.ChangeBatchIndexer;
 import com.google.gerrit.server.index.ChangeIndex;
 import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.SiteIndexer;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -39,14 +39,14 @@
   }
 
   private final IndexCollection indexes;
-  private final ChangeBatchIndexer batchIndexer;
+  private final SiteIndexer batchIndexer;
   private final ProjectCache projectCache;
   private final int version;
 
   @Inject
   OnlineReindexer(
       IndexCollection indexes,
-      ChangeBatchIndexer batchIndexer,
+      SiteIndexer batchIndexer,
       ProjectCache projectCache,
       @Assisted int version) {
     this.indexes = indexes;
@@ -76,8 +76,8 @@
         "not an active write schema version: %s", version);
     log.info("Starting online reindex from schema version {} to {}",
         version(indexes.getSearchIndex()), version(index));
-    ChangeBatchIndexer.Result result = batchIndexer.indexAll(
-        index, projectCache.all(), -1, -1, null, null);
+    SiteIndexer.Result result =
+        batchIndexer.indexAll(index, projectCache.all());
     if (!result.success()) {
       log.error("Online reindex of schema version {} failed. Successfully"
           + " indexed {} changes, failed to index {} changes",
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 2365929..28af057 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
@@ -19,12 +19,12 @@
 import static org.apache.lucene.search.BooleanClause.Occur.SHOULD;
 
 import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.FieldType;
 import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.IntegerRangePredicate;
 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;
@@ -32,7 +32,6 @@
 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.SortKeyPredicate;
 
 import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.index.Term;
@@ -43,7 +42,7 @@
 import org.apache.lucene.search.Query;
 import org.apache.lucene.search.RegexpQuery;
 import org.apache.lucene.search.TermQuery;
-import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.BytesRefBuilder;
 import org.apache.lucene.util.NumericUtils;
 
 import java.util.Date;
@@ -56,15 +55,13 @@
     return intTerm(ID_FIELD, cd.getId().get());
   }
 
-  public static Term idTerm(int id) {
-    return intTerm(ID_FIELD, id);
+  public static Term idTerm(Change.Id id) {
+    return intTerm(ID_FIELD, id.get());
   }
 
-  private final Schema<ChangeData> schema;
   private final org.apache.lucene.util.QueryBuilder queryBuilder;
 
-  public QueryBuilder(Schema<ChangeData> schema, Analyzer analyzer) {
-    this.schema = schema;
+  public QueryBuilder(Analyzer analyzer) {
     queryBuilder = new org.apache.lucene.util.QueryBuilder(analyzer);
   }
 
@@ -150,17 +147,15 @@
       return prefixQuery(p);
     } else if (p.getType() == FieldType.FULL_TEXT) {
       return fullTextQuery(p);
-    } else if (p instanceof SortKeyPredicate) {
-      return sortKeyQuery((SortKeyPredicate) p);
     } else {
       throw badFieldType(p.getType());
     }
   }
 
   private static Term intTerm(String name, int value) {
-    BytesRef bytes = new BytesRef(NumericUtils.BUF_SIZE_INT);
-    NumericUtils.intToPrefixCodedBytes(value, 0, bytes);
-    return new Term(name, bytes);
+    BytesRefBuilder builder = new BytesRefBuilder();
+    NumericUtils.intToPrefixCodedBytes(value, 0, builder);
+    return new Term(name, builder.get());
   }
 
   private Query intQuery(IndexPredicate<ChangeData> p)
@@ -198,56 +193,28 @@
     throw new QueryParseException("not an integer range: " + p);
   }
 
-  private Query sortKeyQuery(SortKeyPredicate p) {
-    long min = p.getMinValue(schema);
-    long max = p.getMaxValue(schema);
-    return NumericRangeQuery.newLongRange(
-        p.getField().getName(),
-        min != Long.MIN_VALUE ? min : null,
-        max != Long.MAX_VALUE ? max : null,
-        false, false);
-  }
-
-  @SuppressWarnings("deprecation")
   private Query timestampQuery(IndexPredicate<ChangeData> p)
       throws QueryParseException {
     if (p instanceof TimestampRangePredicate) {
       TimestampRangePredicate<ChangeData> r =
           (TimestampRangePredicate<ChangeData>) p;
-      if (r.getField() == ChangeField.LEGACY_UPDATED) {
-        return NumericRangeQuery.newIntRange(
-            r.getField().getName(),
-            toIndexTimeInMinutes(r.getMinTimestamp()),
-            toIndexTimeInMinutes(r.getMaxTimestamp()),
-            true, true);
-      } else {
-        return NumericRangeQuery.newLongRange(
-            r.getField().getName(),
-            r.getMinTimestamp().getTime(),
-            r.getMaxTimestamp().getTime(),
-            true, true);
-      }
+      return NumericRangeQuery.newLongRange(
+          r.getField().getName(),
+          r.getMinTimestamp().getTime(),
+          r.getMaxTimestamp().getTime(),
+          true, true);
     }
     throw new QueryParseException("not a timestamp: " + p);
   }
 
-  @SuppressWarnings("deprecation")
   private Query notTimestamp(TimestampRangePredicate<ChangeData> r)
       throws QueryParseException {
     if (r.getMinTimestamp().getTime() == 0) {
-      if (r.getField() == ChangeField.LEGACY_UPDATED) {
-        return NumericRangeQuery.newIntRange(
-            r.getField().getName(),
-            toIndexTimeInMinutes(r.getMaxTimestamp()),
-            null,
-            true, true);
-      } else {
-        return NumericRangeQuery.newLongRange(
-            r.getField().getName(),
-            r.getMaxTimestamp().getTime(),
-            null,
-            true, true);
-      }
+      return NumericRangeQuery.newLongRange(
+          r.getField().getName(),
+          r.getMaxTimestamp().getTime(),
+          null,
+          true, true);
     }
     throw new QueryParseException("cannot negate: " + r);
   }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/SubIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/SubIndex.java
index 4ee3bf4..e024f76 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/SubIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/SubIndex.java
@@ -108,7 +108,7 @@
 
     notDoneNrtFutures = Sets.newConcurrentHashSet();
 
-    reopenThread = new ControlledRealTimeReopenThread<IndexSearcher>(
+    reopenThread = new ControlledRealTimeReopenThread<>(
         writer, searcherManager,
         0.500 /* maximum stale age (seconds) */,
         0.010 /* minimum stale age (seconds) */);
@@ -159,7 +159,7 @@
     try {
       writer.getIndexWriter().commit();
       try {
-        writer.getIndexWriter().close(true);
+        writer.getIndexWriter().close();
       } catch (AlreadyClosedException e) {
         // Ignore.
       }
diff --git a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java
index 43b85bd..4e4c774 100644
--- a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java
+++ b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.HttpLogoutServlet;
 import com.google.gerrit.httpd.WebSession;
-import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.inject.Inject;
@@ -41,10 +40,9 @@
   OAuthLogoutServlet(AuthConfig authConfig,
       DynamicItem<WebSession> webSession,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
-      AccountManager accountManager,
       AuditService audit,
       Provider<OAuthSession> oauthSession) {
-      super(authConfig, webSession, urlProvider, accountManager, audit);
+    super(authConfig, webSession, urlProvider, audit);
     this.oauthSession = oauthSession;
   }
 
@@ -55,3 +53,4 @@
     oauthSession.get().logout();
   }
 }
+
diff --git a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
index 48963a6..4021c57 100644
--- a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
+++ b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.httpd.auth.oauth;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.Nullable;
@@ -148,7 +148,7 @@
       @Nullable String errorMessage)
       throws IOException {
     String self = req.getRequestURI();
-    String cancel = Objects.firstNonNull(
+    String cancel = MoreObjects.firstNonNull(
         urlProvider != null ? urlProvider.get() : "/", "/");
     cancel += LoginUrlToken.getToken(req);
 
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
index 93031fe..bef165b 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.httpd.auth.openid;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -248,7 +248,8 @@
   private void sendForm(HttpServletRequest req, HttpServletResponse res,
       boolean link, @Nullable String errorMessage) throws IOException {
     String self = req.getRequestURI();
-    String cancel = Objects.firstNonNull(urlProvider != null ? urlProvider.get() : "/", "/");
+    String cancel = MoreObjects.firstNonNull(
+        urlProvider != null ? urlProvider.get() : "/", "/");
     cancel += LoginUrlToken.getToken(req);
 
     Document doc = header.parse(LoginForm.class, "LoginForm.html");
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java
index 8ca71ff..9dbff03 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.HttpLogoutServlet;
 import com.google.gerrit.httpd.WebSession;
-import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.inject.Inject;
@@ -40,11 +39,10 @@
   @Inject
   OAuthOverOpenIDLogoutServlet(AuthConfig authConfig,
       DynamicItem<WebSession> webSession,
-      AccountManager accountManager,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
       AuditService audit,
       Provider<OAuthSessionOverOpenID> oauthSession) {
-    super(authConfig, webSession, urlProvider, accountManager, audit);
+    super(authConfig, webSession, urlProvider, audit);
     this.oauthSession = oauthSession;
   }
 
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java
index 53fc889..7766e69 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java
@@ -98,8 +98,7 @@
     }
   }
 
-  private void pickSSOServiceProvider()
-      throws ServletException {
+  private void pickSSOServiceProvider() {
     SortedSet<String> plugins = oauthServiceProviders.plugins();
     if (plugins.size() == 1) {
       SortedMap<String, Provider<OAuthServiceProvider>> services =
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
index 44b58c3..ba144b6 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.httpd.CanonicalWebUrl;
+import com.google.gerrit.httpd.ProxyProperties;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
@@ -55,12 +56,10 @@
 import org.openid4java.message.sreg.SRegRequest;
 import org.openid4java.message.sreg.SRegResponse;
 import org.openid4java.util.HttpClientFactory;
-import org.openid4java.util.ProxyProperties;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
@@ -108,29 +107,17 @@
       final Provider<IdentifiedUser> iu,
       CanonicalWebUrl up,
       @GerritServerConfig final Config config, final AuthConfig ac,
-      final AccountManager am) throws ConsumerException, MalformedURLException {
+      final AccountManager am,
+      ProxyProperties proxyProperties) {
 
-    if (config.getString("http", null, "proxy") != null) {
-      final URL proxyUrl = new URL(config.getString("http", null, "proxy"));
-      String username = config.getString("http", null, "proxyUsername");
-      String password = config.getString("http", null, "proxyPassword");
-
-      final String userInfo = proxyUrl.getUserInfo();
-      if (userInfo != null) {
-        int c = userInfo.indexOf(':');
-        if (0 < c) {
-          username = userInfo.substring(0, c);
-          password = userInfo.substring(c + 1);
-        } else {
-          username = userInfo;
-        }
-      }
-
-      final ProxyProperties proxy = new ProxyProperties();
-      proxy.setProxyHostName(proxyUrl.getHost());
-      proxy.setProxyPort(proxyUrl.getPort());
-      proxy.setUserName(username);
-      proxy.setPassword(password);
+    if (proxyProperties.getProxyUrl() != null) {
+      final org.openid4java.util.ProxyProperties proxy =
+          new org.openid4java.util.ProxyProperties();
+      URL url = proxyProperties.getProxyUrl();
+      proxy.setProxyHostName(url.getHost());
+      proxy.setProxyPort(url.getPort());
+      proxy.setUserName(proxyProperties.getUsername());
+      proxy.setPassword(proxyProperties.getPassword());
       HttpClientFactory.setProxyProperties(proxy);
     }
 
diff --git a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java
index 5a9b935..a99b360 100644
--- a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java
+++ b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java
@@ -30,6 +30,7 @@
 
 public class EditDeserializer implements JsonDeserializer<Edit>,
     JsonSerializer<Edit> {
+  @Override
   public Edit deserialize(final JsonElement json, final Type typeOfT,
       final JsonDeserializationContext context) throws JsonParseException {
     if (json.isJsonNull()) {
@@ -73,6 +74,7 @@
     return p.getAsInt();
   }
 
+  @Override
   public JsonElement serialize(final Edit src, final Type typeOfSrc,
       final JsonSerializationContext context) {
     if (src == null) {
diff --git a/gerrit-patch-jgit/src/test/java/org/eclipse/jgit/diff/EditDeserializerTest.java b/gerrit-patch-jgit/src/test/java/org/eclipse/jgit/diff/EditDeserializerTest.java
index f0ad62a..a2c3dae 100644
--- a/gerrit-patch-jgit/src/test/java/org/eclipse/jgit/diff/EditDeserializerTest.java
+++ b/gerrit-patch-jgit/src/test/java/org/eclipse/jgit/diff/EditDeserializerTest.java
@@ -14,9 +14,10 @@
 
 package org.eclipse.jgit.diff;
 
-import org.junit.Test;
 import static org.junit.Assert.assertNotNull;
 
+import org.junit.Test;
+
 public class EditDeserializerTest {
   @Test
   public void testDiffDeserializer() {
diff --git a/gerrit-pgm/BUCK b/gerrit-pgm/BUCK
index 8b5fdc1..9a317cd 100644
--- a/gerrit-pgm/BUCK
+++ b/gerrit-pgm/BUCK
@@ -1,29 +1,30 @@
 SRCS = 'src/main/java/com/google/gerrit/pgm/'
+RSRCS = 'src/main/resources/com/google/gerrit/pgm/'
 
-INIT_API_SRCS = [SRCS + n for n in [
-  'init/AllProjectsConfig.java',
-  'init/AllProjectsNameOnInitProvider.java',
-  'util/ConsoleUI.java',
-  'init/InitFlags.java',
-  'init/InitStep.java',
-  'init/InitStep.java',
-  'init/InstallPlugins.java',
-  'init/Section.java',
-]]
+INIT_API_SRCS = glob([SRCS + 'init/api/*.java'])
+
+DEPS = [
+    '//gerrit-common:server',
+    '//gerrit-extension-api:api',
+    '//gerrit-gwtexpui:linker_server',
+    '//gerrit-gwtexpui:server',
+    '//gerrit-httpd:httpd',
+    '//gerrit-server:server',
+    '//gerrit-sshd:sshd',
+    '//gerrit-reviewdb:server',
+    '//lib:guava',
+    '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
+    '//lib/guice:guice-servlet',
+    '//lib/jgit:jgit',
+    '//lib/log:api',
+    '//lib/log:log4j',
+]
 
 java_library(
   name = 'init-api',
   srcs = INIT_API_SRCS,
-  deps = [
-    '//gerrit-common:annotations',
-    '//gerrit-common:server',
-    '//gerrit-reviewdb:server',
-    '//gerrit-server:server',
-    '//lib:guava',
-    '//lib/guice:guice',
-    '//lib/guice:guice-assistedinject',
-    '//lib/jgit:jgit',
-  ],
+  deps = DEPS + ['//gerrit-common:annotations'],
   visibility = ['PUBLIC'],
 )
 
@@ -33,101 +34,85 @@
   visibility = ['PUBLIC'],
 )
 
-INIT_BASE_SRCS = [SRCS + 'BaseInit.java'] + glob(
-    [SRCS + n for n in [
-      'init/**/*.java',
-      'util/**/*.java',
-    ]],
-    excludes = INIT_API_SRCS +
-      [SRCS + n for n in [
-        'init/Browser.java',
-        'util/ErrorLogFile.java',
-        'util/GarbageCollectionLogFile.java',
-        'util/LogFileCompressor.java',
-        'util/RuntimeShutdown.java',
-      ]]
-  )
-
-INIT_BASE_RSRCS = ['src/main/resources/com/google/gerrit/pgm/libraries.config']
-
 java_library(
-  name = 'init-base',
-  srcs = INIT_BASE_SRCS,
-  resources = INIT_BASE_RSRCS,
-  deps = [
+  name = 'init',
+  srcs = glob([SRCS + 'init/*.java']),
+  resources = glob([RSRCS + 'init/*']),
+  deps = DEPS + [
     ':init-api',
-    '//gerrit-common:server',
-    '//gerrit-extension-api:api',
+    ':util',
+    '//gerrit-common:annotations',
     '//gerrit-lucene:lucene',
-    '//gerrit-reviewdb:server',
-    '//gerrit-server:server',
-    '//gerrit-util-cli:cli',
-    '//lib/commons:dbcp',
-    '//lib/guice:guice',
-    '//lib/guice:guice-assistedinject',
-    '//lib/jgit:jgit',
-    '//lib/mina:sshd',
     '//lib:args4j',
-    '//lib:guava',
     '//lib:gwtjsonrpc',
     '//lib:gwtorm',
-    '//lib/log:api',
+    '//lib:h2',
+    '//lib/commons:validator',
+    '//lib/mina:sshd',
   ],
   provided_deps = ['//gerrit-launcher:launcher'],
   visibility = [
-    '//gerrit-war:',
     '//gerrit-acceptance-tests/...',
+    '//gerrit-war:',
   ],
 )
 
 java_library(
-  name = 'pgm',
-  srcs = glob(
-    ['src/main/java/**/*.java'],
-    excludes = INIT_API_SRCS + INIT_BASE_SRCS
-  ),
-  resources = glob(
-    ['src/main/resources/**/*'],
-    excludes = INIT_BASE_RSRCS),
-  deps = [
-    ':init-api',
-    ':init-base',
+  name = 'util',
+  srcs = glob([SRCS + 'util/*.java']),
+  deps = DEPS + [
     '//gerrit-cache-h2:cache-h2',
-    '//gerrit-common:server',
-    '//gerrit-extension-api:api',
-    '//gerrit-gwtexpui:linker_server',
-    '//gerrit-gwtexpui:server',
-    '//gerrit-httpd:httpd',
+    '//gerrit-util-cli:cli',
+    '//lib:args4j',
+    '//lib:gwtorm',
+    '//lib/commons:dbcp',
+  ],
+  visibility = [
+    '//gerrit-acceptance-tests/...',
+    '//gerrit-gwtdebug:gwtdebug',
+    '//gerrit-war:',
+  ],
+)
+
+java_library(
+  name = 'http',
+  srcs = glob([SRCS + 'http/**/*.java']),
+  deps = DEPS + [
+    '//lib/jetty:jmx',
+    '//lib/jetty:server',
+    '//lib/jetty:servlet',
+  ],
+  provided_deps = [
+    '//gerrit-launcher:launcher',
+    '//lib:servlet-api-3_1',
+  ],
+  visibility = ['//gerrit-war:'],
+)
+
+java_library(
+  name = 'pgm',
+  srcs = glob([SRCS + '*.java']),
+  resources = glob([RSRCS + '*']),
+  deps = DEPS + [
+    ':http',
+    ':init',
+    ':init-api',
+    ':util',
+    '//gerrit-cache-h2:cache-h2',
     '//gerrit-lucene:lucene',
     '//gerrit-oauth:oauth',
     '//gerrit-openid:openid',
-    '//gerrit-reviewdb:server',
-    '//gerrit-server:server',
-    '//gerrit-server/src/main/prolog:common',
     '//gerrit-solr:solr',
-    '//gerrit-sshd:sshd',
-    '//gerrit-util-cli:cli',
     '//lib:args4j',
-    '//lib:guava',
     '//lib:gwtorm',
-    '//lib:h2',
     '//lib:servlet-api-3_1',
-    '//lib/guice:guice',
-    '//lib/guice:guice-assistedinject',
-    '//lib/guice:guice-servlet',
-    '//lib/jetty:server',
-    '//lib/jetty:servlet',
-    '//lib/jetty:jmx',
-    '//lib/jgit:jgit',
-    '//lib/log:api',
-    '//lib/log:log4j',
-    '//lib/lucene:core',
     '//lib/prolog:prolog-cafe',
   ],
   provided_deps = ['//gerrit-launcher:launcher'],
   visibility = [
     '//:',
     '//gerrit-acceptance-tests/...',
+    '//gerrit-gwtdebug:gwtdebug',
     '//tools/eclipse:classpath',
     '//Documentation:licenses.txt',
   ],
@@ -137,8 +122,8 @@
   name = 'pgm_tests',
   srcs = glob(['src/test/java/**/*.java']),
   deps = [
+    ':init',
     ':init-api',
-    ':init-base',
     ':pgm',
     '//gerrit-server:server',
     '//lib:junit',
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index 1c667e3..f513348 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -17,10 +17,11 @@
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.gerrit.common.ChangeHookRunner;
 import com.google.gerrit.httpd.AllRequestFilter;
-import com.google.gerrit.httpd.GerritUiOptions;
+import com.google.gerrit.httpd.GerritOptions;
+import com.google.gerrit.httpd.GetUserFilter;
 import com.google.gerrit.httpd.GitOverHttpModule;
 import com.google.gerrit.httpd.H2CacheBasedWebSession;
 import com.google.gerrit.httpd.HttpCanonicalWebUrlProvider;
@@ -32,11 +33,9 @@
 import com.google.gerrit.httpd.plugins.HttpPluginModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.lucene.LuceneIndexModule;
-import com.google.gerrit.pgm.http.jetty.GetUserFilter;
 import com.google.gerrit.pgm.http.jetty.JettyEnv;
 import com.google.gerrit.pgm.http.jetty.JettyModule;
 import com.google.gerrit.pgm.http.jetty.ProjectQoSFilter;
-import com.google.gerrit.pgm.shell.JythonShell;
 import com.google.gerrit.pgm.util.ErrorLogFile;
 import com.google.gerrit.pgm.util.GarbageCollectionLogFile;
 import com.google.gerrit.pgm.util.LogFileCompressor;
@@ -45,7 +44,6 @@
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
-import com.google.gerrit.server.change.MergeabilityChecksExecutorModule;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.AuthConfigModule;
 import com.google.gerrit.server.config.CanonicalWebUrlModule;
@@ -54,6 +52,7 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.MasterNodeStartup;
 import com.google.gerrit.server.config.RestCacheAdminModule;
+import com.google.gerrit.server.contact.ContactStoreModule;
 import com.google.gerrit.server.contact.HttpContactStoreConnection;
 import com.google.gerrit.server.git.GarbageCollectionRunner;
 import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
@@ -63,11 +62,16 @@
 import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
 import com.google.gerrit.server.mail.SmtpEmailSender;
+import com.google.gerrit.server.mime.MimeUtil2Module;
 import com.google.gerrit.server.patch.DiffExecutorModule;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
 import com.google.gerrit.server.plugins.PluginRestApiModule;
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.SchemaVersionCheck;
+import com.google.gerrit.server.securestore.DefaultSecureStore;
+import com.google.gerrit.server.securestore.SecureStore;
+import com.google.gerrit.server.securestore.SecureStoreClassName;
+import com.google.gerrit.server.securestore.SecureStoreProvider;
 import com.google.gerrit.server.ssh.NoSshKeyCache;
 import com.google.gerrit.server.ssh.NoSshModule;
 import com.google.gerrit.server.ssh.SshAddressesModule;
@@ -106,7 +110,7 @@
   private Boolean httpd;
 
   @Option(name = "--disable-httpd", usage = "Disable the internal HTTP daemon")
-  void setDisableHttpd(final boolean arg) {
+  void setDisableHttpd(@SuppressWarnings("unused") boolean arg) {
     httpd = false;
   }
 
@@ -114,11 +118,11 @@
   private boolean sshd = true;
 
   @Option(name = "--disable-sshd", usage = "Disable the internal SSH daemon")
-  void setDisableSshd(final boolean arg) {
+  void setDisableSshd(@SuppressWarnings("unused")  boolean arg) {
     sshd = false;
   }
 
-  @Option(name = "--slave", usage = "Support fetch only; implies --disable-httpd")
+  @Option(name = "--slave", usage = "Support fetch only")
   private boolean slave;
 
   @Option(name = "--console-log", usage = "Log to console (not $site_path/logs)")
@@ -190,9 +194,6 @@
     if (!httpd && !sshd) {
       throw die("No services enabled, nothing to do");
     }
-    if (slave && httpd) {
-      throw die("Cannot combine --slave and --enable-httpd");
-    }
 
     manager.add(GarbageCollectionLogFile.start(getSitePath()));
     if (consoleLog) {
@@ -277,7 +278,7 @@
     cfgInjector = createCfgInjector();
     sysInjector = createSysInjector();
     sysInjector.getInstance(PluginGuiceEnvironment.class)
-      .setCfgInjector(cfgInjector);
+      .setDbCfgInjector(dbInjector, cfgInjector);
     manager.add(dbInjector, cfgInjector, sysInjector);
 
     sshd &= !sshdOff();
@@ -285,7 +286,7 @@
       initSshd();
     }
 
-    if (Objects.firstNonNull(httpd, true)) {
+    if (MoreObjects.firstNonNull(httpd, true)) {
       initHttpd();
     }
 
@@ -319,8 +320,8 @@
     modules.add(new WorkQueue.Module());
     modules.add(new ChangeHookRunner.Module());
     modules.add(new ReceiveCommitsExecutorModule());
-    modules.add(new MergeabilityChecksExecutorModule());
     modules.add(new DiffExecutorModule());
+    modules.add(new MimeUtil2Module());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
     modules.add(new InternalAccountDirectory.Module());
     modules.add(new DefaultCacheFactory.Module());
@@ -329,7 +330,7 @@
     modules.add(new PluginRestApiModule());
     modules.add(new RestCacheAdminModule());
     modules.add(createIndexModule());
-    if (Objects.firstNonNull(httpd, true)) {
+    if (MoreObjects.firstNonNull(httpd, true)) {
       modules.add(new CanonicalWebUrlModule() {
         @Override
         protected Class<? extends Provider<String>> provider() {
@@ -355,7 +356,12 @@
     modules.add(new AbstractModule() {
       @Override
       protected void configure() {
-        bind(GerritUiOptions.class).toInstance(new GerritUiOptions(headless));
+        bind(GerritOptions.class).toInstance(new GerritOptions(headless, slave));
+        if (test) {
+          bind(String.class).annotatedWith(SecureStoreClassName.class)
+              .toInstance(DefaultSecureStore.class.getName());
+          bind(SecureStore.class).toProvider(SecureStoreProvider.class);
+        }
       }
     });
     modules.add(GarbageCollectionRunner.module());
@@ -421,6 +427,7 @@
     modules.add(sysInjector.getInstance(GitOverHttpModule.class));
     modules.add(sysInjector.getInstance(WebModule.class));
     modules.add(new HttpPluginModule());
+    modules.add(new ContactStoreModule());
     if (sshd) {
       modules.add(sshInjector.getInstance(WebSshGlueModule.class));
     } else {
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 c30507f..38b1824 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
@@ -17,20 +17,23 @@
 import com.google.common.base.Function;
 import com.google.common.base.Joiner;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.IoUtil;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.PluginData;
+import com.google.gerrit.pgm.init.BaseInit;
 import com.google.gerrit.pgm.init.Browser;
 import com.google.gerrit.pgm.init.InitPlugins;
-import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.util.ErrorLogFile;
-import com.google.gerrit.pgm.util.IoUtil;
 import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.SitePath;
+import com.google.gerrit.server.securestore.SecureStoreClassName;
 import com.google.gerrit.server.util.HostPlatform;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Module;
+import com.google.inject.util.Providers;
 
 import org.kohsuke.args4j.Option;
 
@@ -47,7 +50,7 @@
   @Option(name = "--no-auto-start", usage = "Don't automatically start daemon after init")
   private boolean noAutoStart;
 
-  @Option(name = "--skip-plugins", usage = "Don't install plugin")
+  @Option(name = "--skip-plugins", usage = "Don't install plugins")
   private boolean skipPlugins = false;
 
   @Option(name = "--list-plugins", usage = "List available plugins")
@@ -56,6 +59,10 @@
   @Option(name = "--install-plugin", usage = "Install given plugin without asking")
   private List<String> installPlugins;
 
+  @Option(name = "--secure-store-lib",
+      usage = "Path to jar providing SecureStore implementation class")
+  private String secureStoreLib;
+
   @Inject
   Browser browser;
 
@@ -101,6 +108,8 @@
       protected void configure() {
         bind(File.class).annotatedWith(SitePath.class).toInstance(getSitePath());
         bind(Browser.class);
+        bind(String.class).annotatedWith(SecureStoreClassName.class)
+            .toProvider(Providers.of(getConfiguredSecureStoreClass()));
       }
     });
     modules.add(new GerritServerConfigModule());
@@ -128,6 +137,11 @@
     return skipPlugins;
   }
 
+  @Override
+  protected String getSecureStoreLib() {
+    return secureStoreLib;
+  }
+
   void start(SiteRun run) throws Exception {
     if (run.flags.autoStart) {
       if (HostPlatform.isWin32()) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/shell/JythonShell.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/JythonShell.java
similarity index 99%
rename from gerrit-pgm/src/main/java/com/google/gerrit/pgm/shell/JythonShell.java
rename to gerrit-pgm/src/main/java/com/google/gerrit/pgm/JythonShell.java
index 5b537ae..547c1a9 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/shell/JythonShell.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/JythonShell.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.shell;
+package com.google.gerrit.pgm;
 
 import com.google.gerrit.launcher.GerritLauncher;
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNotedb.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNotedb.java
new file mode 100644
index 0000000..ab20f53
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNotedb.java
@@ -0,0 +1,296 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm;
+
+import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.pgm.util.BatchProgramModule;
+import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.pgm.util.ThreadLimiter;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MultiProgressMonitor;
+import com.google.gerrit.server.git.MultiProgressMonitor.Task;
+import com.google.gerrit.server.git.SearchingChangeCacheImpl;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.index.DummyIndexModule;
+import com.google.gerrit.server.index.ReindexAfterUpdate;
+import com.google.gerrit.server.notedb.ChangeRebuilder;
+import com.google.gerrit.server.notedb.NoteDbModule;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.AbstractModule;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.TypeLiteral;
+
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class RebuildNotedb extends SiteProgram {
+  private static final Logger log =
+      LoggerFactory.getLogger(RebuildNotedb.class);
+
+  @Option(name = "--threads", usage = "Number of threads to use for indexing")
+  private int threads = Runtime.getRuntime().availableProcessors();
+
+  private Injector dbInjector;
+  private Injector sysInjector;
+
+  @Override
+  public int run() throws Exception {
+    mustHaveValidSite();
+    dbInjector = createDbInjector(MULTI_USER);
+    threads = ThreadLimiter.limitThreads(dbInjector, threads);
+
+    LifecycleManager dbManager = new LifecycleManager();
+    dbManager.add(dbInjector);
+    dbManager.start();
+
+    sysInjector = createSysInjector();
+    NotesMigration notesMigration = sysInjector.getInstance(
+        NotesMigration.class);
+    if (!notesMigration.enabled()) {
+      die("Notedb is not enabled.");
+    }
+    LifecycleManager sysManager = new LifecycleManager();
+    sysManager.add(sysInjector);
+    sysManager.start();
+
+    ListeningExecutorService executor = newExecutor();
+    System.out.println("Rebuilding the notedb");
+    ChangeRebuilder rebuilder = sysInjector.getInstance(ChangeRebuilder.class);
+
+    Multimap<Project.NameKey, Change> changesByProject = getChangesByProject();
+    final AtomicBoolean ok = new AtomicBoolean(true);
+    Stopwatch sw = Stopwatch.createStarted();
+    GitRepositoryManager repoManager =
+        sysInjector.getInstance(GitRepositoryManager.class);
+    final Project.NameKey allUsersName =
+        sysInjector.getInstance(AllUsersName.class);
+    final Repository allUsersRepo =
+        repoManager.openMetadataRepository(allUsersName);
+    try {
+      deleteDraftRefs(allUsersRepo);
+      for (final Project.NameKey project : changesByProject.keySet()) {
+        final Repository repo = repoManager.openMetadataRepository(project);
+        try {
+          final BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
+          final BatchRefUpdate bruForDrafts =
+              allUsersRepo.getRefDatabase().newBatchUpdate();
+          List<ListenableFuture<?>> futures = Lists.newArrayList();
+
+          // Here, we truncate the project name to 50 characters to ensure that
+          // the whole monitor line for a project fits on one line (<80 chars).
+          final MultiProgressMonitor mpm = new MultiProgressMonitor(System.out,
+              truncateProjectName(project.get()));
+          final Task doneTask =
+              mpm.beginSubTask("done", changesByProject.get(project).size());
+          final Task failedTask =
+              mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
+
+          for (final Change c : changesByProject.get(project)) {
+            final ListenableFuture<?> future = rebuilder.rebuildAsync(c,
+                executor, bru, bruForDrafts, repo, allUsersRepo);
+            futures.add(future);
+            future.addListener(
+                new RebuildListener(c.getId(), future, ok, doneTask, failedTask),
+                MoreExecutors.directExecutor());
+          }
+
+          mpm.waitFor(Futures.transform(Futures.successfulAsList(futures),
+              new AsyncFunction<List<?>, Void>() {
+                  @Override
+                public ListenableFuture<Void> apply(List<?> input)
+                    throws Exception {
+                  execute(bru, repo);
+                  execute(bruForDrafts, allUsersRepo);
+                  mpm.end();
+                  return Futures.immediateFuture(null);
+                }
+              }));
+        } catch (Exception e) {
+          log.error("Error rebuilding notedb", e);
+          ok.set(false);
+          break;
+        } finally {
+          repo.close();
+        }
+      }
+    } finally {
+      allUsersRepo.close();
+    }
+
+    double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d;
+    System.out.format("Rebuild %d changes in %.01fs (%.01f/s)\n",
+        changesByProject.size(), t, changesByProject.size() / t);
+    return ok.get() ? 0 : 1;
+  }
+
+  private static String truncateProjectName(String projectName) {
+    int monitorStringMaxLength = 50;
+    String monitorString = (projectName.length() > monitorStringMaxLength)
+        ? projectName.substring(0, monitorStringMaxLength)
+        : projectName;
+    if (projectName.length() > monitorString.length()) {
+      monitorString = monitorString + "...";
+    }
+    return monitorString;
+  }
+
+  private static void execute(BatchRefUpdate bru, Repository repo)
+      throws IOException {
+    try (RevWalk rw = new RevWalk(repo)) {
+      bru.execute(rw, NullProgressMonitor.INSTANCE);
+    }
+  }
+
+  private void deleteDraftRefs(Repository allUsersRepo) throws IOException {
+    RefDatabase refDb = allUsersRepo.getRefDatabase();
+    Map<String, Ref> allRefs = refDb.getRefs(RefNames.REFS_DRAFT_COMMENTS);
+    BatchRefUpdate bru = refDb.newBatchUpdate();
+    for (Map.Entry<String, Ref> ref : allRefs.entrySet()) {
+      bru.addCommand(new ReceiveCommand(ref.getValue().getObjectId(),
+          ObjectId.zeroId(), RefNames.REFS_DRAFT_COMMENTS + ref.getKey()));
+    }
+    execute(bru, allUsersRepo);
+  }
+
+  private Injector createSysInjector() {
+    return dbInjector.createChildInjector(new AbstractModule() {
+      @Override
+      public void configure() {
+        install(dbInjector.getInstance(BatchProgramModule.class));
+        install(SearchingChangeCacheImpl.module());
+        install(new NoteDbModule());
+        DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(
+            ReindexAfterUpdate.class);
+        install(new DummyIndexModule());
+      }
+    });
+  }
+
+  private ListeningExecutorService newExecutor() {
+    if (threads > 0) {
+      return MoreExecutors.listeningDecorator(
+          dbInjector.getInstance(WorkQueue.class)
+            .createQueue(threads, "RebuildChange"));
+    } else {
+      return MoreExecutors.newDirectExecutorService();
+    }
+  }
+
+  private Multimap<Project.NameKey, Change> getChangesByProject()
+      throws OrmException {
+    // Memorize all changes so we can close the db connection and allow
+    // rebuilder threads to use the full connection pool.
+    SchemaFactory<ReviewDb> schemaFactory = sysInjector.getInstance(Key.get(
+        new TypeLiteral<SchemaFactory<ReviewDb>>() {}));
+    ReviewDb db = schemaFactory.open();
+    Multimap<Project.NameKey, Change> changesByProject =
+        ArrayListMultimap.create();
+    try {
+      for (Change c : db.changes().all()) {
+        changesByProject.put(c.getProject(), c);
+      }
+      return changesByProject;
+    } finally {
+      db.close();
+    }
+  }
+
+  private class RebuildListener implements Runnable {
+    private Change.Id changeId;
+    private ListenableFuture<?> future;
+    private AtomicBoolean ok;
+    private Task doneTask;
+    private Task failedTask;
+
+
+    private RebuildListener(Change.Id changeId, ListenableFuture<?> future,
+        AtomicBoolean ok, Task doneTask, Task failedTask) {
+      this.changeId = changeId;
+      this.future = future;
+      this.ok = ok;
+      this.doneTask = doneTask;
+      this.failedTask = failedTask;
+    }
+
+    @Override
+    public void run() {
+      try {
+        future.get();
+        doneTask.update(1);
+      } catch (ExecutionException | InterruptedException e) {
+        fail(e);
+      } catch (RuntimeException e) {
+        failAndThrow(e);
+      } catch (Error e) {
+        // Can't join with RuntimeException because "RuntimeException
+        // | Error" becomes Throwable, which messes with signatures.
+        failAndThrow(e);
+      }
+    }
+
+    private void fail(Throwable t) {
+      log.error("Failed to rebuild change " + changeId, t);
+      ok.set(false);
+      failedTask.update(1);
+    }
+
+    private void failAndThrow(RuntimeException e) {
+      fail(e);
+      throw e;
+    }
+
+    private void failAndThrow(Error e) {
+      fail(e);
+      throw e;
+    }
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
index 161efc1..44f80f2 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
@@ -15,103 +15,42 @@
 package com.google.gerrit.pgm;
 
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
-import static com.google.inject.Scopes.SINGLETON;
 
-import com.google.common.cache.Cache;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
-import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.Die;
-import com.google.gerrit.common.DisabledChangeHooks;
-import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
-import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.lucene.LuceneIndexModule;
+import com.google.gerrit.pgm.util.BatchProgramModule;
 import com.google.gerrit.pgm.util.SiteProgram;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.pgm.util.ThreadLimiter;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.rules.PrologModule;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountByEmailCacheImpl;
-import com.google.gerrit.server.account.AccountCacheImpl;
-import com.google.gerrit.server.account.CapabilityControl;
-import com.google.gerrit.server.account.GroupCacheImpl;
-import com.google.gerrit.server.account.GroupIncludeCacheImpl;
-import com.google.gerrit.server.cache.CacheRemovalListener;
-import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
-import com.google.gerrit.server.change.ChangeKindCacheImpl;
-import com.google.gerrit.server.change.MergeabilityChecker;
-import com.google.gerrit.server.change.MergeabilityChecksExecutor;
-import com.google.gerrit.server.change.MergeabilityChecksExecutor.Priority;
-import com.google.gerrit.server.change.PatchSetInserter;
-import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.config.CanonicalWebUrlProvider;
-import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.GitReceivePackGroups;
-import com.google.gerrit.server.config.GitUploadPackGroups;
-import com.google.gerrit.server.git.GitModule;
-import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.git.validators.CommitValidationListener;
-import com.google.gerrit.server.git.validators.CommitValidators;
-import com.google.gerrit.server.group.GroupModule;
-import com.google.gerrit.server.index.ChangeBatchIndexer;
+import com.google.gerrit.server.git.ScanningChangeCacheImpl;
 import com.google.gerrit.server.index.ChangeIndex;
 import com.google.gerrit.server.index.ChangeSchemas;
 import com.google.gerrit.server.index.IndexCollection;
 import com.google.gerrit.server.index.IndexModule;
 import com.google.gerrit.server.index.IndexModule.IndexType;
-import com.google.gerrit.server.mail.ReplacePatchSetSender;
-import com.google.gerrit.server.notedb.NoteDbModule;
-import com.google.gerrit.server.patch.DiffExecutorModule;
-import com.google.gerrit.server.patch.PatchListCacheImpl;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.CommentLinkInfo;
-import com.google.gerrit.server.project.CommentLinkProvider;
-import com.google.gerrit.server.project.ProjectCacheImpl;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.project.SectionSortCache;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.schema.DataSourceProvider;
-import com.google.gerrit.server.schema.DataSourceType;
+import com.google.gerrit.server.index.SiteIndexer;
 import com.google.gerrit.solr.SolrIndexModule;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.AbstractModule;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Module;
-import com.google.inject.Provider;
-import com.google.inject.Provides;
-import com.google.inject.ProvisionException;
-import com.google.inject.Singleton;
-import com.google.inject.TypeLiteral;
-import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.TextProgressMonitor;
 import org.eclipse.jgit.util.io.NullOutputStream;
 import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 public class Reindex extends SiteProgram {
-  private static final Logger log = LoggerFactory.getLogger(Reindex.class);
-
   @Option(name = "--threads", usage = "Number of threads to use for indexing")
   private int threads = Runtime.getRuntime().availableProcessors();
 
@@ -122,9 +61,6 @@
   @Option(name = "--output", usage = "Prefix for output; path for local disk index, or prefix for remote index")
   private String outputBase;
 
-  @Option(name = "--recheck-mergeable", usage = "Recheck mergeable flag on all changes")
-  private boolean recheckMergeable;
-
   @Option(name = "--verbose", usage = "Output debug information for each change")
   private boolean verbose;
 
@@ -132,19 +68,20 @@
   private boolean dryRun;
 
   private Injector dbInjector;
-  private Config cfg;
   private Injector sysInjector;
+  private Config globalConfig;
   private ChangeIndex index;
 
   @Override
   public int run() throws Exception {
     mustHaveValidSite();
     dbInjector = createDbInjector(MULTI_USER);
-    cfg = dbInjector.getInstance(
-        Key.get(Config.class, GerritServerConfig.class));
+    globalConfig =
+        dbInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
+    threads = ThreadLimiter.limitThreads(dbInjector, threads);
     checkNotSlaveMode();
-    limitThreads();
     disableLuceneAutomaticCommit();
+    disableChangeCache();
     if (version == null) {
       version = ChangeSchemas.getLatest().getVersion();
     }
@@ -173,28 +110,14 @@
   }
 
   private void checkNotSlaveMode() throws Die {
-    if (cfg.getBoolean("container", "slave", false)) {
+    if (globalConfig.getBoolean("container", "slave", false)) {
       throw die("Cannot run reindex in slave mode");
     }
   }
 
-  private void limitThreads() {
-    boolean usePool = cfg.getBoolean("database", "connectionpool",
-        dbInjector.getInstance(DataSourceType.class).usePool());
-    int poolLimit = cfg.getInt("database", "poollimit",
-        DataSourceProvider.DEFAULT_POOL_LIMIT);
-    if (usePool && threads > poolLimit) {
-      log.warn("Limiting reindexing to " + poolLimit
-          + " threads due to database.poolLimit");
-      threads = poolLimit;
-    }
-  }
-
   private Injector createSysInjector() {
     List<Module> modules = Lists.newArrayList();
-    modules.add(new DiffExecutorModule());
-    modules.add(PatchListCacheImpl.module());
-    AbstractModule changeIndexModule;
+    Module changeIndexModule;
     switch (IndexModule.getIndexType(dbInjector)) {
       case LUCENE:
         changeIndexModule = new LuceneIndexModule(version, threads, outputBase);
@@ -206,142 +129,23 @@
         throw new IllegalStateException("unsupported index.type");
     }
     modules.add(changeIndexModule);
-    modules.add(new ReviewDbModule());
-    modules.add(new FactoryModule() {
-      @SuppressWarnings("rawtypes")
-      @Override
-      protected void configure() {
-        // Plugins are not loaded and we're just running through each change
-        // once, so don't worry about cache removal.
-        bind(new TypeLiteral<DynamicSet<CacheRemovalListener>>() {})
-            .toInstance(DynamicSet.<CacheRemovalListener> emptySet());
-        bind(new TypeLiteral<DynamicMap<Cache<?, ?>>>() {})
-            .toInstance(DynamicMap.<Cache<?, ?>> emptyMap());
-        bind(new TypeLiteral<List<CommentLinkInfo>>() {})
-            .toProvider(CommentLinkProvider.class).in(SINGLETON);
-        bind(String.class).annotatedWith(CanonicalWebUrl.class)
-            .toProvider(CanonicalWebUrlProvider.class);
-        bind(IdentifiedUser.class)
-          .toProvider(Providers. <IdentifiedUser>of(null));
-        bind(CurrentUser.class).to(IdentifiedUser.class);
-
-        bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
-            .annotatedWith(GitUploadPackGroups.class)
-            .toInstance(Collections.<AccountGroup.UUID> emptySet());
-        bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
-            .annotatedWith(GitReceivePackGroups.class)
-            .toInstance(Collections.<AccountGroup.UUID> emptySet());
-        factory(ChangeControl.AssistedFactory.class);
-        factory(ProjectControl.AssistedFactory.class);
-
-        install(new DefaultCacheFactory.Module());
-        install(new GroupModule());
-        install(new PrologModule());
-        install(AccountByEmailCacheImpl.module());
-        install(AccountCacheImpl.module());
-        install(GroupCacheImpl.module());
-        install(GroupIncludeCacheImpl.module());
-        install(ProjectCacheImpl.module());
-        install(SectionSortCache.module());
-        install(ChangeKindCacheImpl.module());
-        factory(CapabilityControl.Factory.class);
-        factory(ChangeData.Factory.class);
-        factory(ProjectState.Factory.class);
-
-        if (recheckMergeable) {
-          install(new MergeabilityModule());
-        } else {
-          bind(MergeabilityChecker.class)
-              .toProvider(Providers.<MergeabilityChecker> of(null));
-        }
-      }
-    });
+    // Scan changes from git instead of relying on the secondary index, as we
+    // will have just deleted the old (possibly corrupt) index.
+    modules.add(ScanningChangeCacheImpl.module());
+    modules.add(dbInjector.getInstance(BatchProgramModule.class));
 
     return dbInjector.createChildInjector(modules);
   }
 
   private void disableLuceneAutomaticCommit() {
-    Config cfg =
-        dbInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
     if (IndexModule.getIndexType(dbInjector) == IndexType.LUCENE) {
-      cfg.setLong("index", "changes_open", "commitWithin", -1);
-      cfg.setLong("index", "changes_closed", "commitWithin", -1);
+      globalConfig.setLong("index", "changes_open", "commitWithin", -1);
+      globalConfig.setLong("index", "changes_closed", "commitWithin", -1);
     }
   }
 
-  private class ReviewDbModule extends LifecycleModule {
-    @Override
-    protected void configure() {
-      final SchemaFactory<ReviewDb> schema = dbInjector.getInstance(
-          Key.get(new TypeLiteral<SchemaFactory<ReviewDb>>() {}));
-      final List<ReviewDb> dbs = Collections.synchronizedList(
-          Lists.<ReviewDb> newArrayListWithCapacity(threads + 1));
-      final ThreadLocal<ReviewDb> localDb = new ThreadLocal<>();
-
-      bind(ReviewDb.class).toProvider(new Provider<ReviewDb>() {
-        @Override
-        public ReviewDb get() {
-          ReviewDb db = localDb.get();
-          if (db == null) {
-            try {
-              db = schema.open();
-              dbs.add(db);
-              localDb.set(db);
-            } catch (OrmException e) {
-              throw new ProvisionException("unable to open ReviewDb", e);
-            }
-          }
-          return db;
-        }
-      });
-      listener().toInstance(new LifecycleListener() {
-        @Override
-        public void start() {
-          // Do nothing.
-        }
-
-        @Override
-        public void stop() {
-          for (ReviewDb db : dbs) {
-            db.close();
-          }
-        }
-      });
-    }
-  }
-
-  private static class MergeabilityModule extends FactoryModule {
-    @Override
-    public void configure() {
-      factory(PatchSetInserter.Factory.class);
-      bind(ChangeHooks.class).to(DisabledChangeHooks.class);
-      bind(ReplacePatchSetSender.Factory.class).toProvider(
-          Providers.<ReplacePatchSetSender.Factory>of(null));
-
-      factory(MergeUtil.Factory.class);
-      DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
-      DynamicSet.setOf(binder(), CommitValidationListener.class);
-      factory(CommitValidators.Factory.class);
-      install(new GitModule());
-      install(new NoteDbModule());
-    }
-
-    @Provides
-    @Singleton
-    @MergeabilityChecksExecutor(Priority.BACKGROUND)
-    public WorkQueue.Executor createMergeabilityChecksExecutor(
-        WorkQueue queues) {
-      return queues.createQueue(1, "MergeabilityChecks");
-    }
-
-    @Provides
-    @Singleton
-    @MergeabilityChecksExecutor(Priority.INTERACTIVE)
-    public WorkQueue.Executor createInteractiveMergeabilityChecksExecutor(
-        @MergeabilityChecksExecutor(Priority.BACKGROUND)
-          WorkQueue.Executor bg) {
-      return bg;
-    }
+  private void disableChangeCache() {
+    globalConfig.setLong("cache", "changes", "maximumWeight", 0);
   }
 
   private int indexAll() throws Exception {
@@ -363,11 +167,12 @@
     }
     pm.endTask();
 
-    ChangeBatchIndexer batchIndexer =
-        sysInjector.getInstance(ChangeBatchIndexer.class);
-    ChangeBatchIndexer.Result result = batchIndexer.indexAll(
-      index, projects, projects.size(), changeCount, System.err,
-      verbose ? System.out : NullOutputStream.INSTANCE);
+    SiteIndexer batchIndexer =
+        sysInjector.getInstance(SiteIndexer.class);
+    SiteIndexer.Result result = batchIndexer.setNumChanges(changeCount)
+        .setProgressOut(System.err)
+        .setVerboseOut(verbose ? System.out : NullOutputStream.INSTANCE)
+        .indexAll(index, projects);
     int n = result.doneCount() + result.failedCount();
     double t = result.elapsed(TimeUnit.MILLISECONDS) / 1000d;
     System.out.format("Reindexed %d changes in %.01fs (%.01f/s)\n", n, t, n/t);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SwitchSecureStore.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SwitchSecureStore.java
new file mode 100644
index 0000000..f2feae1
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SwitchSecureStore.java
@@ -0,0 +1,217 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm;
+
+import static com.google.gerrit.server.schema.DataSourceProvider.Context.SINGLE_USER;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.io.Files;
+import com.google.gerrit.common.IoUtil;
+import com.google.gerrit.common.SiteLibraryLoaderUtil;
+import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.plugins.JarScanner;
+import com.google.gerrit.server.securestore.DefaultSecureStore;
+import com.google.gerrit.server.securestore.SecureStore;
+import com.google.gerrit.server.securestore.SecureStore.EntryKey;
+import com.google.inject.Injector;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.jar.JarFile;
+import java.util.zip.ZipEntry;
+
+public class SwitchSecureStore extends SiteProgram {
+  private static String getSecureStoreClassFromGerritConfig(SitePaths sitePaths) {
+    FileBasedConfig cfg =
+        new FileBasedConfig(sitePaths.gerrit_config, FS.DETECTED);
+    try {
+      cfg.load();
+    } catch (IOException | ConfigInvalidException e) {
+      throw new RuntimeException("Cannot read gerrit.config file", e);
+    }
+    return cfg.getString("gerrit", null, "secureStoreClass");
+  }
+
+  private static final Logger log = LoggerFactory
+      .getLogger(SwitchSecureStore.class);
+
+  @Option(name = "--new-secure-store-lib",
+      usage = "Path to new SecureStore implementation",
+      required = true)
+  private String newSecureStoreLib;
+
+  @Override
+  public int run() throws Exception {
+    SitePaths sitePaths = new SitePaths(getSitePath());
+    File newSecureStoreFile = new File(newSecureStoreLib);
+    if (!newSecureStoreFile.exists()) {
+      log.error(String.format("File %s doesn't exists",
+          newSecureStoreFile.getAbsolutePath()));
+      return -1;
+    }
+
+    String newSecureStore = getNewSecureStoreClassName(newSecureStoreFile);
+    String currentSecureStoreName = getCurrentSecureStoreClassName(sitePaths);
+
+    if (currentSecureStoreName.equals(newSecureStore)) {
+      log.error("Old and new SecureStore implementation names "
+          + "are the same. Migration will not work");
+      return -1;
+    }
+
+    IoUtil.loadJARs(newSecureStoreFile);
+    SiteLibraryLoaderUtil.loadSiteLib(sitePaths.lib_dir);
+
+    log.info("Current secureStoreClass property ({}) will be replaced with {}",
+        currentSecureStoreName, newSecureStore);
+    Injector dbInjector = createDbInjector(SINGLE_USER);
+    SecureStore currentStore =
+        getSecureStore(currentSecureStoreName, dbInjector);
+    SecureStore newStore = getSecureStore(newSecureStore, dbInjector);
+
+    migrateProperties(currentStore, newStore);
+
+    removeOldLib(sitePaths, currentSecureStoreName);
+    copyNewLib(sitePaths, newSecureStoreFile);
+
+    updateGerritConfig(sitePaths, newSecureStore);
+
+    return 0;
+  }
+
+  private void migrateProperties(SecureStore currentStore, SecureStore newStore) {
+    log.info("Migrate entries");
+    for (EntryKey key : currentStore.list()) {
+      String[] value =
+          currentStore.getList(key.section, key.subsection, key.name);
+      if (value != null) {
+        newStore.setList(key.section, key.subsection, key.name,
+            Arrays.asList(value));
+      } else {
+        String msg =
+            String.format("Cannot migrate entry for %s", key.section);
+        if (key.subsection != null) {
+          msg = msg + String.format(".%s", key.subsection);
+        }
+        msg = msg + String.format(".%s", key.name);
+        throw new RuntimeException(msg);
+      }
+    }
+  }
+
+  private void removeOldLib(SitePaths sitePaths, String currentSecureStoreName) {
+    File oldSecureStore =
+        findJarWithSecureStore(sitePaths, currentSecureStoreName);
+    if (oldSecureStore != null) {
+      log.info("Removing old SecureStore ({}) from lib/ directory",
+          oldSecureStore.getName());
+      if (!oldSecureStore.delete()) {
+        log.error("Cannot remove {}", oldSecureStore.getAbsolutePath());
+      }
+    } else {
+      log.info("Cannot find jar with old SecureStore ({}) in lib/ directory",
+          currentSecureStoreName);
+    }
+  }
+
+  private void copyNewLib(SitePaths sitePaths, File newSecureStoreFile)
+      throws IOException {
+    log.info("Copy new SecureStore ({}) into lib/ directory",
+        newSecureStoreFile.getName());
+    Files.copy(newSecureStoreFile, new File(sitePaths.lib_dir,
+        newSecureStoreFile.getName()));
+  }
+
+  private void updateGerritConfig(SitePaths sitePaths, String newSecureStore)
+      throws IOException, ConfigInvalidException {
+    log.info("Set gerrit.secureStoreClass property of gerrit.config to {}",
+        newSecureStore);
+    FileBasedConfig config =
+        new FileBasedConfig(sitePaths.gerrit_config, FS.DETECTED);
+    config.load();
+    config.setString("gerrit", null, "secureStoreClass", newSecureStore);
+    config.save();
+  }
+
+  private String getNewSecureStoreClassName(File secureStore)
+      throws IOException {
+    JarScanner scanner = new JarScanner(secureStore);
+    List<String> newSecureStores =
+        scanner.findSubClassesOf(SecureStore.class);
+    if (newSecureStores.isEmpty()) {
+      throw new RuntimeException(String.format(
+          "Cannot find implementation of SecureStore interface in %s",
+          secureStore.getAbsolutePath()));
+    }
+    if (newSecureStores.size() > 1) {
+      throw new RuntimeException(String.format(
+          "Found too many implementations of SecureStore:\n%s\nin %s", Joiner
+              .on("\n").join(newSecureStores), secureStore.getAbsolutePath()));
+    }
+    return Iterables.getOnlyElement(newSecureStores);
+  }
+
+  private String getCurrentSecureStoreClassName(SitePaths sitePaths) {
+    String current = getSecureStoreClassFromGerritConfig(sitePaths);
+    if (!Strings.isNullOrEmpty(current)) {
+      return current;
+    }
+    return DefaultSecureStore.class.getName();
+  }
+
+  private SecureStore getSecureStore(String className, Injector injector) {
+    try {
+      @SuppressWarnings("unchecked")
+      Class<? extends SecureStore> clazz =
+          (Class<? extends SecureStore>) Class.forName(className);
+      return injector.getInstance(clazz);
+    } catch (ClassNotFoundException e) {
+      throw new RuntimeException(
+          String.format("Cannot load SecureStore implementation: %s", className), e);
+    }
+  }
+
+  private File findJarWithSecureStore(SitePaths sitePaths,
+      String secureStoreClass) {
+    File[] jars = SiteLibraryLoaderUtil.listJars(sitePaths.lib_dir);
+    if (jars == null || jars.length == 0) {
+      return null;
+    }
+    String secureStoreClassPath = secureStoreClass.replace('.', '/') + ".class";
+    for (File jar : jars) {
+      try (JarFile jarFile = new JarFile(jar)) {
+        ZipEntry entry = jarFile.getEntry(secureStoreClassPath);
+        if (entry != null) {
+          return jar;
+        }
+      } catch (IOException e) {
+        log.error(e.getMessage(), e);
+      }
+    }
+    return null;
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/WarDistribution.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/WarDistribution.java
index 2c34711..3328a54 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/WarDistribution.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/WarDistribution.java
@@ -49,8 +49,9 @@
             String pluginJarName = new File(ze.getName()).getName();
             String pluginName = pluginJarName.substring(0,
                 pluginJarName.length() - JAR.length());
-            final InputStream in = zf.getInputStream(ze);
-            processor.process(pluginName, in);
+            try (InputStream in = zf.getInputStream(ze)) {
+              processor.process(pluginName, in);
+            }
           }
         }
       }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
index 5ca53df..3b0a590 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
@@ -35,6 +35,7 @@
 class HiddenErrorHandler extends ErrorHandler {
   private static final Logger log = LoggerFactory.getLogger(HiddenErrorHandler.class);
 
+  @Override
   public void handle(String target, Request baseRequest,
       HttpServletRequest req, HttpServletResponse res) throws IOException {
     HttpConnection conn = HttpConnection.getCurrentConnection();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
index ba97d4a..a326919 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
@@ -14,10 +14,9 @@
 
 package com.google.gerrit.pgm.http.jetty;
 
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.httpd.GetUserFilter;
 import com.google.gerrit.server.util.SystemLog;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.inject.Inject;
 
 import org.apache.log4j.AsyncAppender;
@@ -66,11 +65,6 @@
 
   @Override
   public void log(final Request req, final Response rsp) {
-    CurrentUser user = (CurrentUser) req.getAttribute(GetUserFilter.REQ_ATTR_KEY);
-    doLog(req, rsp, user);
-  }
-
-  private void doLog(Request req, Response rsp, CurrentUser user) {
     final LoggingEvent event = new LoggingEvent( //
         Logger.class.getName(), // fqnOfCategoryClass
         log, // logger
@@ -90,13 +84,9 @@
       uri = uri + "?" + qs;
     }
 
-    if (user != null && user.isIdentifiedUser()) {
-      IdentifiedUser who = (IdentifiedUser) user;
-      if (who.getUserName() != null && !who.getUserName().isEmpty()) {
-        event.setProperty(P_USER, who.getUserName());
-      } else {
-        event.setProperty(P_USER, "a/" + who.getAccountId());
-      }
+    String user = (String) req.getAttribute(GetUserFilter.REQ_ATTR_KEY);
+    if (user != null) {
+      event.setProperty(P_USER, user);
     }
 
     set(event, P_HOST, req.getRemoteAddr());
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 1d6d2bb..907624d 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -18,19 +18,18 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.base.Charsets;
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.escape.Escaper;
 import com.google.common.html.HtmlEscapers;
 import com.google.common.io.ByteStreams;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.launcher.GerritLauncher;
 import com.google.gerrit.pgm.http.jetty.HttpLog.HttpLogFactory;
 import com.google.gerrit.reviewdb.client.AuthType;
-import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtexpui.linker.server.UserAgentRule;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
@@ -198,7 +197,7 @@
     final URI[] listenUrls = listenURLs(cfg);
     final boolean reuseAddress = cfg.getBoolean("httpd", "reuseaddress", true);
     final int acceptors = cfg.getInt("httpd", "acceptorThreads", 2);
-    final AuthType authType = ConfigUtil.getEnum(cfg, "auth", null, "type", AuthType.OPENID);
+    final AuthType authType = cfg.getEnum("auth", null, "type", AuthType.OPENID);
 
     reverseProxy = isReverseProxied(listenUrls);
     final Connector[] connectors = new Connector[listenUrls.length];
@@ -579,7 +578,10 @@
     p.deleteOnExit();
 
     app.addFilter(new FilterHolder(new Filter() {
+      private final boolean gwtuiRecompile =
+          System.getProperty("gerrit.disable-gwtui-recompile") == null;
       private final UserAgentRule rule = new UserAgentRule();
+      private final Set<String> uaInitialized = new HashSet<>();
       private String lastTarget;
       private long lastTime;
 
@@ -588,30 +590,32 @@
           FilterChain chain) throws IOException, ServletException {
         String pkg = "gerrit-gwtui";
         String target = "ui_" + rule.select((HttpServletRequest) request);
-        String rule = "//" + pkg + ":" + target;
-        // TODO(davido): instead of assuming specific Buck's internal
-        // target directory for gwt_binary() artifacts, ask Buck for
-        // the location of user agent permutation GWT zip, e. g.:
-        // $ buck targets --show_output //gerrit-gwtui:ui_safari \
-        //    | awk '{print $2}'
-        String child = String.format("%s/__gwt_binary_%s__", pkg, target);
-        File zip = new File(new File(gen, child), target + ".zip");
+        if (gwtuiRecompile || !uaInitialized.contains(target)) {
+          String rule = "//" + pkg + ":" + target;
+          // TODO(davido): instead of assuming specific Buck's internal
+          // target directory for gwt_binary() artifacts, ask Buck for
+          // the location of user agent permutation GWT zip, e. g.:
+          // $ buck targets --show_output //gerrit-gwtui:ui_safari \
+          //    | awk '{print $2}'
+          String child = String.format("%s/__gwt_binary_%s__", pkg, target);
+          File zip = new File(new File(gen, child), target + ".zip");
 
-        synchronized (this) {
-          try {
-            build(root, gen, rule);
-          } catch (BuildFailureException e) {
-            displayFailure(rule, e.why, (HttpServletResponse) res);
-            return;
-          }
+          synchronized (this) {
+            try {
+              build(root, gen, rule);
+            } catch (BuildFailureException e) {
+              displayFailure(rule, e.why, (HttpServletResponse) res);
+              return;
+            }
 
-          if (!target.equals(lastTarget) || lastTime != zip.lastModified()) {
-            lastTarget = target;
-            lastTime = zip.lastModified();
-            unpack(zip, dstwar);
+            if (!target.equals(lastTarget) || lastTime != zip.lastModified()) {
+              lastTarget = target;
+              lastTime = zip.lastModified();
+              unpack(zip, dstwar);
+            }
           }
+          uaInitialized.add(target);
         }
-
         chain.doFilter(request, res);
       }
 
@@ -648,7 +652,7 @@
       throws IOException, BuildFailureException {
     log.info("buck build " + target);
     Properties properties = loadBuckProperties(gen);
-    String buck = Objects.firstNonNull(properties.getProperty("buck"), "buck");
+    String buck = MoreObjects.firstNonNull(properties.getProperty("buck"), "buck");
     ProcessBuilder proc = new ProcessBuilder(buck, "build", target)
         .directory(root)
         .redirectErrorStream(true);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AllUsersNameOnInitProvider.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AllUsersNameOnInitProvider.java
index 4c65218a..29e6166 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AllUsersNameOnInitProvider.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AllUsersNameOnInitProvider.java
@@ -14,8 +14,9 @@
 
 package com.google.gerrit.pgm.init;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -26,10 +27,11 @@
   @Inject
   AllUsersNameOnInitProvider(Section.Factory sections) {
     String n = sections.get("gerrit", null).get("allUsers");
-    name = Objects.firstNonNull(
+    name = MoreObjects.firstNonNull(
         Strings.emptyToNull(n), AllUsersNameProvider.DEFAULT);
   }
 
+  @Override
   public String get() {
     return name;
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/BaseInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
similarity index 70%
rename from gerrit-pgm/src/main/java/com/google/gerrit/pgm/BaseInit.java
rename to gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
index 9638db6..c9e76c8 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/BaseInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -12,27 +12,31 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm;
+package com.google.gerrit.pgm.init;
 
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.SINGLE_USER;
+import static com.google.inject.Scopes.SINGLETON;
 import static com.google.inject.Stage.PRODUCTION;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Die;
-import com.google.gerrit.pgm.init.InitFlags;
-import com.google.gerrit.pgm.init.InitModule;
-import com.google.gerrit.pgm.init.InstallPlugins;
-import com.google.gerrit.pgm.init.PluginsDistribution;
-import com.google.gerrit.pgm.init.SitePathInitializer;
-import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.common.IoUtil;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitFlags;
+import com.google.gerrit.pgm.init.api.InstallPlugins;
 import com.google.gerrit.pgm.util.SiteProgram;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.plugins.JarScanner;
 import com.google.gerrit.server.schema.SchemaUpdater;
 import com.google.gerrit.server.schema.UpdateUI;
+import com.google.gerrit.server.securestore.SecureStore;
+import com.google.gerrit.server.securestore.SecureStoreClassName;
+import com.google.gerrit.server.securestore.SecureStoreProvider;
 import com.google.gwtorm.jdbc.JdbcExecutor;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
@@ -47,12 +51,14 @@
 import com.google.inject.Provider;
 import com.google.inject.TypeLiteral;
 import com.google.inject.spi.Message;
+import com.google.inject.util.Providers;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
@@ -135,10 +141,26 @@
     return false;
   }
 
+  protected String getSecureStoreLib() {
+    return null;
+  }
+
+  /**
+   * Invoked before site init is called.
+   *
+   * @param init initializer instance.
+   * @throws Exception
+   */
   protected boolean beforeInit(SiteInit init) throws Exception {
     return false;
   }
 
+  /**
+   * Invoked after site init is called.
+   *
+   * @param run completed run instance.
+   * @throws Exception
+   */
   protected void afterInit(SiteRun run) throws Exception {
   }
 
@@ -168,8 +190,8 @@
     return false;
   }
 
-  static class SiteInit {
-    final SitePaths site;
+  public static class SiteInit {
+    public final SitePaths site;
     final InitFlags flags;
     final ConsoleUI ui;
     final SitePathInitializer initializer;
@@ -188,6 +210,18 @@
     final ConsoleUI ui = getConsoleUI();
     final File sitePath = getSitePath();
     final List<Module> m = new ArrayList<>();
+    final SecureStoreInitData secureStoreInitData = discoverSecureStoreClass();
+    final String currentSecureStoreClassName = getConfiguredSecureStoreClass();
+
+    if (secureStoreInitData != null && currentSecureStoreClassName != null
+        && !currentSecureStoreClassName.equals(secureStoreInitData.className)) {
+      String err =
+          String.format(
+              "Different secure store was previously configured: %s. "
+              + "Use SwitchSecureStore program to switch between implementations.",
+              currentSecureStoreClassName);
+      die(err, new RuntimeException("secure store mismatch"));
+    }
 
     m.add(new InitModule(standalone, initDb));
     m.add(new AbstractModule() {
@@ -196,10 +230,26 @@
         bind(ConsoleUI.class).toInstance(ui);
         bind(File.class).annotatedWith(SitePath.class).toInstance(sitePath);
         List<String> plugins =
-            Objects.firstNonNull(getInstallPlugins(), Lists.<String> newArrayList());
+            MoreObjects.firstNonNull(
+                getInstallPlugins(), Lists.<String> newArrayList());
         bind(new TypeLiteral<List<String>>() {}).annotatedWith(
             InstallPlugins.class).toInstance(plugins);
         bind(PluginsDistribution.class).toInstance(pluginsDistribution);
+
+        String secureStoreClassName;
+        if (secureStoreInitData != null) {
+          secureStoreClassName = secureStoreInitData.className;
+        } else {
+          secureStoreClassName = currentSecureStoreClassName;
+        }
+        if (secureStoreClassName != null) {
+          ui.message("Using secure store: %s\n", secureStoreClassName);
+        }
+        bind(SecureStoreInitData.class).toProvider(
+            Providers.of(secureStoreInitData));
+        bind(String.class).annotatedWith(SecureStoreClassName.class)
+            .toProvider(Providers.of(secureStoreClassName));
+        bind(SecureStore.class).toProvider(SecureStoreProvider.class).in(SINGLETON);
       }
     });
 
@@ -230,10 +280,43 @@
     return ConsoleUI.getInstance(false);
   }
 
-  static class SiteRun {
-    final ConsoleUI ui;
-    final SitePaths site;
-    final InitFlags flags;
+  private SecureStoreInitData discoverSecureStoreClass() {
+    String secureStore = getSecureStoreLib();
+    if (Strings.isNullOrEmpty(secureStore)) {
+      return null;
+    }
+
+    try {
+      File secureStoreLib = new File(secureStore);
+      if (!secureStoreLib.exists()) {
+        throw new InvalidSecureStoreException(String.format(
+            "File %s doesn't exist", secureStore));
+      }
+      JarScanner scanner = new JarScanner(secureStoreLib);
+      List<String> secureStores =
+          scanner.findSubClassesOf(SecureStore.class);
+      if (secureStores.isEmpty()) {
+        throw new InvalidSecureStoreException(String.format(
+            "Cannot find class implementing %s interface in %s",
+            SecureStore.class.getName(), secureStore));
+      }
+      if (secureStores.size() > 1) {
+        throw new InvalidSecureStoreException(String.format(
+            "%s has more that one implementation of %s interface",
+            secureStore, SecureStore.class.getName()));
+      }
+      IoUtil.loadJARs(secureStoreLib);
+      return new SecureStoreInitData(secureStoreLib, secureStores.get(0));
+    } catch (IOException e) {
+      throw new InvalidSecureStoreException(String.format("%s is not a valid jar",
+          secureStore));
+    }
+  }
+
+  public static class SiteRun {
+    public final ConsoleUI ui;
+    public final SitePaths site;
+    public final InitFlags flags;
     final SchemaUpdater schemaUpdater;
     final SchemaFactory<ReviewDb> schema;
     final GitRepositoryManager repositoryManager;
@@ -295,18 +378,11 @@
           System.err.flush();
 
         } else if (ui.yesno(true, "%s\nExecute now", msg)) {
-          final JdbcSchema db = (JdbcSchema) schema.open();
-          try {
-            final JdbcExecutor e = new JdbcExecutor(db);
-            try {
-              for (String sql : pruneList) {
-                e.execute(sql);
-              }
-            } finally {
-              e.close();
+          try (JdbcSchema db = (JdbcSchema) schema.open();
+              JdbcExecutor e = new JdbcExecutor(db)) {
+            for (String sql : pruneList) {
+              e.execute(sql);
             }
-          } finally {
-            db.close();
           }
         }
       }
@@ -319,7 +395,7 @@
 
   private Injector createSysInjector(final SiteInit init) {
     if (sysInjector == null) {
-      final List<Module> modules = new ArrayList<Module>();
+      final List<Module> modules = new ArrayList<>();
       modules.add(new AbstractModule() {
         @Override
         protected void configure() {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java
index 96ab103..5a1eab2 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.pgm.init;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.pgm.init.api.InitUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigInitializer.java
index 7ac1ed6..c30edf8 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigInitializer.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.pgm.init;
 
+import com.google.gerrit.pgm.init.api.Section;
+
 /** Abstraction of initializer for the database section */
 interface DatabaseConfigInitializer {
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/H2Initializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/H2Initializer.java
index 0ea3ff0..e20346a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/H2Initializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/H2Initializer.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.pgm.init;
 
+import com.google.gerrit.pgm.init.api.InitUtil;
+import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
new file mode 100644
index 0000000..c1f0090
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.init;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitFlags;
+import com.google.gerrit.pgm.init.api.InitStep;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import org.apache.commons.validator.routines.EmailValidator;
+
+import java.util.Collections;
+
+public class InitAdminUser implements InitStep {
+  private final ConsoleUI ui;
+  private final InitFlags flags;
+  private SchemaFactory<ReviewDb> dbFactory;
+
+  @Inject
+  InitAdminUser(
+      InitFlags flags,
+      ConsoleUI ui) {
+    this.flags = flags;
+    this.ui = ui;
+  }
+
+  @Override
+  public void run() {
+  }
+
+  @Inject(optional = true)
+  void set(SchemaFactory<ReviewDb> dbFactory) {
+    this.dbFactory = dbFactory;
+  }
+
+  @Override
+  public void postRun() throws Exception {
+    AuthType authType =
+        flags.cfg.getEnum(AuthType.values(), "auth", null, "type", null);
+    if (authType != AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) {
+      return;
+    }
+
+    ReviewDb db = dbFactory.open();
+    try {
+      if (db.accounts().anyAccounts().toList().isEmpty()) {
+        ui.header("Gerrit Administrator");
+        if (ui.yesno(true, "Create administrator user")) {
+          Account.Id id = new Account.Id(db.nextAccountId());
+          String username = ui.readString("admin", "username");
+          String name = ui.readString("Administrator", "name");
+          String email = readEmail();
+          String httpPassword = ui.readString("secret", "HTTP password");
+
+          AccountExternalId extUser =
+              new AccountExternalId(id, new AccountExternalId.Key(
+                  AccountExternalId.SCHEME_USERNAME, username));
+          if (!Strings.isNullOrEmpty(httpPassword)) {
+            extUser.setPassword(httpPassword);
+          }
+          db.accountExternalIds().insert(Collections.singleton(extUser));
+
+          if (email != null) {
+            AccountExternalId extMailto =
+                new AccountExternalId(id, new AccountExternalId.Key(
+                    AccountExternalId.SCHEME_MAILTO, email));
+            extMailto.setEmailAddress(email);
+            db.accountExternalIds().insert(Collections.singleton(extMailto));
+          }
+
+          Account a = new Account(id, TimeUtil.nowTs());
+          a.setFullName(name);
+          a.setPreferredEmail(email);
+          db.accounts().insert(Collections.singleton(a));
+
+          AccountGroupMember m =
+              new AccountGroupMember(new AccountGroupMember.Key(id,
+                  new AccountGroup.Id(1)));
+          db.accountGroupMembers().insert(Collections.singleton(m));
+        }
+      }
+    } finally {
+      db.close();
+    }
+  }
+
+  private String readEmail() {
+    String email = ui.readString("admin@example.com", "email");
+    if (email != null && !EmailValidator.getInstance().isValid(email)) {
+      ui.message("error: invalid email address\n");
+      return readEmail();
+    }
+    return email;
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
index 74884a4..5c716ef 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.pgm.init.InitUtil.dnOf;
+import static com.google.gerrit.pgm.init.api.InitUtil.dnOf;
 
-import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitStep;
+import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gwtjsonrpc.server.SignedToken;
 import com.google.inject.Inject;
@@ -36,6 +38,7 @@
     this.ldap = sections.get("ldap", null);
   }
 
+  @Override
   public void run() {
     ui.header("User Authentication");
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java
index 7230c9d..8da4a03 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.pgm.init.InitUtil.die;
+import static com.google.gerrit.pgm.init.api.InitUtil.die;
 
+import com.google.gerrit.pgm.init.api.InitStep;
+import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -34,6 +36,7 @@
     this.cache = sections.get("cache", null);
   }
 
+  @Override
   public void run() {
     String path = cache.get("directory");
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java
index 92cd46d..f830854 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java
@@ -14,11 +14,13 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.pgm.init.InitUtil.die;
-import static com.google.gerrit.pgm.init.InitUtil.username;
+import static com.google.gerrit.pgm.init.api.InitUtil.die;
+import static com.google.gerrit.pgm.init.api.InitUtil.username;
 
 import com.google.gerrit.launcher.GerritLauncher;
-import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitStep;
+import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -47,6 +49,7 @@
     this.container = sections.get("container", null);
   }
 
+  @Override
   public void run() throws FileNotFoundException, IOException {
     ui.header("Container Process");
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
index 0c64552..3fcc911 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
@@ -18,7 +18,9 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Sets;
-import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitStep;
+import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Binding;
 import com.google.inject.Guice;
@@ -51,6 +53,7 @@
     this.database = sections.get("database", null);
   }
 
+  @Override
   public void run() {
     ui.header("SQL Database");
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java
index dc8a440..067b103 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.pgm.init.InitUtil.die;
+import static com.google.gerrit.pgm.init.api.InitUtil.die;
 
-import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitStep;
+import com.google.gerrit.pgm.init.api.Section;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -34,6 +36,7 @@
     this.gerrit = sections.get("gerrit", null);
   }
 
+  @Override
   public void run() {
     ui.header("Git Repositories");
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java
index 8929a7b..c8f1cd7 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java
@@ -15,12 +15,15 @@
 package com.google.gerrit.pgm.init;
 
 import static com.google.gerrit.common.FileUtil.chmod;
-import static com.google.gerrit.pgm.init.InitUtil.die;
-import static com.google.gerrit.pgm.init.InitUtil.domainOf;
-import static com.google.gerrit.pgm.init.InitUtil.isAnyAddress;
-import static com.google.gerrit.pgm.init.InitUtil.toURI;
+import static com.google.gerrit.pgm.init.api.InitUtil.die;
+import static com.google.gerrit.pgm.init.api.InitUtil.domainOf;
+import static com.google.gerrit.pgm.init.api.InitUtil.isAnyAddress;
+import static com.google.gerrit.pgm.init.api.InitUtil.toURI;
 
-import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitFlags;
+import com.google.gerrit.pgm.init.api.InitStep;
+import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gwtjsonrpc.server.SignedToken;
 import com.google.inject.Inject;
@@ -50,6 +53,7 @@
     this.gerrit = sections.get("gerrit", null);
   }
 
+  @Override
   public void run() throws IOException, InterruptedException {
     ui.header("HTTP Daemon");
 
@@ -150,10 +154,10 @@
       return;
     }
 
-    String ssl_pass = flags.sec.getString("http", null, "sslKeyPassword");
+    String ssl_pass = flags.sec.get("http", null, "sslKeyPassword");
     if (ssl_pass == null || ssl_pass.isEmpty()) {
       ssl_pass = SignedToken.generateRandomKey();
-      flags.sec.setString("httpd", null, "sslKeyPassword", ssl_pass);
+      flags.sec.set("httpd", null, "sslKeyPassword", ssl_pass);
     }
 
     hostname = ui.readString(hostname, "Certificate server name");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
index 9966fda..acb8a6b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
@@ -15,7 +15,10 @@
 package com.google.gerrit.pgm.init;
 
 import com.google.gerrit.lucene.LuceneChangeIndex;
-import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitFlags;
+import com.google.gerrit.pgm.init.api.InitStep;
+import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.index.ChangeSchemas;
 import com.google.gerrit.server.index.IndexModule.IndexType;
@@ -43,6 +46,7 @@
     this.initFlags = initFlags;
   }
 
+  @Override
   public void run() throws IOException {
     ui.header("Index");
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitLabels.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitLabels.java
index 78cfa3b..8fb05ca 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitLabels.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitLabels.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.pgm.init;
 
-import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.pgm.init.api.AllProjectsConfig;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitStep;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -42,7 +44,7 @@
 
   @Override
   public void run() throws Exception {
-    Config cfg = allProjectsConfig.load();
+    Config cfg = allProjectsConfig.load().getConfig();
     if (cfg == null || !cfg.getSubsections(KEY_LABEL).contains(LABEL_VERIFIED)) {
       ui.header("Review Labels");
       installVerified = ui.yesno(false, "Install Verified label");
@@ -51,7 +53,7 @@
 
   @Override
   public void postRun() throws Exception {
-    Config cfg = allProjectsConfig.load();
+    Config cfg = allProjectsConfig.load().getConfig();
     if (installVerified) {
       cfg.setString(KEY_LABEL, LABEL_VERIFIED, KEY_FUNCTION, "MaxWithBlock");
       cfg.setStringList(KEY_LABEL, LABEL_VERIFIED, KEY_VALUE,
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
index 64960d7..8d08520 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.pgm.init;
 
+import com.google.gerrit.pgm.init.api.InitFlags;
+import com.google.gerrit.pgm.init.api.InitStep;
+import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.binder.LinkedBindingBuilder;
@@ -48,9 +51,9 @@
     if (initDb) {
       step().to(InitDatabase.class);
     }
-    step().to(UpdatePrimaryKeys.class);
     step().to(InitIndex.class);
     step().to(InitAuth.class);
+    step().to(InitAdminUser.class);
     step().to(InitLabels.class);
     step().to(InitSendEmail.class);
     if (standalone) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
index 1372c31..fa9f710 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
@@ -14,9 +14,10 @@
 
 package com.google.gerrit.pgm.init;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitStep;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.plugins.JarPluginProvider;
 import com.google.gerrit.server.plugins.PluginLoader;
@@ -84,13 +85,18 @@
         return getPluginInjector(jar).getInstance(initStepClass);
       } catch (ClassCastException e) {
         ui.message(
-            "WARN: InitStep from plugin %s does not implement %s (Exception: %s)",
+            "WARN: InitStep from plugin %s does not implement %s (Exception: %s)\n",
             jar.getName(), InitStep.class.getName(), e.getMessage());
         return null;
+      } catch (NoClassDefFoundError e) {
+        ui.message(
+            "WARN: Failed to run InitStep from plugin %s (Missing class: %s)\n",
+            jar.getName(), e.getMessage());
+        return null;
       }
     } catch (Exception e) {
       ui.message(
-          "WARN: Cannot load and get plugin init step for %s (Exception: %s)",
+          "WARN: Cannot load and get plugin init step for %s (Exception: %s)\n",
           jar, e.getMessage());
       return null;
     }
@@ -98,7 +104,8 @@
 
   private Injector getPluginInjector(final File jarFile) throws IOException {
     final String pluginName =
-        Objects.firstNonNull(JarPluginProvider.getJarPluginName(jarFile),
+        MoreObjects.firstNonNull(
+            JarPluginProvider.getJarPluginName(jarFile),
             PluginLoader.nameOf(jarFile));
     return initInjector.createChildInjector(new AbstractModule() {
       @Override
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
index 2326e48..72400a4 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
@@ -16,7 +16,9 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.PluginData;
-import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitFlags;
+import com.google.gerrit.pgm.init.api.InitStep;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.plugins.JarPluginProvider;
 import com.google.inject.Inject;
@@ -26,6 +28,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Collection;
 import java.util.List;
 import java.util.jar.Attributes;
 import java.util.jar.JarFile;
@@ -102,6 +105,7 @@
   }
 
   private void installPlugins() throws IOException {
+    ui.message("Installing plugins.\n");
     List<PluginData> plugins = listPlugins(site, pluginsDistribution);
     for (PluginData plugin : plugins) {
       String pluginName = plugin.name;
@@ -140,13 +144,19 @@
       }
     }
     if (plugins.isEmpty()) {
-      ui.message("No plugins found.");
+      ui.message("No plugins found to install.\n");
     }
   }
 
   private void initPlugins() throws Exception {
-    for (InitStep initStep : pluginLoader.getInitSteps()) {
-      initStep.run();
+    ui.message("Initializing plugins.\n");
+    Collection<InitStep> initSteps = pluginLoader.getInitSteps();
+    if (initSteps.isEmpty()) {
+      ui.message("No plugins found with init steps.\n");
+    } else {
+      for (InitStep initStep : initSteps) {
+        initStep.run();
+      }
     }
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java
index ae621ae..51eaa22 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.pgm.init.InitUtil.isLocal;
-import static com.google.gerrit.pgm.init.InitUtil.username;
+import static com.google.gerrit.pgm.init.api.InitUtil.isLocal;
+import static com.google.gerrit.pgm.init.api.InitUtil.username;
 
-import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitStep;
+import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.mail.SmtpEmailSender.Encryption;
 import com.google.inject.Inject;
@@ -38,6 +40,7 @@
     this.site = site;
   }
 
+  @Override
   public void run() {
     ui.header("Email Delivery");
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
index 9003b30..ed18d73 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
@@ -15,10 +15,12 @@
 package com.google.gerrit.pgm.init;
 
 import static com.google.gerrit.common.FileUtil.chmod;
-import static com.google.gerrit.pgm.init.InitUtil.die;
-import static com.google.gerrit.pgm.init.InitUtil.hostname;
+import static com.google.gerrit.pgm.init.api.InitUtil.die;
+import static com.google.gerrit.pgm.init.api.InitUtil.hostname;
 
-import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitStep;
+import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.util.SocketUtil;
 import com.google.inject.Inject;
@@ -48,6 +50,7 @@
     this.sshd = sections.get("sshd", null);
   }
 
+  @Override
   public void run() throws Exception {
     ui.header("SSH Daemon");
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PathConflictException.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InvalidSecureStoreException.java
similarity index 64%
copy from gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PathConflictException.java
copy to gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InvalidSecureStoreException.java
index 7e2f2d7..52f794d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PathConflictException.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InvalidSecureStoreException.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2014 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,13 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.changedetail;
+package com.google.gerrit.pgm.init;
 
-/** Indicates a path conflict during rebase or merge */
-public class PathConflictException extends Exception {
+public class InvalidSecureStoreException extends RuntimeException {
   private static final long serialVersionUID = 1L;
 
-  public PathConflictException(String msg) {
-    super(msg);
+  public InvalidSecureStoreException(String message) {
+    super(message);
+  }
+
+  public InvalidSecureStoreException(String message, Throwable why) {
+    super(message, why);
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java
index 20034c1..dfd6171 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java
@@ -14,9 +14,10 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.pgm.init.InitUtil.username;
+import static com.google.gerrit.pgm.init.api.InitUtil.username;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.pgm.init.api.Section;
 
 class JDBCInitializer implements DatabaseConfigInitializer {
   @Override
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
index 7209990..ea8f0f1 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
@@ -33,7 +33,7 @@
 @Singleton
 class Libraries {
   private static final String RESOURCE_FILE =
-      "com/google/gerrit/pgm/libraries.config";
+      "com/google/gerrit/pgm/init/libraries.config";
 
   private final Provider<LibraryDownloader> downloadProvider;
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
index b943ca3..4bf1c88 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
@@ -17,8 +17,8 @@
 import com.google.common.base.Strings;
 import com.google.common.io.Files;
 import com.google.gerrit.common.Die;
-import com.google.gerrit.pgm.util.ConsoleUI;
-import com.google.gerrit.pgm.util.IoUtil;
+import com.google.gerrit.common.IoUtil;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/MaxDbInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/MaxDbInitializer.java
index 4d746cc..0f696b7 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/MaxDbInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/MaxDbInitializer.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.pgm.init.InitUtil.username;
+import static com.google.gerrit.pgm.init.api.InitUtil.username;
+
+import com.google.gerrit.pgm.init.api.Section;
 
 public class MaxDbInitializer implements DatabaseConfigInitializer {
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/MySqlInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/MySqlInitializer.java
index fe6a4d9..037b52b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/MySqlInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/MySqlInitializer.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.pgm.init.InitUtil.username;
+import static com.google.gerrit.pgm.init.api.InitUtil.username;
+
+import com.google.gerrit.pgm.init.api.Section;
 
 class MySqlInitializer implements DatabaseConfigInitializer {
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/OracleInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/OracleInitializer.java
index 180beb0..e58c6ad 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/OracleInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/OracleInitializer.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.pgm.init.InitUtil.username;
+import static com.google.gerrit.pgm.init.api.InitUtil.username;
+
+import com.google.gerrit.pgm.init.api.Section;
 
 
 public class OracleInitializer implements DatabaseConfigInitializer {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/PostgreSQLInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/PostgreSQLInitializer.java
index 1425663..ffb0017 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/PostgreSQLInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/PostgreSQLInitializer.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.pgm.init.InitUtil.username;
+import static com.google.gerrit.pgm.init.api.InitUtil.username;
+
+import com.google.gerrit.pgm.init.api.Section;
 
 class PostgreSQLInitializer implements DatabaseConfigInitializer {
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SecureStoreInitData.java
similarity index 70%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SecureStoreInitData.java
index 407b7c7..8926759 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SecureStoreInitData.java
@@ -12,10 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.pgm.init;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+import java.io.File;
+
+class SecureStoreInitData {
+  final File jarFile;
+  final String className;
+
+  SecureStoreInitData(File jar, String className) {
+    this.className = className;
+    this.jarFile = jar;
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index 800f2c3..10c9bad 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -15,15 +15,19 @@
 package com.google.gerrit.pgm.init;
 
 import static com.google.gerrit.common.FileUtil.chmod;
-import static com.google.gerrit.pgm.init.InitUtil.die;
-import static com.google.gerrit.pgm.init.InitUtil.extract;
-import static com.google.gerrit.pgm.init.InitUtil.mkdir;
-import static com.google.gerrit.pgm.init.InitUtil.savePublic;
-import static com.google.gerrit.pgm.init.InitUtil.saveSecure;
-import static com.google.gerrit.pgm.init.InitUtil.version;
+import static com.google.gerrit.pgm.init.api.InitUtil.die;
+import static com.google.gerrit.pgm.init.api.InitUtil.extract;
+import static com.google.gerrit.pgm.init.api.InitUtil.mkdir;
+import static com.google.gerrit.pgm.init.api.InitUtil.savePublic;
+import static com.google.gerrit.pgm.init.api.InitUtil.version;
 
-import com.google.gerrit.pgm.BaseInit;
-import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.common.io.Files;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitFlags;
+import com.google.gerrit.pgm.init.api.InitStep;
+import com.google.gerrit.pgm.init.api.Section;
+import com.google.gerrit.pgm.init.api.Section.Factory;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.mail.OutgoingEmail;
 import com.google.inject.Binding;
@@ -32,6 +36,7 @@
 import com.google.inject.TypeLiteral;
 
 import java.io.File;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -41,13 +46,19 @@
   private final InitFlags flags;
   private final SitePaths site;
   private final List<InitStep> steps;
+  private final Factory sectionFactory;
+  private final SecureStoreInitData secureStoreInitData;
 
   @Inject
   public SitePathInitializer(final Injector injector, final ConsoleUI ui,
-      final InitFlags flags, final SitePaths site) {
+      final InitFlags flags, final SitePaths site,
+      final Section.Factory sectionFactory,
+      final @Nullable SecureStoreInitData secureStoreInitData) {
     this.ui = ui;
     this.flags = flags;
     this.site = site;
+    this.sectionFactory = sectionFactory;
+    this.secureStoreInitData = secureStoreInitData;
     this.steps = stepsOf(injector);
   }
 
@@ -82,10 +93,10 @@
       step.run();
     }
 
+    saveSecureStore();
     savePublic(flags.cfg);
-    saveSecure(flags.sec);
 
-    extract(site.gerrit_sh, BaseInit.class, "gerrit.sh");
+    extract(site.gerrit_sh, getClass(), "gerrit.sh");
     chmod(0755, site.gerrit_sh);
     chmod(0700, site.tmp_dir);
 
@@ -119,6 +130,15 @@
     }
   }
 
+  private void saveSecureStore() throws IOException {
+    if (secureStoreInitData != null) {
+      File dst = new File(site.lib_dir, secureStoreInitData.jarFile.getName());
+      Files.copy(secureStoreInitData.jarFile, dst);
+      Section gerritSection = sectionFactory.get("gerrit", null);
+      gerritSection.set("secureStoreClass", secureStoreInitData.className);
+    }
+  }
+
   private void extractMailExample(String orig) throws Exception {
     File ex = new File(site.mail_dir, orig + ".example");
     extract(ex, OutgoingEmail.class, orig);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpdatePrimaryKeys.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpdatePrimaryKeys.java
deleted file mode 100644
index 79bd730..0000000
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpdatePrimaryKeys.java
+++ /dev/null
@@ -1,180 +0,0 @@
-// Copyright (C) 2014 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.pgm.init;
-
-import com.google.common.base.Joiner;
-import com.google.gerrit.pgm.util.ConsoleUI;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcExecutor;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.ColumnModel;
-import com.google.gwtorm.schema.RelationModel;
-import com.google.gwtorm.schema.java.JavaSchemaModel;
-import com.google.gwtorm.schema.sql.DialectPostgreSQL;
-import com.google.gwtorm.schema.sql.SqlDialect;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.gwtorm.server.StatementExecutor;
-import com.google.inject.Inject;
-
-import java.sql.Connection;
-import java.sql.DatabaseMetaData;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.TreeMap;
-
-public class UpdatePrimaryKeys implements InitStep {
-
-  private static class PrimaryKey {
-    String oldNameInDb;
-    List<String> cols;
-  }
-
-  private final ConsoleUI ui;
-
-  private SchemaFactory<ReviewDb> dbFactory;
-  private ReviewDb db;
-  private Connection conn;
-  private SqlDialect dialect;
-
-  @Inject
-  UpdatePrimaryKeys(ConsoleUI ui) {
-    this.ui = ui;
-  }
-
-  @Override
-  public void run() throws Exception {
-  }
-
-  @Override
-  public void postRun() throws Exception {
-    db = dbFactory.open();
-    try {
-      conn = ((JdbcSchema) db).getConnection();
-      dialect = ((JdbcSchema) db).getDialect();
-      Map<String, PrimaryKey> corrections = findPKUpdates();
-      if (corrections.isEmpty()) {
-        return;
-      }
-
-      ui.header("Wrong Primary Key Column Order Detected");
-      ui.message("The following tables are affected:\n");
-      ui.message("%s\n", Joiner.on(", ").join(corrections.keySet()));
-      if (ui.yesno(true, "Fix primary keys column order")) {
-        ui.message("fixing primary keys...\n");
-        JdbcExecutor executor = new JdbcExecutor(conn);
-        try {
-          for (Map.Entry<String, PrimaryKey> c : corrections.entrySet()) {
-            ui.message("  table: %s ... ", c.getKey());
-            recreatePK(executor, c.getKey(), c.getValue());
-            ui.message("done\n");
-          }
-          ui.message("done\n");
-        } finally {
-          executor.close();
-        }
-      }
-    } finally {
-      db.close();
-    }
-  }
-
-  @Inject(optional = true)
-  void setSchemaFactory(SchemaFactory<ReviewDb> dbFactory) {
-    this.dbFactory = dbFactory;
-  }
-
-  private Map<String, PrimaryKey> findPKUpdates()
-      throws OrmException, SQLException {
-    Map<String, PrimaryKey> corrections = new TreeMap<>();
-    ReviewDb db = dbFactory.open();
-    try {
-      DatabaseMetaData meta = conn.getMetaData();
-      JavaSchemaModel jsm = new JavaSchemaModel(ReviewDb.class);
-      for (RelationModel rm : jsm.getRelations()) {
-        String tableName = rm.getRelationName();
-        List<String> expectedPKCols = relationPK(rm);
-        PrimaryKey actualPK = dbTablePK(meta, tableName);
-        if (!expectedPKCols.equals(actualPK.cols)) {
-          actualPK.cols = expectedPKCols;
-          corrections.put(tableName, actualPK);
-        }
-      }
-      return corrections;
-    } finally {
-      db.close();
-    }
-  }
-
-  private List<String> relationPK(RelationModel rm) {
-    Collection<ColumnModel> cols = rm.getPrimaryKeyColumns();
-    List<String> pk = new ArrayList<>(cols.size());
-    for (ColumnModel cm : cols) {
-      pk.add(cm.getColumnName().toLowerCase(Locale.US));
-    }
-    return pk;
-  }
-
-  private PrimaryKey dbTablePK(DatabaseMetaData meta, String tableName)
-      throws SQLException {
-    if (meta.storesUpperCaseIdentifiers()) {
-      tableName = tableName.toUpperCase();
-    } else if (meta.storesLowerCaseIdentifiers()) {
-      tableName = tableName.toLowerCase();
-    }
-
-    ResultSet cols = meta.getPrimaryKeys(null, null, tableName);
-    try {
-      PrimaryKey pk = new PrimaryKey();
-      Map<Short, String> seqToName = new TreeMap<>();
-      while (cols.next()) {
-        seqToName.put(cols.getShort("KEY_SEQ"), cols.getString("COLUMN_NAME"));
-        if (pk.oldNameInDb == null) {
-          pk.oldNameInDb = cols.getString("PK_NAME");
-        }
-      }
-
-      pk.cols = new ArrayList<>(seqToName.size());
-      for (String name : seqToName.values()) {
-        pk.cols.add(name.toLowerCase(Locale.US));
-      }
-      return pk;
-    } finally {
-      cols.close();
-    }
-  }
-
-  private void recreatePK(StatementExecutor executor, String tableName,
-      PrimaryKey pk) throws OrmException {
-    if (pk.oldNameInDb == null) {
-      ui.message("WARN: primary key for table %s didn't exist ... ", tableName);
-    } else {
-      if (dialect instanceof DialectPostgreSQL) {
-        // postgresql doesn't support the ALTER TABLE foo DROP PRIMARY KEY form
-        executor.execute("ALTER TABLE " + tableName + " DROP CONSTRAINT "
-            + pk.oldNameInDb);
-      } else {
-        executor.execute("ALTER TABLE " + tableName + " DROP PRIMARY KEY");
-      }
-    }
-    executor.execute("ALTER TABLE " + tableName
-        + " ADD PRIMARY KEY(" + Joiner.on(",").join(pk.cols) + ")");
-  }
-}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
index 97be0c5..8c13540 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
@@ -14,12 +14,15 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.pgm.init.InitUtil.die;
-import static com.google.gerrit.pgm.init.InitUtil.savePublic;
-import static com.google.gerrit.pgm.init.InitUtil.saveSecure;
+import static com.google.gerrit.pgm.init.api.InitUtil.die;
+import static com.google.gerrit.pgm.init.api.InitUtil.savePublic;
 
-import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitFlags;
+import com.google.gerrit.pgm.init.api.InitStep;
+import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.securestore.SecureStore;
 import com.google.gerrit.server.util.SocketUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -60,7 +63,7 @@
   private final ConsoleUI ui;
 
   private final FileBasedConfig cfg;
-  private final FileBasedConfig sec;
+  private final SecureStore sec;
   private final File site_path;
   private final File etc_dir;
   private final Section.Factory sections;
@@ -86,6 +89,7 @@
     return false;
   }
 
+  @Override
   public void run() throws IOException, ConfigInvalidException {
     if (!isNeedUpgrade()) {
       return;
@@ -113,7 +117,6 @@
     // believed to be empty) file.
     //
     cfg.load();
-    sec.load();
 
     final Properties oldprop = readGerritServerProperties();
     if (oldprop != null) {
@@ -136,7 +139,7 @@
 
       String password = oldprop.getProperty("password");
       if (password != null && !password.isEmpty()) {
-        sec.setString("database", null, "password", password);
+        sec.set("database", null, "password", password);
       }
     }
 
@@ -144,13 +147,12 @@
 
     values = cfg.getStringList("ldap", null, "password");
     cfg.unset("ldap", null, "password");
-    sec.setStringList("ldap", null, "password", Arrays.asList(values));
+    sec.setList("ldap", null, "password", Arrays.asList(values));
 
     values = cfg.getStringList("sendemail", null, "smtpPass");
     cfg.unset("sendemail", null, "smtpPass");
-    sec.setStringList("sendemail", null, "smtpPass", Arrays.asList(values));
+    sec.setList("sendemail", null, "smtpPass", Arrays.asList(values));
 
-    saveSecure(sec);
     savePublic(cfg);
   }
 
@@ -247,7 +249,7 @@
       database.set("username", username);
     }
     if (password != null && !password.isEmpty()) {
-      sec.setString("database", null, "password", password);
+      sec.set("database", null, "password", password);
     }
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AllProjectsConfig.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
similarity index 62%
rename from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AllProjectsConfig.java
rename to gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
index cd4a0b8..f45a970 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AllProjectsConfig.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsConfig.java
@@ -12,11 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.init;
+package com.google.gerrit.pgm.init.api;
 
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GroupList;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.VersionedMetaData;
 import com.google.inject.Inject;
 
@@ -28,21 +30,28 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache;
 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.io.IOException;
 
 public class AllProjectsConfig extends VersionedMetaData {
+
+  private static final Logger log = LoggerFactory.getLogger(AllProjectsConfig.class);
+
   private final String project;
   private final SitePaths site;
   private final InitFlags flags;
 
   private Config cfg;
   private ObjectId revision;
+  private GroupList groupList;
 
   @Inject
   AllProjectsConfig(AllProjectsNameOnInitProvider allProjects, SitePaths site,
@@ -66,34 +75,53 @@
     return FileKey.resolve(new File(basePath, project), FS.DETECTED);
   }
 
-  public Config load() throws IOException, ConfigInvalidException {
+  public AllProjectsConfig load() throws IOException, ConfigInvalidException {
     File path = getPath();
-    if (path == null) {
-      return null;
+    if (path != null) {
+      Repository repo = new FileRepository(path);
+      try {
+        load(repo);
+      } finally {
+        repo.close();
+      }
     }
+    return this;
+  }
 
-    Repository repo = new FileRepository(path);
-    try {
-      load(repo);
-    } finally {
-      repo.close();
-    }
+  public Config getConfig() {
     return cfg;
   }
 
+  public GroupList getGroups() {
+    return groupList;
+  }
+
   @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
+    groupList = readGroupList();
     cfg = readConfig(ProjectConfig.PROJECT_CONFIG);
     revision = getRevision();
   }
 
+  private GroupList readGroupList() throws IOException {
+    ValidationError.Sink errors = new ValidationError.Sink() {
+      @Override
+      public void error(ValidationError error) {
+        log.error("Error parsing file " + GroupList.FILE_NAME + ": " + error.getMessage());
+      }
+    };
+    String text = readUTF8(GroupList.FILE_NAME);
+
+    return GroupList.parse(text, errors);
+  }
+
   @Override
   protected boolean onSave(CommitBuilder commit) throws IOException,
       ConfigInvalidException {
     throw new UnsupportedOperationException();
   }
 
-  void save(String message) throws IOException {
+  public void save(String message) throws IOException {
     save(new PersonIdent("Gerrit Initialization", "init@gerrit"), message);
   }
 
@@ -108,49 +136,50 @@
       throw new IOException("All-Projects does not exist.");
     }
 
-    Repository repo = new FileRepository(path);
-    try {
+    try (Repository repo = new FileRepository(path)) {
       inserter = repo.newObjectInserter();
       reader = repo.newObjectReader();
-      try {
-        RevWalk rw = new RevWalk(reader);
-        try {
-          RevTree srcTree = revision != null ? rw.parseTree(revision) : null;
-          newTree = readTree(srcTree);
-          saveConfig(ProjectConfig.PROJECT_CONFIG, cfg);
-          ObjectId res = newTree.writeTree(inserter);
-          if (res.equals(srcTree)) {
-            // If there are no changes to the content, don't create the commit.
-            return;
-          }
-
-          CommitBuilder commit = new CommitBuilder();
-          commit.setAuthor(ident);
-          commit.setCommitter(ident);
-          commit.setMessage(msg);
-          commit.setTreeId(res);
-          if (revision != null) {
-            commit.addParentId(revision);
-          }
-          ObjectId newRevision = inserter.insert(commit);
-          updateRef(repo, ident, newRevision, "commit: " + msg);
-          revision = newRevision;
-        } finally {
-          rw.release();
+      try (RevWalk rw = new RevWalk(reader)) {
+        RevTree srcTree = revision != null ? rw.parseTree(revision) : null;
+        newTree = readTree(srcTree);
+        saveConfig(ProjectConfig.PROJECT_CONFIG, cfg);
+        saveGroupList();
+        ObjectId res = newTree.writeTree(inserter);
+        if (res.equals(srcTree)) {
+          // If there are no changes to the content, don't create the commit.
+          return;
         }
+
+        CommitBuilder commit = new CommitBuilder();
+        commit.setAuthor(ident);
+        commit.setCommitter(ident);
+        commit.setMessage(msg);
+        commit.setTreeId(res);
+        if (revision != null) {
+          commit.addParentId(revision);
+        }
+        ObjectId newRevision = inserter.insert(commit);
+        updateRef(repo, ident, newRevision, "commit: " + msg);
+        revision = newRevision;
       } finally {
         if (inserter != null) {
-          inserter.release();
+          inserter.close();
           inserter = null;
         }
         if (reader != null) {
-          reader.release();
+          reader.close();
           reader = null;
         }
       }
-    } finally {
-      repo.close();
     }
+
+    // we need to invalidate the JGit cache if the group list is invalidated in
+    // an unattended init step
+    RepositoryCache.clear();
+  }
+
+  private void saveGroupList() throws IOException {
+    saveUTF8(GroupList.FILE_NAME, groupList.asText());
   }
 
   private void updateRef(Repository repo, PersonIdent ident,
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AllProjectsNameOnInitProvider.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsNameOnInitProvider.java
similarity index 89%
rename from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AllProjectsNameOnInitProvider.java
rename to gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsNameOnInitProvider.java
index 1c9415a..9a6faea 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AllProjectsNameOnInitProvider.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/AllProjectsNameOnInitProvider.java
@@ -12,9 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.init;
+package com.google.gerrit.pgm.init.api;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.inject.Inject;
@@ -26,10 +26,11 @@
   @Inject
   AllProjectsNameOnInitProvider(Section.Factory sections) {
     String n = sections.get("gerrit", null).get("allProjects");
-    name = Objects.firstNonNull(
+    name = MoreObjects.firstNonNull(
         Strings.emptyToNull(n), AllProjectsNameProvider.DEFAULT);
   }
 
+  @Override
   public String get() {
     return name;
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
similarity index 99%
rename from gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java
rename to gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
index 3cbf047..fb0ef28 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.util;
+package com.google.gerrit.pgm.init.api;
 
 import static org.eclipse.jgit.util.StringUtils.equalsIgnoreCase;
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitFlags.java
similarity index 83%
rename from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java
rename to gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitFlags.java
index 267b41a..2a8155e 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitFlags.java
@@ -12,9 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.init;
+package com.google.gerrit.pgm.init.api;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.securestore.SecureStore;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -38,19 +40,19 @@
   public boolean skipPlugins;
 
   public final FileBasedConfig cfg;
-  public final FileBasedConfig sec;
+  public final SecureStore sec;
   public final List<String> installPlugins;
 
-
+  @VisibleForTesting
   @Inject
-  InitFlags(final SitePaths site,
+  public InitFlags(final SitePaths site,
+      final SecureStore secureStore,
       final @InstallPlugins List<String> installPlugins) throws IOException,
       ConfigInvalidException {
+    sec = secureStore;
     this.installPlugins = installPlugins;
     cfg = new FileBasedConfig(site.gerrit_config, FS.DETECTED);
-    sec = new FileBasedConfig(site.secure_config, FS.DETECTED);
 
     cfg.load();
-    sec.load();
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitStep.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitStep.java
similarity index 95%
rename from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitStep.java
rename to gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitStep.java
index 5a9a334..250cf59 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitStep.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitStep.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.init;
+package com.google.gerrit.pgm.init.api;
 
 /** A single step in the site initialization process. */
 public interface InitStep {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitUtil.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitUtil.java
similarity index 76%
rename from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitUtil.java
rename to gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitUtil.java
index 7e06b5a..881208d 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitUtil.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitUtil.java
@@ -12,15 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.init;
+package com.google.gerrit.pgm.init.api;
 
-import static com.google.gerrit.common.FileUtil.chmod;
 import static com.google.gerrit.common.FileUtil.modified;
 
 import com.google.gerrit.common.Die;
 
 import org.eclipse.jgit.internal.storage.file.LockFile;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.IO;
@@ -38,60 +36,40 @@
 import java.nio.ByteBuffer;
 
 /** Utility functions to help initialize a site. */
-class InitUtil {
-  static Die die(String why) {
+public class InitUtil {
+  public static Die die(String why) {
     return new Die(why);
   }
 
-  static Die die(String why, Throwable cause) {
+  public static Die die(String why, Throwable cause) {
     return new Die(why, cause);
   }
 
-  static void savePublic(final FileBasedConfig sec) throws IOException {
+  public static void savePublic(final FileBasedConfig sec) throws IOException {
     if (modified(sec)) {
       sec.save();
     }
   }
 
-  static void saveSecure(final FileBasedConfig sec) throws IOException {
-    if (modified(sec)) {
-      final byte[] out = Constants.encode(sec.toText());
-      final File path = sec.getFile();
-      final LockFile lf = new LockFile(path, FS.DETECTED);
-      if (!lf.lock()) {
-        throw new IOException("Cannot lock " + path);
-      }
-      try {
-        chmod(0600, new File(path.getParentFile(), path.getName() + ".lock"));
-        lf.write(out);
-        if (!lf.commit()) {
-          throw new IOException("Cannot commit write to " + path);
-        }
-      } finally {
-        lf.unlock();
-      }
-    }
-  }
-
-  static void mkdir(final File path) {
+  public static void mkdir(final File path) {
     if (!path.isDirectory() && !path.mkdir()) {
       throw die("Cannot make directory " + path);
     }
   }
 
-  static String version() {
+  public static String version() {
     return com.google.gerrit.common.Version.getVersion();
   }
 
-  static String username() {
+  public static String username() {
     return System.getProperty("user.name");
   }
 
-  static String hostname() {
+  public static String hostname() {
     return SystemReader.getInstance().getHostname();
   }
 
-  static boolean isLocal(final String hostname) {
+  public static boolean isLocal(final String hostname) {
     try {
       return InetAddress.getByName(hostname).isLoopbackAddress();
     } catch (UnknownHostException e) {
@@ -99,7 +77,7 @@
     }
   }
 
-  static String dnOf(String name) {
+  public static String dnOf(String name) {
     if (name != null) {
       int p = name.indexOf("://");
       if (0 < p) {
@@ -117,7 +95,7 @@
     return name;
   }
 
-  static String domainOf(String name) {
+  public static String domainOf(String name) {
     if (name != null) {
       int p = name.indexOf("://");
       if (0 < p) {
@@ -131,7 +109,7 @@
     return name;
   }
 
-  static void extract(final File dst, final Class<?> sibling,
+  public static void extract(final File dst, final Class<?> sibling,
       final String name) throws IOException {
     try (InputStream in = open(sibling, name)) {
       if (in != null) {
@@ -158,7 +136,7 @@
     return in;
   }
 
-  static void copy(final File dst, final ByteBuffer buf)
+  public static void copy(final File dst, final ByteBuffer buf)
       throws FileNotFoundException, IOException {
     // If the file already has the content we want to put there,
     // don't attempt to overwrite the file.
@@ -196,7 +174,7 @@
     }
   }
 
-  static URI toURI(String url) throws URISyntaxException {
+  public static URI toURI(String url) throws URISyntaxException {
     final URI u = new URI(url);
     if (isAnyAddress(u)) {
       // If the URL uses * it means all addresses on this system, use the
@@ -208,12 +186,12 @@
     return new URI(url);
   }
 
-  static boolean isAnyAddress(final URI u) {
+  public static boolean isAnyAddress(final URI u) {
     return u.getHost() == null
         && (u.getAuthority().equals("*") || u.getAuthority().startsWith("*:"));
   }
 
-  static int portOf(final URI uri) {
+  public static int portOf(final URI uri) {
     int port = uri.getPort();
     if (port < 0) {
       port = "https".equals(uri.getScheme()) ? 443 : 80;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InstallPlugins.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InstallPlugins.java
similarity index 95%
rename from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InstallPlugins.java
rename to gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InstallPlugins.java
index 73db6f5..256892a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InstallPlugins.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InstallPlugins.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.init;
+package com.google.gerrit.pgm.init.api;
 
 import com.google.inject.BindingAnnotation;
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/Section.java
similarity index 89%
rename from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java
rename to gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/Section.java
index d0a89e7..fbd8ecd 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/Section.java
@@ -12,12 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.init;
+package com.google.gerrit.pgm.init.api;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.pgm.util.ConsoleUI;
-import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.securestore.SecureStore;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -38,19 +37,22 @@
   private final ConsoleUI ui;
   private final String section;
   private final String subsection;
+  private final SecureStore secureStore;
 
   @Inject
   public Section(final InitFlags flags, final SitePaths site,
-      final ConsoleUI ui, @Assisted("section") final String section,
+      final SecureStore secureStore, final ConsoleUI ui,
+      @Assisted("section") final String section,
       @Assisted("subsection") @Nullable final String subsection) {
     this.flags = flags;
     this.site = site;
     this.ui = ui;
     this.section = section;
     this.subsection = subsection;
+    this.secureStore = secureStore;
   }
 
-  String get(String name) {
+  public String get(String name) {
     return flags.cfg.getString(section, subsection, name);
   }
 
@@ -116,7 +118,7 @@
   public <T extends Enum<?>> T select(final String title, final String name,
       final T defValue, final boolean nullIfDefault) {
     final boolean set = get(name) != null;
-    T oldValue = ConfigUtil.getEnum(flags.cfg, section, subsection, name, defValue);
+    T oldValue = flags.cfg.getEnum(section, subsection, name, defValue);
     T newValue = ui.readEnum(oldValue, "%s", title);
     if (nullIfDefault && newValue == defValue) {
       newValue = null;
@@ -144,7 +146,7 @@
   public String password(final String username, final String password) {
     final String ov = getSecure(password);
 
-    String user = flags.sec.getString(section, subsection, username);
+    String user = flags.sec.get(section, subsection, username);
     if (user == null) {
       user = get(username);
     }
@@ -189,14 +191,14 @@
   }
 
   public String getSecure(String name) {
-    return flags.sec.getString(section, subsection, name);
+    return flags.sec.get(section, subsection, name);
   }
 
   public void setSecure(String name, String value) {
     if (value != null) {
-      flags.sec.setString(section, subsection, name, value);
+      secureStore.set(section, subsection, name, value);
     } else {
-      flags.sec.unset(section, subsection, name);
+      secureStore.unset(section, subsection, name);
     }
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java
new file mode 100644
index 0000000..11ab073
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.util;
+
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.DisabledChangeHooks;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.git.GitModule;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.CommitValidators;
+
+/** Module for batch programs that need git access. */
+public class BatchGitModule extends FactoryModule {
+  @Override
+  protected void configure() {
+    bind(ChangeHooks.class).to(DisabledChangeHooks.class);
+    DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
+    DynamicSet.setOf(binder(), CommitValidationListener.class);
+    factory(CommitValidators.Factory.class);
+    install(new GitModule());
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
new file mode 100644
index 0000000..23983b7
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -0,0 +1,138 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.util;
+
+import static com.google.inject.Scopes.SINGLETON;
+
+import com.google.common.cache.Cache;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.rules.PrologModule;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountByEmailCacheImpl;
+import com.google.gerrit.server.account.AccountCacheImpl;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.FakeRealm;
+import com.google.gerrit.server.account.GroupCacheImpl;
+import com.google.gerrit.server.account.GroupIncludeCacheImpl;
+import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.cache.CacheRemovalListener;
+import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
+import com.google.gerrit.server.change.ChangeKindCacheImpl;
+import com.google.gerrit.server.change.MergeabilityCacheImpl;
+import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.CanonicalWebUrlProvider;
+import com.google.gerrit.server.config.DisableReverseDnsLookup;
+import com.google.gerrit.server.config.DisableReverseDnsLookupProvider;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.config.GitReceivePackGroups;
+import com.google.gerrit.server.config.GitUploadPackGroups;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.TagCache;
+import com.google.gerrit.server.group.GroupModule;
+import com.google.gerrit.server.mail.ReplacePatchSetSender;
+import com.google.gerrit.server.notedb.NoteDbModule;
+import com.google.gerrit.server.patch.DiffExecutorModule;
+import com.google.gerrit.server.patch.PatchListCacheImpl;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.CommentLinkInfo;
+import com.google.gerrit.server.project.CommentLinkProvider;
+import com.google.gerrit.server.project.ProjectCacheImpl;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.SectionSortCache;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.TypeLiteral;
+import com.google.inject.util.Providers;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Module for programs that perform batch operations on a site.
+ * <p>
+ * Any program that requires this module likely also requires using
+ * {@link ThreadLimiter} to limit the number of threads accessing the database
+ * concurrently.
+ */
+public class BatchProgramModule extends FactoryModule {
+  private final Module reviewDbModule;
+
+  @Inject
+  BatchProgramModule(PerThreadReviewDbModule reviewDbModule) {
+    this.reviewDbModule = reviewDbModule;
+  }
+
+  @SuppressWarnings("rawtypes")
+  @Override
+  protected void configure() {
+    install(reviewDbModule);
+    install(new DiffExecutorModule());
+    install(PatchListCacheImpl.module());
+    // Plugins are not loaded and we're just running through each change
+    // once, so don't worry about cache removal.
+    bind(new TypeLiteral<DynamicSet<CacheRemovalListener>>() {})
+        .toInstance(DynamicSet.<CacheRemovalListener> emptySet());
+    bind(new TypeLiteral<DynamicMap<Cache<?, ?>>>() {})
+        .toInstance(DynamicMap.<Cache<?, ?>> emptyMap());
+    bind(new TypeLiteral<List<CommentLinkInfo>>() {})
+        .toProvider(CommentLinkProvider.class).in(SINGLETON);
+    bind(String.class).annotatedWith(CanonicalWebUrl.class)
+        .toProvider(CanonicalWebUrlProvider.class);
+    bind(Boolean.class).annotatedWith(DisableReverseDnsLookup.class)
+        .toProvider(DisableReverseDnsLookupProvider.class).in(SINGLETON);
+    bind(Realm.class).to(FakeRealm.class);
+    bind(IdentifiedUser.class)
+      .toProvider(Providers.<IdentifiedUser> of(null));
+    bind(ReplacePatchSetSender.Factory.class).toProvider(
+        Providers.<ReplacePatchSetSender.Factory>of(null));
+    bind(CurrentUser.class).to(IdentifiedUser.class);
+    factory(MergeUtil.Factory.class);
+    factory(PatchSetInserter.Factory.class);
+
+    bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
+      .annotatedWith(GitUploadPackGroups.class)
+      .toInstance(Collections.<AccountGroup.UUID> emptySet());
+    bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
+      .annotatedWith(GitReceivePackGroups.class)
+      .toInstance(Collections.<AccountGroup.UUID> emptySet());
+    factory(ChangeControl.AssistedFactory.class);
+    factory(ProjectControl.AssistedFactory.class);
+
+    install(new BatchGitModule());
+    install(new DefaultCacheFactory.Module());
+    install(new GroupModule());
+    install(new NoteDbModule());
+    install(new PrologModule());
+    install(AccountByEmailCacheImpl.module());
+    install(AccountCacheImpl.module());
+    install(GroupCacheImpl.module());
+    install(GroupIncludeCacheImpl.module());
+    install(ProjectCacheImpl.module());
+    install(SectionSortCache.module());
+    install(ChangeKindCacheImpl.module());
+    install(MergeabilityCacheImpl.module());
+    install(TagCache.module());
+    factory(CapabilityControl.Factory.class);
+    factory(ChangeData.Factory.class);
+    factory(ProjectState.Factory.class);
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/GuiceLogger.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/GuiceLogger.java
index d807af6..6776ca9 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/GuiceLogger.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/GuiceLogger.java
@@ -25,6 +25,7 @@
   private static final Handler HANDLER;
   static {
     HANDLER = new StreamHandler(System.out, new Formatter() {
+      @Override
       public String format(LogRecord record) {
         return String.format("[Guice %s] %s%n", record.getLevel().getName(),
             record.getMessage());
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/PerThreadReviewDbModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/PerThreadReviewDbModule.java
new file mode 100644
index 0000000..eb12937
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/PerThreadReviewDbModule.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.util;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Module to bind a single {@link ReviewDb} instance per thread.
+ * <p>
+ * New instances are opened on demand, but are closed only at shutdown.
+ */
+class PerThreadReviewDbModule extends LifecycleModule {
+  private final SchemaFactory<ReviewDb> schema;
+
+  @Inject
+  PerThreadReviewDbModule(SchemaFactory<ReviewDb> schema) {
+    this.schema = schema;
+  }
+
+  @Override
+  protected void configure() {
+    final List<ReviewDb> dbs = Collections.synchronizedList(
+        Lists.<ReviewDb> newArrayList());
+    final ThreadLocal<ReviewDb> localDb = new ThreadLocal<>();
+
+    bind(ReviewDb.class).toProvider(new Provider<ReviewDb>() {
+      @Override
+      public ReviewDb get() {
+        ReviewDb db = localDb.get();
+        if (db == null) {
+          try {
+            db = schema.open();
+            dbs.add(db);
+            localDb.set(db);
+          } catch (OrmException e) {
+            throw new ProvisionException("unable to open ReviewDb", e);
+          }
+        }
+        return db;
+      }
+    });
+    listener().toInstance(new LifecycleListener() {
+      @Override
+      public void start() {
+        // Do nothing.
+      }
+
+      @Override
+      public void stop() {
+        for (ReviewDb db : dbs) {
+          db.close();
+        }
+      }
+    });
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreException.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SecureStoreException.java
similarity index 94%
rename from gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreException.java
rename to gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SecureStoreException.java
index 01450f8..9693399 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreException.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SecureStoreException.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.securestore;
+package com.google.gerrit.pgm.util;
 
 public class SecureStoreException extends RuntimeException {
   private static final long serialVersionUID = 5581700510568485065L;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
index 0614b53..c713b79 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.pgm.util;
 
+import com.google.gerrit.common.SiteLibraryLoaderUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.schema.DataSourceProvider;
@@ -24,9 +25,6 @@
 import org.eclipse.jgit.lib.Config;
 
 import java.io.File;
-import java.io.FileFilter;
-import java.util.Arrays;
-import java.util.Comparator;
 
 import javax.sql.DataSource;
 
@@ -41,40 +39,16 @@
       @GerritServerConfig Config cfg,
       DataSourceProvider.Context ctx,
       DataSourceType dst) {
-    super(site, cfg, ctx, dst);
+    super(cfg, ctx, dst);
     libdir = site.lib_dir;
   }
 
+  @Override
   public synchronized DataSource get() {
     if (!init) {
-      loadSiteLib();
+      SiteLibraryLoaderUtil.loadSiteLib(libdir);
       init = true;
     }
     return super.get();
   }
-
-  private void loadSiteLib() {
-    File[] jars = libdir.listFiles(new FileFilter() {
-      @Override
-      public boolean accept(File path) {
-        String name = path.getName();
-        return (name.endsWith(".jar") || name.endsWith(".zip"))
-            && path.isFile();
-      }
-    });
-    if (jars != null && 0 < jars.length) {
-      Arrays.sort(jars, new Comparator<File>() {
-        @Override
-        public int compare(File a, File b) {
-          // Sort by reverse last-modified time so newer JARs are first.
-          int cmp = Long.compare(b.lastModified(), a.lastModified());
-          if (cmp != 0) {
-            return cmp;
-          }
-          return a.getName().compareTo(b.getName());
-        }
-      });
-      IoUtil.loadJARs(jars);
-    }
-  }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
index 3c60510..9763b1a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.pgm.util;
 
+import static com.google.gerrit.server.config.GerritServerConfigModule.getSecureStoreClassName;
 import static com.google.inject.Scopes.SINGLETON;
 import static com.google.inject.Stage.PRODUCTION;
 
@@ -30,6 +31,7 @@
 import com.google.gerrit.server.schema.DataSourceType;
 import com.google.gerrit.server.schema.DatabaseModule;
 import com.google.gerrit.server.schema.SchemaModule;
+import com.google.gerrit.server.securestore.SecureStoreClassName;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.AbstractModule;
 import com.google.inject.Binding;
@@ -43,6 +45,7 @@
 import com.google.inject.name.Named;
 import com.google.inject.name.Names;
 import com.google.inject.spi.Message;
+import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.lib.Config;
 import org.kohsuke.args4j.Option;
@@ -96,6 +99,8 @@
       @Override
       protected void configure() {
         bind(File.class).annotatedWith(SitePath.class).toInstance(sitePath);
+        bind(String.class).annotatedWith(SecureStoreClassName.class)
+            .toProvider(Providers.of(getConfiguredSecureStoreClass()));
       }
     };
     modules.add(sitePathModule);
@@ -177,15 +182,14 @@
     }
   }
 
+  protected final String getConfiguredSecureStoreClass() {
+    return getSecureStoreClassName(sitePath);
+  }
+
   private String getDbType(Provider<DataSource> dsProvider) {
     String dbProductName;
-    try {
-      Connection conn = dsProvider.get().getConnection();
-      try {
-        dbProductName = conn.getMetaData().getDatabaseProductName().toLowerCase();
-      } finally {
-        conn.close();
-      }
+    try (Connection conn = dsProvider.get().getConnection()) {
+      dbProductName = conn.getMetaData().getDatabaseProductName().toLowerCase();
     } catch (SQLException e) {
       throw new RuntimeException(e);
     }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ThreadLimiter.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ThreadLimiter.java
new file mode 100644
index 0000000..44361aa
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ThreadLimiter.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.util;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.schema.DataSourceProvider;
+import com.google.gerrit.server.schema.DataSourceType;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+// TODO(dborowitz): Not necessary once we switch to notedb.
+/** Utility to limit threads used by a batch program. */
+public class ThreadLimiter {
+  private static final Logger log =
+      LoggerFactory.getLogger(ThreadLimiter.class);
+
+  public static int limitThreads(Injector dbInjector, int threads) {
+    return limitThreads(
+        dbInjector.getInstance(Key.get(Config.class, GerritServerConfig.class)),
+        dbInjector.getInstance(DataSourceType.class),
+        threads);
+  }
+
+  private static int limitThreads(Config cfg, DataSourceType dst, int threads) {
+    boolean usePool = cfg.getBoolean("database", "connectionpool",
+        dst.usePool());
+    int poolLimit = cfg.getInt("database", "poollimit",
+        DataSourceProvider.DEFAULT_POOL_LIMIT);
+    if (usePool && threads > poolLimit) {
+      log.warn("Limiting program to " + poolLimit
+          + " threads due to database.poolLimit");
+      return poolLimit;
+    }
+    return threads;
+  }
+
+  private ThreadLimiter() {
+  }
+}
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/Startup.py b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/Startup.py
index 92d6e56..cf6fac9 100644
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/Startup.py
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/Startup.py
@@ -18,7 +18,7 @@
 
 import sys
 
-def help():
+def print_help():
   for (n, v) in vars(sys.modules['__main__']).items():
     if not n.startswith("__") and not n in ['help', 'reload'] \
        and str(type(v)) != "<type 'javapackage'>"             \
@@ -28,4 +28,4 @@
   print "Welcome to the Gerrit Inspector"
   print "Enter help() to see the above again, EOF to quit and stop Gerrit"
 
-help()
+print_help()
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh
similarity index 96%
rename from gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
rename to gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh
index 4b9b3ef..7e6f943 100755
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh
@@ -47,7 +47,7 @@
 
 usage() {
     me=`basename "$0"`
-    echo >&2 "Usage: $me {start|stop|restart|check|status|run|supervise} [-d site]"
+    echo >&2 "Usage: $me {start|stop|restart|check|status|run|supervise|threads} [-d site]"
     exit 1
 }
 
@@ -63,6 +63,13 @@
   return 0
 }
 
+thread_dump() {
+  test -f $1 || return 1
+  PID=`cat $1`
+  $JSTACK $PID || return 1
+  return 0;
+}
+
 get_config() {
   if test -f "$GERRIT_CONFIG" ; then
     if test "x$1" = x--int ; then
@@ -258,6 +265,10 @@
   exit 1
 fi
 
+if test -z "$JSTACK"; then
+  JSTACK="$JAVA_HOME/bin/jstack"
+fi
+
 #####################################################
 # Add Gerrit properties to Java VM options.
 #####################################################
@@ -326,6 +337,11 @@
 if test "`get_config --bool container.slave`" = "true" ; then
   RUN_ARGS="$RUN_ARGS --slave"
 fi
+DAEMON_OPTS=`get_config --get-all container.daemonOpt`
+if test -n "$DAEMON_OPTS" ; then
+  RUN_ARGS="$RUN_ARGS $DAEMON_OPTS"
+fi
+
 if test -n "$JAVA_OPTIONS" ; then
   RUN_ARGS="$JAVA_OPTIONS $RUN_ARGS"
 fi
@@ -531,6 +547,16 @@
     exit 3
   ;;
 
+  threads)
+    if running "$GERRIT_PID" ; then
+      thread_dump "$GERRIT_PID"
+      exit 0
+    else
+      echo "Gerrit not running?"
+    fi
+    exit 3
+  ;;
+
   *)
     usage
   ;;
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
similarity index 100%
rename from gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config
rename to gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java
index 37eeda5..a37c97d 100644
--- a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java
+++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java
@@ -17,12 +17,13 @@
 import static org.easymock.EasyMock.createStrictMock;
 import static org.easymock.EasyMock.replay;
 import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertNotNull;
 
-import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Provider;
+
 import org.junit.Test;
-import static org.junit.Assert.assertNotNull;
 
 import java.io.File;
 import java.io.FileNotFoundException;
diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
index 26c4382..720d108 100644
--- a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
+++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
@@ -24,10 +24,14 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
-import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitFlags;
+import com.google.gerrit.pgm.init.api.Section;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.securestore.SecureStore;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.IO;
@@ -38,6 +42,7 @@
 import java.io.IOException;
 import java.io.Writer;
 import java.util.Collections;
+import java.util.List;
 
 
 public class UpgradeFrom2_0_xTest extends InitTestCase {
@@ -69,13 +74,14 @@
     old.setString("sendemail", null, "smtpPass", "email.s3kr3t");
     old.save();
 
+    final InMemorySecureStore secureStore = new InMemorySecureStore();
     final InitFlags flags =
-        new InitFlags(site, Collections.<String> emptyList());
+        new InitFlags(site, secureStore, Collections.<String> emptyList());
     final ConsoleUI ui = createStrictMock(ConsoleUI.class);
     Section.Factory sections = new Section.Factory() {
       @Override
       public Section get(String name, String subsection) {
-        return new Section(flags, site, ui, name, subsection);
+        return new Section(flags, site, secureStore, ui, name, subsection);
       }
     };
 
@@ -97,18 +103,41 @@
     }
 
     FileBasedConfig cfg = new FileBasedConfig(site.gerrit_config, FS.DETECTED);
-    FileBasedConfig sec = new FileBasedConfig(site.secure_config, FS.DETECTED);
     cfg.load();
-    sec.load();
 
     assertEquals("email.user", cfg.getString("sendemail", null, "smtpUser"));
     assertNull(cfg.getString("sendemail", null, "smtpPass"));
-    assertEquals("email.s3kr3t", sec.getString("sendemail", null, "smtpPass"));
+    assertEquals("email.s3kr3t", secureStore.get("sendemail", null, "smtpPass"));
 
     assertEquals("ldap.user", cfg.getString("ldap", null, "username"));
     assertNull(cfg.getString("ldap", null, "password"));
-    assertEquals("ldap.s3kr3t", sec.getString("ldap", null, "password"));
+    assertEquals("ldap.s3kr3t", secureStore.get("ldap", null, "password"));
 
     u.run();
   }
+
+  private static class InMemorySecureStore extends SecureStore {
+    private final Config cfg = new Config();
+
+    @Override
+    public String[] getList(String section, String subsection, String name) {
+      return cfg.getStringList(section, subsection, name);
+    }
+
+    @Override
+    public void setList(String section, String subsection, String name,
+        List<String> values) {
+      cfg.setStringList(section, subsection, name, values);
+    }
+
+    @Override
+    public void unset(String section, String subsection, String name) {
+      cfg.unset(section, subsection, name);
+    }
+
+    @Override
+    public Iterable<EntryKey> list() {
+      throw new UnsupportedOperationException("not used by tests");
+    }
+  }
 }
diff --git a/gerrit-plugin-api/BUCK b/gerrit-plugin-api/BUCK
index a6afafb..80ab9a3 100644
--- a/gerrit-plugin-api/BUCK
+++ b/gerrit-plugin-api/BUCK
@@ -25,12 +25,16 @@
     '//gerrit-common:annotations',
     '//gerrit-common:server',
     '//gerrit-extension-api:api',
+    '//gerrit-gwtexpui:server',
     '//gerrit-reviewdb:server',
     '//lib:args4j',
     '//lib:guava',
     '//lib:gwtorm',
     '//lib:jsch',
+    '//lib:mime-util',
     '//lib:servlet-api-3_1',
+    '//lib/commons:io',
+    '//lib/commons:lang',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',
@@ -52,7 +56,7 @@
 java_doc(
   name = 'plugin-api-javadoc',
   title = 'Gerrit Review Plugin API Documentation',
-  pkg = 'com.google.gerrit',
+  pkgs = ['com.google.gerrit'],
   paths = [n for n in SRCS],
   srcs = glob([n + '**/*.java' for n in SRCS]),
   deps = [
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
index 3b8be2f..3f9e37c 100644
--- a/gerrit-plugin-api/pom.xml
+++ b/gerrit-plugin-api/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-api</artifactId>
-  <version>2.10.3</version>
+  <version>2.11</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin API</name>
   <description>API for Gerrit Plugins</description>
diff --git a/gerrit-plugin-archetype/pom.xml b/gerrit-plugin-archetype/pom.xml
index 4056cf3..4e2104b 100644
--- a/gerrit-plugin-archetype/pom.xml
+++ b/gerrit-plugin-archetype/pom.xml
@@ -20,7 +20,7 @@
 
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-archetype</artifactId>
-  <version>2.10.3</version>
+  <version>2.11</version>
   <name>Gerrit Code Review - Plugin Archetype</name>
   <description>Maven Archetype for Gerrit Plugins</description>
   <url>http://code.google.com/p/gerrit/</url>
diff --git a/gerrit-plugin-gwt-archetype/pom.xml b/gerrit-plugin-gwt-archetype/pom.xml
index 08a0f01..d6ea69a 100644
--- a/gerrit-plugin-gwt-archetype/pom.xml
+++ b/gerrit-plugin-gwt-archetype/pom.xml
@@ -20,7 +20,7 @@
 
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-gwt-archetype</artifactId>
-  <version>2.10.3</version>
+  <version>2.11</version>
   <name>Gerrit Code Review - Web UI GWT Plugin Archetype</name>
   <description>Maven Archetype for Gerrit Web UI GWT Plugins</description>
   <url>http://code.google.com/p/gerrit/</url>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
index b443c4d..fa86ab4 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -25,7 +25,7 @@
       <defaultValue>http://code.google.com/p/gerrit/</defaultValue>
     </requiredProperty>
     <requiredProperty key="Gwt-Version">
-      <defaultValue>2.6.1</defaultValue>
+      <defaultValue>2.7.0</defaultValue>
     </requiredProperty>
 
     <requiredProperty key="gerritApiVersion">
@@ -51,6 +51,18 @@
       </includes>
     </fileSet>
 
+    <fileSet filtered="true">
+      <directory></directory>
+      <include>.buckconfig</include>
+      <include>BUCK</include>
+      <include>VERSION</include>
+      <include>lib/gerrit/BUCK</include>
+      <include>lib/gwt/BUCK</include>
+      <excludes>
+        <exclude>**/*.java</exclude>
+      </excludes>
+    </fileSet>
+
     <fileSet>
       <directory></directory>
       <includes>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.buckconfig b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.buckconfig
new file mode 100644
index 0000000..1044c12
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.buckconfig
@@ -0,0 +1,14 @@
+[alias]
+  ${pluginName} = //:${pluginName}
+  plugin = //:${pluginName}
+
+[java]
+  src_roots = java, resources
+
+[project]
+  ignore = .git
+
+[cache]
+  mode = dir
+  dir = buck-out/cache
+
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.gitignore b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.gitignore
index 80d6257..43838b0 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.gitignore
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.gitignore
@@ -1,3 +1,7 @@
+/.buckversion
+/.buckd
+/buck-out
+/bucklets
 /target
 /.classpath
 /.project
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/BUCK b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/BUCK
new file mode 100644
index 0000000..b19312c
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/BUCK
@@ -0,0 +1,23 @@
+include_defs('//bucklets/gerrit_plugin.bucklet')
+
+gerrit_plugin(
+  name = '${pluginName}',
+  srcs = glob(['src/main/java/**/*.java']),
+  resources = glob(['src/main/**/*']),
+  gwt_module = '${package}.HelloPlugin',
+  manifest_entries = [
+    'Gerrit-PluginName: ${pluginName}',
+    'Gerrit-ApiType: plugin',
+    'Gerrit-ApiVersion: ${gerritApiVersion}',
+    'Gerrit-Module: ${package}.Module',
+    'Gerrit-SshModule: ${package}.SshModule',
+    'Gerrit-HttpModule: ${package}.HttpModule',
+  ],
+)
+
+# this is required for bucklets/tools/eclipse/project.py to work
+java_library(
+  name = 'classpath',
+  deps = [':${pluginName}__plugin'],
+)
+
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/VERSION b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/VERSION
new file mode 100644
index 0000000..8bbb460
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/VERSION
@@ -0,0 +1,5 @@
+# Used by BUCK to include "Implementation-Version" in plugin Manifest.
+# If this file doesn't exist the output of 'git describe' is used
+# instead.
+PLUGIN_VERSION = '${version}'
+
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gerrit/BUCK b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gerrit/BUCK
new file mode 100644
index 0000000..0a0d8b9
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gerrit/BUCK
@@ -0,0 +1,20 @@
+include_defs('//bucklets/maven_jar.bucklet')
+
+VER = '${gerritApiVersion}'
+REPO = MAVEN_LOCAL
+
+maven_jar(
+  name = 'plugin-api',
+  id = 'com.google.gerrit:gerrit-plugin-api:' + VER,
+  attach_source = False,
+  repository = REPO,
+  license = 'Apache2.0',
+)
+
+maven_jar(
+  name = 'gwtui-api',
+  id = 'com.google.gerrit:gerrit-plugin-gwtui:' + VER,
+  attach_source = False,
+  repository = REPO,
+  license = 'Apache2.0',
+)
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gwt/BUCK b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gwt/BUCK
new file mode 100644
index 0000000..511a8ec
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/lib/gwt/BUCK
@@ -0,0 +1,32 @@
+include_defs('//bucklets/maven_jar.bucklet')
+
+VERSION = '${Gwt-Version}'
+
+maven_jar(
+  name = 'user',
+  id = 'com.google.gwt:gwt-user:' + VERSION,
+  license = 'Apache2.0',
+  attach_source = False,
+)
+
+maven_jar(
+  name = 'dev',
+  id = 'com.google.gwt:gwt-dev:' + VERSION,
+  license = 'Apache2.0',
+  deps = [
+    ':javax-validation',
+    ':javax-validation_src',
+  ],
+  attach_source = False,
+  exclude = ['org/eclipse/jetty/*'],
+)
+
+maven_jar(
+  name = 'javax-validation',
+  id = 'javax.validation:validation-api:1.0.0.GA',
+  bin_sha1 = 'b6bd7f9d78f6fdaa3c37dae18a4bd298915f328e',
+  src_sha1 = '7a561191db2203550fbfa40d534d4997624cd369',
+  license = 'Apache2.0',
+  visibility = [],
+)
+
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md
new file mode 100644
index 0000000..4c56ed6
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/build.md
@@ -0,0 +1,77 @@
+Build
+=====
+
+This plugin can be built with Buck or Maven.
+
+Buck
+----
+
+Two build modes are supported: Standalone and in Gerrit tree.
+The standalone build mode is recommended, as this mode doesn't require
+the Gerrit tree to exist locally.
+
+
+
+Clone bucklets library:
+
+```
+  git clone https://gerrit.googlesource.com/bucklets
+
+```
+and link it to @PLUGIN@ plugin directory:
+
+```
+  cd @PLUGIN@ && ln -s ../bucklets .
+```
+
+Add link to the .buckversion file:
+
+```
+  cd @PLUGIN@ && ln -s bucklets/buckversion .buckversion
+```
+
+To build the plugin, issue the following command:
+
+
+```
+  buck build plugin
+```
+
+The output is created in
+
+```
+  buck-out/gen/@PLUGIN@.jar
+```
+
+
+Clone or link this plugin to the plugins directory of Gerrit's source
+tree, and issue the command:
+
+```
+  buck build plugins/@PLUGIN@
+```
+
+The output is created in
+
+```
+  buck-out/gen/plugins/@PLUGIN@/@PLUGIN@.jar
+```
+
+This project can be imported into the Eclipse IDE:
+
+```
+  ./tools/eclipse/project.py
+```
+
+Maven
+-----
+
+Note that the Maven build is provided for compatibility reasons, but
+it is considered to be deprecated and will be removed in a future
+version of this plugin.
+
+To build with Maven, run
+
+```
+mvn clean package
+```
diff --git a/gerrit-plugin-gwtui/BUCK b/gerrit-plugin-gwtui/BUCK
index 419176d..3b695ce 100644
--- a/gerrit-plugin-gwtui/BUCK
+++ b/gerrit-plugin-gwtui/BUCK
@@ -1,4 +1,5 @@
 COMMON = ['gerrit-gwtui-common/src/main/java/']
+GWTEXPUI = ['gerrit-gwtexpui/src/main/java/']
 SRC = 'src/main/java/com/google/gerrit/'
 SRCS = glob([SRC + '**/*.java'])
 
@@ -11,6 +12,7 @@
   name = 'gwtui-api',
   deps = [
     ':gwtui-api-lib',
+    '//gerrit-extension-api:client-lib',
     '//gerrit-gwtui-common:client-lib',
   ],
   visibility = ['PUBLIC'],
@@ -26,7 +28,14 @@
   name = 'gwtui-api-lib2',
   srcs = SRCS,
   resources = glob(['src/main/**/*']),
-  exported_deps = ['//gerrit-gwtui-common:client-lib2'],
+  exported_deps = [
+    '//gerrit-extension-api:client-lib',
+    '//gerrit-gwtexpui:Clippy',
+    '//gerrit-gwtexpui:GlobalKey',
+    '//gerrit-gwtexpui:SafeHtml',
+    '//gerrit-gwtexpui:UserAgent',
+    '//gerrit-gwtui-common:client-lib2',
+  ],
   provided_deps = DEPS,
   visibility = ['PUBLIC'],
 )
@@ -35,6 +44,7 @@
   name = 'gwtui-api-src',
   deps = [
     ':gwtui-api-src-lib',
+    '//gerrit-gwtexpui:client-src-lib',
     '//gerrit-gwtui-common:client-src-lib',
   ],
   visibility = ['PUBLIC'],
@@ -50,9 +60,16 @@
 java_doc(
   name = 'gwtui-api-javadoc',
   title = 'Gerrit Review GWT Extension API Documentation',
-  pkg = 'com.google.gerrit',
-  paths = ['src/main/java'] + COMMON,
+  pkgs = [
+    'com.google.gerrit',
+    'com.google.gwtexpui.clippy',
+    'com.google.gwtexpui.globalkey',
+    'com.google.gwtexpui.safehtml',
+    'com.google.gwtexpui.user',
+  ],
+  paths = COMMON + GWTEXPUI,
   srcs = SRCS,
   deps = DEPS + ['//gerrit-gwtui-common:client-lib2'],
   visibility = ['PUBLIC'],
+  do_it_wrong = True,
 )
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml
index 69c776c..caf53ca 100644
--- a/gerrit-plugin-gwtui/pom.xml
+++ b/gerrit-plugin-gwtui/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-gwtui</artifactId>
-  <version>2.10.3</version>
+  <version>2.11</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin GWT UI</name>
   <description>Common Classes for Gerrit GWT UI Plugins</description>
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java
index 046488d..bf19352 100644
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java
@@ -54,6 +54,10 @@
   public final native void refreshMenuBar()
   /*-{ return this.refreshMenuBar() }-*/;
 
+  /** Check if user is signed in. */
+  public final native boolean isSignedIn()
+  /*-{ return this.isSignedIn() }-*/;
+
   /** Show message in Gerrit's ErrorDialog. */
   public final native void showError(String message)
   /*-{ return this.showError(message) }-*/;
@@ -92,7 +96,7 @@
 
   native void _initialized() /*-{ this._success = true }-*/;
   native void _loaded() /*-{ this._loadedGwt() }-*/;
-  private static native final Plugin install(String u)
+  private static final native Plugin install(String u)
   /*-{ return $wnd.Gerrit.installGwt(u) }-*/;
 
   private static final native JavaScriptObject wrap(Screen.EntryPoint b) /*-{
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/PluginEntryPoint.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/PluginEntryPoint.java
index ca1cdbf..0f8a16a 100644
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/PluginEntryPoint.java
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/PluginEntryPoint.java
@@ -32,6 +32,7 @@
    */
   public abstract void onPluginLoad();
 
+  @Override
   public final void onModuleLoad() {
     Plugin self = Plugin.get();
     try {
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/RestApi.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/RestApi.java
index cfbc8a5..8b2a6b8 100644
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/RestApi.java
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/RestApi.java
@@ -109,7 +109,7 @@
     get(NativeString.unwrap(cb));
   }
 
-  private native static void get(String p, JavaScriptObject r)
+  private static native void get(String p, JavaScriptObject r)
   /*-{ $wnd.Gerrit.get(p, r) }-*/;
 
   public <T extends JavaScriptObject>
@@ -117,7 +117,7 @@
     put(path(), wrap(cb));
   }
 
-  private native static void put(String p, JavaScriptObject r)
+  private static native void put(String p, JavaScriptObject r)
   /*-{ $wnd.Gerrit.put(p, r) }-*/;
 
   public <T extends JavaScriptObject>
@@ -125,7 +125,7 @@
     put(path(), content, wrap(cb));
   }
 
-  private native static
+  private static native
   void put(String p, String c, JavaScriptObject r)
   /*-{ $wnd.Gerrit.put(p, c, r) }-*/;
 
@@ -134,7 +134,7 @@
     put(path(), content, wrap(cb));
   }
 
-  private native static
+  private static native
   void put(String p, JavaScriptObject c, JavaScriptObject r)
   /*-{ $wnd.Gerrit.put(p, c, r) }-*/;
 
@@ -143,7 +143,7 @@
     post(path(), content, wrap(cb));
   }
 
-  private native static
+  private static native
   void post(String p, String c, JavaScriptObject r)
   /*-{ $wnd.Gerrit.post(p, c, r) }-*/;
 
@@ -152,7 +152,7 @@
     post(path(), content, wrap(cb));
   }
 
-  private native static
+  private static native
   void post(String p, JavaScriptObject c, JavaScriptObject r)
   /*-{ $wnd.Gerrit.post(p, c, r) }-*/;
 
@@ -160,10 +160,10 @@
     delete(path(), wrap(cb));
   }
 
-  private native static void delete(String p, JavaScriptObject r)
+  private static native void delete(String p, JavaScriptObject r)
   /*-{ $wnd.Gerrit.del(p, r) }-*/;
 
-  private native static <T extends JavaScriptObject>
+  private static native <T extends JavaScriptObject>
   JavaScriptObject wrap(AsyncCallback<T> b) /*-{
     return function(r) {
       b.@com.google.gwt.user.client.rpc.AsyncCallback::onSuccess(Ljava/lang/Object;)(r)
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/rebind/PluginGenerator.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/rebind/PluginGenerator.java
index 8278280..89bb026 100644
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/rebind/PluginGenerator.java
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/rebind/PluginGenerator.java
@@ -15,8 +15,6 @@
 
 package com.google.gerrit.plugin.rebind;
 
-import java.io.PrintWriter;
-
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.ext.Generator;
 import com.google.gwt.core.ext.GeneratorContext;
@@ -27,6 +25,8 @@
 import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
 import com.google.gwt.user.rebind.SourceWriter;
 
+import java.io.PrintWriter;
+
 /**
  * Write the top layer in the Gadget bootstrap sandwich and generate a stub
  * manifest that will be completed by the linker.
diff --git a/gerrit-plugin-js-archetype/pom.xml b/gerrit-plugin-js-archetype/pom.xml
index 959556d..3fb270c 100644
--- a/gerrit-plugin-js-archetype/pom.xml
+++ b/gerrit-plugin-js-archetype/pom.xml
@@ -20,7 +20,7 @@
 
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-js-archetype</artifactId>
-  <version>2.10.3</version>
+  <version>2.11</version>
   <name>Gerrit Code Review - Web UI JavaScript Plugin Archetype</name>
   <description>Maven Archetype for Gerrit Web UI JavaScript Plugins</description>
   <url>http://code.google.com/p/gerrit/</url>
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
index ed42f10..cdf800c 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
@@ -28,7 +28,7 @@
 import java.util.Set;
 
 public abstract class PrettyFormatter implements SparseHtmlFile {
-  public static abstract class EditFilter {
+  public abstract static class EditFilter {
     abstract String getStyleName();
 
     abstract int getBegin(Edit edit);
@@ -82,10 +82,12 @@
   private Tag lastTag;
   private StringBuilder buf;
 
+  @Override
   public SafeHtml getSafeHtmlLine(int lineNo) {
     return SafeHtml.asis(content.get(lineNo));
   }
 
+  @Override
   public int size() {
     return content.size();
   }
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java
index 1d23009..6a42a8e 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java
@@ -39,14 +39,17 @@
 
   public Iterable<Hunk> getHunks() {
     return new Iterable<Hunk>() {
+      @Override
       public Iterator<Hunk> iterator() {
         return new Iterator<Hunk>() {
           private int curIdx;
 
+          @Override
           public boolean hasNext() {
             return curIdx < edits.size();
           }
 
+          @Override
           public Hunk next() {
             final int c = curIdx;
             final int e = findCombinedEnd(c);
@@ -54,6 +57,7 @@
             return new Hunk(c, e);
           }
 
+          @Override
           public void remove() {
             throw new UnsupportedOperationException();
           }
diff --git a/gerrit-reviewdb/BUCK b/gerrit-reviewdb/BUCK
index faf80a8..ca2c18c 100644
--- a/gerrit-reviewdb/BUCK
+++ b/gerrit-reviewdb/BUCK
@@ -1,4 +1,5 @@
 SRC = 'src/main/java/com/google/gerrit/reviewdb/'
+TESTS = 'src/test/java/com/google/gerrit/reviewdb/'
 
 gwt_module(
   name = 'client',
@@ -22,3 +23,17 @@
   ],
   visibility = ['PUBLIC'],
 )
+
+java_test(
+  name = 'client_tests',
+  srcs = glob([TESTS + 'client/**/*.java']),
+  deps = [
+    ':client',
+    '//lib:guava',
+    '//lib:gwtorm',
+    '//lib:junit',
+    '//lib:truth',
+  ],
+  source_under_test = [':client'],
+  visibility = ['//tools/eclipse:classpath'],
+)
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
index e131f7a..d4fb311 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.reviewdb.client;
 
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_USER;
+
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
 
@@ -104,6 +106,65 @@
       r.fromString(str);
       return r;
     }
+
+    public static Id fromRef(String name) {
+      if (name == null) {
+        return null;
+      }
+      if (name.startsWith(REFS_USER)) {
+        return fromRefPart(name.substring(REFS_USER.length()));
+      }
+      return null;
+    }
+
+    /**
+     * Parse an Account.Id out of a part of a ref-name.
+     *
+     * @param name  a ref name with the following syntax: {@code "34/1234..."}.
+     *              We assume that the caller has trimmed any prefix.
+     */
+    public static Id fromRefPart(String name) {
+      if (name == null) {
+        return null;
+      }
+
+      String[] parts = name.split("/");
+      int n = parts.length;
+      if (n < 2) {
+        return null;
+      }
+
+      // Last 2 digits.
+      int le;
+      for (le = 0; le < parts[0].length(); le++) {
+        if (!Character.isDigit(parts[0].charAt(le))) {
+          return null;
+        }
+      }
+      if (le != 2) {
+        return null;
+      }
+
+      // Full ID.
+      int ie;
+      for (ie = 0; ie < parts[1].length(); ie++) {
+        if (!Character.isDigit(parts[1].charAt(ie))) {
+          if (ie == 0) {
+            return null;
+          } else {
+            break;
+          }
+        }
+      }
+
+      int shard = Integer.parseInt(parts[0]);
+      int id = Integer.parseInt(parts[1].substring(0, ie));
+
+      if (id % 100 != shard) {
+        return null;
+      }
+      return new Account.Id(id);
+    }
   }
 
   @Column(id = 1)
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
index cf951c1..7948080 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.reviewdb.client;
 
+import com.google.gerrit.extensions.client.Theme;
 import com.google.gwtorm.client.Column;
 
 /** Diff formatting preferences of an account */
@@ -41,6 +42,7 @@
       code = c;
     }
 
+    @Override
     public char getCode() {
       return code;
     }
@@ -55,29 +57,6 @@
     }
   }
 
-  public static enum Theme {
-    // Light themes
-    DEFAULT,
-    ECLIPSE,
-    ELEGANT,
-    NEAT,
-    // Dark themes
-    MIDNIGHT,
-    NIGHT,
-    TWILIGHT;
-
-    public boolean isDark() {
-      switch (this) {
-        case MIDNIGHT:
-        case NIGHT:
-        case TWILIGHT:
-          return true;
-        default:
-          return false;
-      }
-    }
-  }
-
   public static AccountDiffPreference createDefault(Account.Id accountId) {
     AccountDiffPreference p = new AccountDiffPreference(accountId);
     p.setIgnoreWhitespace(Whitespace.IGNORE_NONE);
@@ -92,6 +71,7 @@
     p.setContext(DEFAULT_CONTEXT);
     p.setManualReview(false);
     p.setHideEmptyPane(false);
+    p.setAutoHideDiffTableHeader(true);
     return p;
   }
 
@@ -156,6 +136,9 @@
   @Column(id = 20)
   protected boolean hideEmptyPane;
 
+  @Column(id = 21)
+  protected boolean autoHideDiffTableHeader;
+
   protected AccountDiffPreference() {
   }
 
@@ -183,6 +166,7 @@
     this.hideLineNumbers = p.hideLineNumbers;
     this.renderEntireFile = p.renderEntireFile;
     this.hideEmptyPane = p.hideEmptyPane;
+    this.autoHideDiffTableHeader = p.autoHideDiffTableHeader;
   }
 
   public Account.Id getAccountId() {
@@ -343,4 +327,12 @@
   public void setHideEmptyPane(boolean hideEmptyPane) {
     this.hideEmptyPane = hideEmptyPane;
   }
+
+  public void setAutoHideDiffTableHeader(boolean hide) {
+    autoHideDiffTableHeader = hide;
+  }
+
+  public boolean isAutoHideDiffTableHeader() {
+    return autoHideDiffTableHeader;
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
index 8181d50..8f9c726 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
@@ -36,6 +36,9 @@
   /** Scheme for the username used to authenticate an account, e.g. over SSH. */
   public static final String SCHEME_USERNAME = "username:";
 
+  /** Scheme for external auth used during authentication, e.g. OAuth Token */
+  public static final String SCHEME_EXTERNAL = "external:";
+
   public static class Key extends StringKey<com.google.gwtorm.client.Key<?>> {
     private static final long serialVersionUID = 1L;
 
@@ -65,6 +68,11 @@
     protected void set(String newValue) {
       externalId = newValue;
     }
+
+    public String getScheme() {
+      int c = externalId.indexOf(':');
+      return 0 < c ? externalId.substring(0, c) : null;
+    }
   }
 
   @Column(id = 1, name = Column.NONE)
@@ -126,9 +134,10 @@
   }
 
   public String getSchemeRest() {
-    String id = getExternalId();
-    int c = id.indexOf(':');
-    return 0 < c ? id.substring(c + 1) : null;
+    String scheme = key.getScheme();
+    return null != scheme
+        ? getExternalId().substring(scheme.length() + 1)
+        : null;
   }
 
   public String getPassword() {
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 c6b3089..31494ba 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
@@ -68,13 +68,6 @@
     }
   }
 
-  public static enum CommentVisibilityStrategy {
-    COLLAPSE_ALL,
-    EXPAND_MOST_RECENT,
-    EXPAND_RECENT,
-    EXPAND_ALL
-  }
-
   public static enum ReviewCategoryStrategy {
     NONE,
     NAME,
@@ -88,11 +81,6 @@
     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"),
@@ -147,24 +135,18 @@
   @Column(id = 9, length = 10, notNull = false)
   protected String timeFormat;
 
-  /**
-   * If true display the patch sets in the ChangeScreen in reverse order
-   * (show latest patch set on top).
-   */
-  @Column(id = 10)
-  protected boolean reversePatchSetOrder;
+  // DELETED: id = 10 (reversePatchSetOrder)
+  // DELETED: id = 11 (showUserInReview)
 
   @Column(id = 12)
   protected boolean relativeDateInChangeTable;
 
-  @Column(id = 13, length = 20, notNull = false)
-  protected String commentVisibilityStrategy;
+  // DELETED: id = 13 (commentVisibilityStrategy)
 
   @Column(id = 14, length = 20, notNull = false)
   protected String diffView;
 
-  @Column(id = 15, length = 20, notNull = false)
-  protected String changeScreen;
+  // DELETED: id = 15 (changeScreen)
 
   @Column(id = 16)
   protected boolean sizeBarInChangeTable;
@@ -175,6 +157,9 @@
   @Column(id = 18, length = 20, notNull = false)
   protected String reviewCategoryStrategy;
 
+  @Column(id = 19)
+  protected boolean muteCommonPathPrefixes;
+
   public AccountGeneralPreferences() {
   }
 
@@ -240,14 +225,6 @@
     copySelfOnEmail = includeSelfOnEmail;
   }
 
-  public boolean isReversePatchSetOrder() {
-    return reversePatchSetOrder;
-  }
-
-  public void setReversePatchSetOrder(final boolean reversePatchSetOrder) {
-    this.reversePatchSetOrder = reversePatchSetOrder;
-  }
-
   public boolean isShowInfoInReviewCategory() {
     return getReviewCategoryStrategy() != ReviewCategoryStrategy.NONE;
   }
@@ -294,18 +271,6 @@
     reviewCategoryStrategy = strategy.name();
   }
 
-  public CommentVisibilityStrategy getCommentVisibilityStrategy() {
-    if (commentVisibilityStrategy == null) {
-      return CommentVisibilityStrategy.EXPAND_RECENT;
-    }
-    return CommentVisibilityStrategy.valueOf(commentVisibilityStrategy);
-  }
-
-  public void setCommentVisibilityStrategy(
-      CommentVisibilityStrategy strategy) {
-    commentVisibilityStrategy = strategy.name();
-  }
-
   public DiffView getDiffView() {
     if (diffView == null) {
       return DiffView.SIDE_BY_SIDE;
@@ -317,14 +282,6 @@
     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 boolean isSizeBarInChangeTable() {
     return sizeBarInChangeTable;
   }
@@ -341,22 +298,29 @@
     this.legacycidInChangeTable = legacycidInChangeTable;
   }
 
+  public boolean isMuteCommonPathPrefixes() {
+    return muteCommonPathPrefixes;
+  }
+
+  public void setMuteCommonPathPrefixes(
+      boolean muteCommonPathPrefixes) {
+    this.muteCommonPathPrefixes = muteCommonPathPrefixes;
+  }
+
   public void resetToDefaults() {
     maximumPageSize = DEFAULT_PAGESIZE;
     showSiteHeader = true;
     useFlashClipboard = true;
     copySelfOnEmail = false;
-    reversePatchSetOrder = false;
     reviewCategoryStrategy = null;
     downloadUrl = null;
     downloadCommand = null;
     dateFormat = null;
     timeFormat = null;
     relativeDateInChangeTable = false;
-    commentVisibilityStrategy = null;
     diffView = null;
-    changeScreen = null;
     sizeBarInChangeTable = true;
     legacycidInChangeTable = false;
+    muteCommonPathPrefixes = true;
   }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
index 46f7873..284ae0a 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
@@ -124,13 +124,20 @@
   @Column(id = 2)
   protected Id groupId;
 
+  // DELETED: id = 3 (ownerGroupId)
+
   /** A textual description of the group's purpose. */
   @Column(id = 4, length = Integer.MAX_VALUE, notNull = false)
   protected String description;
 
+  // DELETED: id = 5 (groupType)
+  // DELETED: id = 6 (externalName)
+
   @Column(id = 7)
   protected boolean visibleToAll;
 
+  // DELETED: id = 8 (emailOnlyAuthors)
+
   /** Globally unique identifier name for this group. */
   @Column(id = 9)
   protected UUID groupUUID;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index dbce048..fe2cd96 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -14,12 +14,16 @@
 
 package com.google.gerrit.reviewdb.client;
 
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
+
+import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
 import com.google.gwtorm.client.RowVersion;
 import com.google.gwtorm.client.StringKey;
 
 import java.sql.Timestamp;
+import java.util.Arrays;
 
 /**
  * A change proposed to be merged into a {@link Branch}.
@@ -128,8 +132,64 @@
       return r;
     }
 
-    public static Id fromRef(final String ref) {
-      return PatchSet.Id.fromRef(ref).getParentKey();
+    public static Id fromRef(String ref) {
+      int cs = startIndex(ref);
+      if (cs < 0) {
+        return null;
+      }
+      int ce = nextNonDigit(ref, cs);
+      if (ref.substring(ce).equals(RefNames.META_SUFFIX)
+          || PatchSet.Id.fromRef(ref, ce) >= 0) {
+        return new Change.Id(Integer.parseInt(ref.substring(cs, ce)));
+      }
+      return null;
+    }
+
+    static int startIndex(String ref) {
+      if (ref == null || !ref.startsWith(REFS_CHANGES)) {
+        return -1;
+      }
+
+      // Last 2 digits.
+      int ls = REFS_CHANGES.length();
+      int le = nextNonDigit(ref, ls);
+      if (le - ls != 2 || le >= ref.length() || ref.charAt(le) != '/') {
+        return -1;
+      }
+
+      // Change ID.
+      int cs = le + 1;
+      if (cs >= ref.length() || ref.charAt(cs) == '0') {
+        return -1;
+      }
+      int ce = nextNonDigit(ref, cs);
+      if (ce >= ref.length() || ref.charAt(ce) != '/') {
+        return -1;
+      }
+      switch (ce - cs) {
+        case 0:
+          return -1;
+        case 1:
+          if (ref.charAt(ls) != '0'
+              || ref.charAt(ls + 1) != ref.charAt(cs)) {
+            return -1;
+          }
+          break;
+        default:
+          if (ref.charAt(ls) != ref.charAt(ce - 2)
+              || ref.charAt(ls + 1) != ref.charAt(ce - 1)) {
+            return -1;
+          }
+          break;
+      }
+      return cs;
+    }
+
+    static int nextNonDigit(String s, int i) {
+      while (i < s.length() && s.charAt(i) >= '0' && s.charAt(i) <= '9') {
+        i++;
+      }
+      return i;
     }
   }
 
@@ -221,7 +281,7 @@
      * <li>{@link #ABANDONED} - when the Abandon action is used.
      * </ul>
      */
-    NEW(STATUS_NEW),
+    NEW(STATUS_NEW, ChangeStatus.NEW),
 
     /**
      * Change is open, but has been submitted to the merge queue.
@@ -248,7 +308,7 @@
      * <li>{@link #ABANDONED} - when the Abandon action is used.
      * </ul>
      */
-    SUBMITTED(STATUS_SUBMITTED),
+    SUBMITTED(STATUS_SUBMITTED, ChangeStatus.SUBMITTED),
 
     /**
      * Change is a draft change that only consists of draft patchsets.
@@ -266,7 +326,7 @@
      * <li>{@link #NEW} - when the change is published, it becomes a new change;
      * </ul>
      */
-    DRAFT(STATUS_DRAFT),
+    DRAFT(STATUS_DRAFT, ChangeStatus.DRAFT),
 
     /**
      * Change is closed, and submitted to its destination branch.
@@ -276,7 +336,7 @@
      * replacement patch set. Draft comments however may be published,
      * supporting a post-submit review.
      */
-    MERGED(STATUS_MERGED),
+    MERGED(STATUS_MERGED, ChangeStatus.MERGED),
 
     /**
      * Change is closed, but was not submitted to its destination branch.
@@ -286,14 +346,31 @@
      * a replacement patch set, and it cannot be merged. Draft comments however
      * may be published, permitting reviewers to send constructive feedback.
      */
-    ABANDONED('A');
+    ABANDONED('A', ChangeStatus.ABANDONED);
+
+    static {
+      boolean ok = true;
+      if (Status.values().length != ChangeStatus.values().length) {
+        ok = false;
+      }
+      for (Status s : Status.values()) {
+        ok &= s.name().equals(s.changeStatus.name());
+      }
+      if (!ok) {
+        throw new IllegalStateException("Mismatched status mapping: "
+            + Arrays.asList(Status.values()) + " != "
+            + Arrays.asList(ChangeStatus.values()));
+      }
+    }
 
     private final char code;
     private final boolean closed;
+    private final ChangeStatus changeStatus;
 
-    private Status(final char c) {
+    private Status(char c, ChangeStatus cs) {
       code = c;
       closed = !(MIN_OPEN <= c && c <= MAX_OPEN);
+      changeStatus = cs;
     }
 
     public char getCode() {
@@ -308,6 +385,10 @@
       return closed;
     }
 
+    public ChangeStatus asChangeStatus() {
+      return changeStatus;
+    }
+
     public static Status forCode(final char c) {
       for (final Status s : Status.values()) {
         if (s.code == c) {
@@ -316,6 +397,15 @@
       }
       return null;
     }
+
+    public static Status forChangeStatus(ChangeStatus cs) {
+      for (Status s : Status.values()) {
+        if (s.changeStatus == cs) {
+          return s;
+        }
+      }
+      return null;
+    }
   }
 
   /** Locally assigned unique identifier of the change */
@@ -343,9 +433,7 @@
   @Column(id = 5)
   protected Timestamp lastUpdatedOn;
 
-  /** A {@link #lastUpdatedOn} ASC,{@link #changeId} ASC for sorting. */
-  @Column(id = 6, length = 16)
-  protected String sortKey;
+  // DELETED: id = 6 (sortkey)
 
   @Column(id = 7, name = "owner_account_id")
   protected Account.Id owner;
@@ -354,14 +442,14 @@
   @Column(id = 8)
   protected Branch.NameKey dest;
 
-  /** Is the change currently open? Set to {@link #status}.isOpen(). */
-  @Column(id = 9)
-  protected boolean open;
+  // DELETED: id = 9 (open)
 
   /** Current state code; see {@link Status}. */
   @Column(id = 10)
   protected char status;
 
+  // DELETED: id = 11 (nbrPatchSets)
+
   /** The current patch set. */
   @Column(id = 12)
   protected int currentPatchSetId;
@@ -374,16 +462,17 @@
   @Column(id = 14, notNull = false)
   protected String topic;
 
-  /**
-   * Null if the change has never been tested.
-   * Empty if it has been tested but against a branch that does
-   * not exist.
-   */
-  @Column(id = 15, notNull = false)
-  protected RevId lastSha1MergeTested;
+  // DELETED: id = 15 (lastSha1MergeTested)
+  // DELETED: id = 16 (mergeable)
 
-  @Column(id = 16)
-  protected boolean mergeable;
+  /**
+   * First line of first patch set's commit message.
+   *
+   * Unlike {@link #subject}, this string does not change if future patch sets
+   * change the first line.
+   */
+  @Column(id = 17, notNull = false)
+  protected String originalSubject;
 
   protected Change() {
   }
@@ -397,7 +486,6 @@
     owner = ownedBy;
     dest = forBranch;
     setStatus(Status.NEW);
-    setLastSha1MergeTested(null);
   }
 
   public Change(Change other) {
@@ -406,16 +494,13 @@
     rowVersion = other.rowVersion;
     createdOn = other.createdOn;
     lastUpdatedOn = other.lastUpdatedOn;
-    sortKey = other.sortKey;
     owner = other.owner;
     dest = other.dest;
-    open = other.open;
     status = other.status;
     currentPatchSetId = other.currentPatchSetId;
     subject = other.subject;
+    originalSubject = other.originalSubject;
     topic = other.topic;
-    mergeable = other.mergeable;
-    lastSha1MergeTested = other.lastSha1MergeTested;
   }
 
   /** Legacy 32 bit integer identity for a change. */
@@ -453,14 +538,6 @@
     return rowVersion;
   }
 
-  public String getSortKey() {
-    return sortKey;
-  }
-
-  public void setSortKey(final String newSortKey) {
-    sortKey = newSortKey;
-  }
-
   public Account.Id getOwner() {
     return owner;
   }
@@ -477,6 +554,10 @@
     return subject;
   }
 
+  public String getOriginalSubject() {
+    return originalSubject != null ? originalSubject : subject;
+  }
+
   /** Get the id of the most current {@link PatchSet} in this change. */
   public PatchSet.Id currentPatchSetId() {
     if (currentPatchSetId > 0) {
@@ -486,16 +567,27 @@
   }
 
   public void setCurrentPatchSet(final PatchSetInfo ps) {
+    if (originalSubject == null && subject != null) {
+      // Change was created before schema upgrade. Use the last subject
+      // associated with this change, as the most recent discussion will
+      // be under that thread in an email client such as GMail.
+      originalSubject = subject;
+    }
+
     currentPatchSetId = ps.getKey().get();
     subject = ps.getSubject();
+
+    if (originalSubject == null) {
+      // Newly created changes remember the first commit's subject.
+      originalSubject = subject;
+    }
   }
 
   public Status getStatus() {
     return Status.forCode(status);
   }
 
-  public void setStatus(final Status newStatus) {
-    open = newStatus.isOpen();
+  public void setStatus(Status newStatus) {
     status = newStatus.getCode();
   }
 
@@ -507,19 +599,13 @@
     this.topic = topic;
   }
 
-  public RevId getLastSha1MergeTested() {
-    return lastSha1MergeTested;
-  }
-
-  public void setLastSha1MergeTested(RevId lastSha1MergeTested) {
-    this.lastSha1MergeTested = lastSha1MergeTested;
-  }
-
-  public boolean isMergeable() {
-    return mergeable;
-  }
-
-  public void setMergeable(boolean mergeable) {
-    this.mergeable = mergeable;
+  @Override
+  public String toString() {
+    return new StringBuilder(getClass().getSimpleName())
+        .append('{').append(changeId)
+        .append(" (").append(changeKey).append("), ")
+        .append("dest=").append(dest).append(", ")
+        .append("status=").append(status).append('}')
+        .toString();
   }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/InheritedBoolean.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/InheritedBoolean.java
deleted file mode 100644
index b5cef60..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/InheritedBoolean.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.client;
-
-import com.google.gerrit.extensions.common.InheritableBoolean;
-
-public class InheritedBoolean {
-
-  public InheritableBoolean value;
-  public boolean inheritedValue;
-
-  public InheritedBoolean() {
-  }
-
-  public void setValue(final InheritableBoolean value) {
-    this.value = value;
-  }
-
-  public void setInheritedValue(final boolean inheritedValue) {
-    this.inheritedValue = inheritedValue;
-  }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/LabelId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/LabelId.java
new file mode 100644
index 0000000..9a15966
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/LabelId.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.client;
+
+import com.google.gwtorm.client.Column;
+import com.google.gwtorm.client.StringKey;
+
+public class LabelId extends StringKey<com.google.gwtorm.client.Key<?>> {
+  private static final long serialVersionUID = 1L;
+
+  public static final LabelId SUBMIT = new LabelId("SUBM");
+
+  @Column(id = 1)
+  protected String id;
+
+  protected LabelId() {
+  }
+
+  public LabelId(final String n) {
+    id = n;
+  }
+
+  @Override
+  public String get() {
+    return id;
+  }
+
+  @Override
+  protected void set(String newValue) {
+    id = newValue;
+  }
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
index f5ecd2e..3637914 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
@@ -93,6 +93,7 @@
       code = c;
     }
 
+    @Override
     public char getCode() {
       return code;
     }
@@ -151,6 +152,7 @@
       code = c;
     }
 
+    @Override
     public char getCode() {
       return code;
     }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
index 08c3f52..acf8b45 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.reviewdb.client;
 
+import com.google.gerrit.extensions.client.Comment.Range;
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.StringKey;
 
@@ -214,6 +215,16 @@
     parentUuid = inReplyTo;
   }
 
+  public void setRange(Range r) {
+    if (r != null) {
+      range = new CommentRange(
+          r.startLine, r.startCharacter,
+          r.endLine, r.endCharacter);
+    } else {
+      range = null;
+    }
+  }
+
   public void setRange(CommentRange r) {
     range = r;
   }
@@ -249,6 +260,11 @@
   }
 
   @Override
+  public int hashCode() {
+    return key.hashCode();
+  }
+
+  @Override
   public String toString() {
     StringBuilder builder = new StringBuilder();
     builder.append("PatchLineComment{");
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
index 613978a..48623bd 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
@@ -24,28 +24,8 @@
 /** A single revision of a {@link Change}. */
 public final class PatchSet {
   /** Is the reference name a change reference? */
-  public static boolean isRef(final String name) {
-    if (name == null || !name.startsWith(REFS_CHANGES)) {
-      return false;
-    }
-    boolean accepted = false;
-    int numsFound = 0;
-    for (int i = name.length() - 1; i >= REFS_CHANGES.length() - 1; i--) {
-      char c = name.charAt(i);
-      if (c >= '0' && c <= '9') {
-        accepted = (c != '0');
-      } else if (c == '/') {
-        if (accepted) {
-          if (++numsFound == 2) {
-            return true;
-          }
-          accepted = false;
-        }
-      } else {
-        return false;
-      }
-    }
-    return false;
+  public static boolean isRef(String name) {
+    return Id.fromRef(name) != null;
   }
 
   public static class Id extends IntKey<Change.Id> {
@@ -105,19 +85,43 @@
     }
 
     /** Parse a PatchSet.Id from a {@link PatchSet#getRefName()} result. */
-    public static Id fromRef(String name) {
-      if (!name.startsWith(REFS_CHANGES)) {
-        throw new IllegalArgumentException("Not a PatchSet.Id: " + name);
+    public static Id fromRef(String ref) {
+      int cs = Change.Id.startIndex(ref);
+      if (cs < 0) {
+        return null;
       }
-      final String[] parts = name.substring(REFS_CHANGES.length()).split("/");
-      final int n = parts.length;
-      if (n < 2) {
-        throw new IllegalArgumentException("Not a PatchSet.Id: " + name);
+      int ce = Change.Id.nextNonDigit(ref, cs);
+      int patchSetId = fromRef(ref, ce);
+      if (patchSetId < 0) {
+        return null;
       }
-      final int changeId = Integer.parseInt(parts[n - 2]);
-      final int patchSetId = Integer.parseInt(parts[n - 1]);
+      int changeId = Integer.parseInt(ref.substring(cs, ce));
       return new PatchSet.Id(new Change.Id(changeId), patchSetId);
     }
+
+    static int fromRef(String ref, int changeIdEnd) {
+      // Patch set ID.
+      int ps = changeIdEnd + 1;
+      if (ps >= ref.length() || ref.charAt(ps) == '0') {
+        return -1;
+      }
+      for (int i = ps; i < ref.length(); i++) {
+        if (ref.charAt(i) < '0' || ref.charAt(i) > '9') {
+          return -1;
+        }
+      }
+      return Integer.parseInt(ref.substring(ps));
+    }
+
+    public String getId() {
+      return toId(patchSetId);
+    }
+
+    public static String toId(int number) {
+      return number == 0
+          ? "edit"
+          : String.valueOf(number);
+    }
   }
 
   @Column(id = 1, name = Column.NONE)
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
index 83a9b67..ddfc8c6 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
@@ -16,53 +16,12 @@
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.CompoundKey;
-import com.google.gwtorm.client.StringKey;
 
 import java.sql.Timestamp;
 import java.util.Objects;
 
 /** An approval (or negative approval) on a patch set. */
 public final class PatchSetApproval {
-  public static class LabelId extends
-      StringKey<com.google.gwtorm.client.Key<?>> {
-    private static final long serialVersionUID = 1L;
-
-    public static final LabelId SUBMIT = new LabelId("SUBM");
-
-    @Column(id = 1)
-    protected String id;
-
-    protected LabelId() {
-    }
-
-    public LabelId(final String n) {
-      id = n;
-    }
-
-    @Override
-    public String get() {
-      return id;
-    }
-
-    @Override
-    protected void set(String newValue) {
-      id = newValue;
-    }
-
-    @Override
-    public int hashCode() {
-      return get().hashCode();
-    }
-
-    @Override
-    public boolean equals(Object b) {
-      if (b instanceof LabelId) {
-        return get().equals(((LabelId) b).get());
-      }
-      return false;
-    }
-  }
-
   public static class Key extends CompoundKey<PatchSet.Id> {
     private static final long serialVersionUID = 1L;
 
@@ -130,6 +89,9 @@
   @Column(id = 3)
   protected Timestamp granted;
 
+  // DELETED: id = 4 (changeOpen)
+  // DELETED: id = 5 (changeSortKey)
+
   protected PatchSetApproval() {
   }
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
index 1114813..209998a 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import com.google.gerrit.extensions.api.projects.ProjectState;
-import com.google.gerrit.extensions.common.InheritableBoolean;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.StringKey;
 
@@ -94,6 +94,8 @@
 
   protected String themeName;
 
+  protected InheritableBoolean createNewChangeForAllNotInTarget;
+
   protected Project() {
   }
 
@@ -105,6 +107,7 @@
     useSignedOffBy = InheritableBoolean.INHERIT;
     requireChangeID = InheritableBoolean.INHERIT;
     useContentMerge = InheritableBoolean.INHERIT;
+    createNewChangeForAllNotInTarget = InheritableBoolean.INHERIT;
   }
 
   public Project.NameKey getNameKey() {
@@ -159,6 +162,15 @@
     requireChangeID = cid;
   }
 
+  public InheritableBoolean getCreateNewChangeForAllNotInTarget() {
+    return createNewChangeForAllNotInTarget;
+  }
+
+  public void setCreateNewChangeForAllNotInTarget(
+      InheritableBoolean useAllNotInTarget) {
+    this.createNewChangeForAllNotInTarget = useAllNotInTarget;
+  }
+
   public void setMaxObjectSizeLimit(final String limit) {
     maxObjectSizeLimit = limit;
   }
@@ -212,6 +224,7 @@
     submitType = update.submitType;
     state = update.state;
     maxObjectSizeLimit = update.maxObjectSizeLimit;
+    createNewChangeForAllNotInTarget = update.createNewChangeForAllNotInTarget;
   }
 
   /**
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
index 968cfde..072982a 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -31,6 +31,8 @@
   /** Configurations of project-specific dashboards (canned search queries). */
   public static final String REFS_DASHBOARDS = "refs/meta/dashboards/";
 
+  public static final String REFS_DRAFT_COMMENTS = "refs/draft-comments/";
+
   /**
    * Prefix applied to merge commit base nodes.
    * <p>
@@ -43,6 +45,9 @@
    */
   public static final String REFS_CACHE_AUTOMERGE = "refs/cache-automerge/";
 
+  /** Suffix of a meta ref in the notedb. */
+  public static final String META_SUFFIX = "/meta";
+
   public static String refsUsers(Account.Id accountId) {
     StringBuilder r = new StringBuilder();
     r.append(REFS_USER);
@@ -57,6 +62,52 @@
     return r.toString();
   }
 
+  public static String refsDraftComments(Account.Id accountId,
+      Change.Id changeId) {
+    StringBuilder r = new StringBuilder();
+    r.append(REFS_DRAFT_COMMENTS);
+    int n = accountId.get() % 100;
+    if (n < 10) {
+      r.append('0');
+    }
+    r.append(n);
+    r.append('/');
+    r.append(accountId.get());
+    r.append('-');
+    r.append(changeId.get());
+    return r.toString();
+  }
+
+  /**
+   * Returns reference for this change edit with sharded user and change number:
+   * refs/users/UU/UUUU/edit-CCCC/P.
+   *
+   * @param accountId account id
+   * @param changeId change number
+   * @param psId patch set number
+   * @return reference for this change edit
+   */
+  public static String refsEdit(Account.Id accountId, Change.Id changeId,
+      PatchSet.Id psId) {
+    return refsEditPrefix(accountId, changeId) + psId.get();
+  }
+
+  /**
+   * Returns reference prefix for this change edit with sharded user and
+   * change number: refs/users/UU/UUUU/edit-CCCC/.
+   *
+   * @param accountId account id
+   * @param changeId change number
+   * @return reference prefix for this change edit
+   */
+  public static String refsEditPrefix(Account.Id accountId, Change.Id changeId) {
+    return new StringBuilder(refsUsers(accountId))
+      .append("/edit-")
+      .append(changeId.get())
+      .append("/")
+      .toString();
+  }
+
   private RefNames() {
   }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
index 6603c17..de81a90 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
@@ -24,6 +24,7 @@
 /** Access interface for {@link Account}. */
 public interface AccountAccess extends Access<Account, Account.Id> {
   /** Locate an account by our locally generated identity. */
+  @Override
   @PrimaryKey("accountId")
   Account get(Account.Id key) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java
index fffcf9e..499cb77 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java
@@ -22,6 +22,7 @@
 
 public interface AccountDiffPreferenceAccess extends Access<AccountDiffPreference, Account.Id> {
 
+  @Override
   @PrimaryKey("accountId")
   AccountDiffPreference get(Account.Id key) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
index bf6c0ef..12bd80f 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
@@ -24,6 +24,7 @@
 
 public interface AccountExternalIdAccess extends
     Access<AccountExternalId, AccountExternalId.Key> {
+  @Override
   @PrimaryKey("key")
   AccountExternalId get(AccountExternalId.Key key) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java
index 1de80f3..3b82773 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java
@@ -23,6 +23,7 @@
 
 public interface AccountGroupAccess extends
     Access<AccountGroup, AccountGroup.Id> {
+  @Override
   @PrimaryKey("groupId")
   AccountGroup get(AccountGroup.Id id) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAccess.java
index d1eaed8..4fce0cd 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAccess.java
@@ -24,6 +24,7 @@
 
 public interface AccountGroupByIdAccess extends
     Access<AccountGroupById, AccountGroupById.Key> {
+  @Override
   @PrimaryKey("key")
   AccountGroupById get(AccountGroupById.Key key) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java
index e772c8c..d16d286 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java
@@ -24,6 +24,7 @@
 
 public interface AccountGroupByIdAudAccess extends
     Access<AccountGroupByIdAud, AccountGroupByIdAud.Key> {
+  @Override
   @PrimaryKey("key")
   AccountGroupByIdAud get(AccountGroupByIdAud.Key key)
       throws OrmException;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java
index e070d69..b3296d9 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java
@@ -25,6 +25,7 @@
 
 public interface AccountGroupMemberAccess extends
     Access<AccountGroupMember, AccountGroupMember.Key> {
+  @Override
   @PrimaryKey("key")
   AccountGroupMember get(AccountGroupMember.Key key) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
index 48c4e2d..236d1c1 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
@@ -25,6 +25,7 @@
 
 public interface AccountGroupMemberAuditAccess extends
     Access<AccountGroupMemberAudit, AccountGroupMemberAudit.Key> {
+  @Override
   @PrimaryKey("key")
   AccountGroupMemberAudit get(AccountGroupMemberAudit.Key key)
       throws OrmException;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
index 30e685c..0a06d07 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
@@ -25,6 +25,7 @@
 
 public interface AccountGroupNameAccess extends
     Access<AccountGroupName, AccountGroup.NameKey> {
+  @Override
   @PrimaryKey("name")
   AccountGroupName get(AccountGroup.NameKey name) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountPatchReviewAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountPatchReviewAccess.java
index 80b2dc4..6b9e160 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountPatchReviewAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountPatchReviewAccess.java
@@ -25,6 +25,7 @@
 
 public interface AccountPatchReviewAccess
     extends Access<AccountPatchReview, AccountPatchReview.Key> {
+  @Override
   @PrimaryKey("key")
   AccountPatchReview get(AccountPatchReview.Key id) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java
index c073468..c6f4775 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java
@@ -25,6 +25,7 @@
 
 public interface AccountProjectWatchAccess extends
     Access<AccountProjectWatch, AccountProjectWatch.Key> {
+  @Override
   @PrimaryKey("key")
   AccountProjectWatch get(AccountProjectWatch.Key key) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
index b170a3d..6f71ba4 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
@@ -24,6 +24,7 @@
 
 public interface AccountSshKeyAccess extends
     Access<AccountSshKey, AccountSshKey.Id> {
+  @Override
   @PrimaryKey("id")
   AccountSshKey get(AccountSshKey.Id id) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
index 54cdbfa..4e46efb 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
@@ -14,9 +14,7 @@
 
 package com.google.gerrit.reviewdb.server;
 
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
@@ -24,41 +22,10 @@
 import com.google.gwtorm.server.ResultSet;
 
 public interface ChangeAccess extends Access<Change, Change.Id> {
+  @Override
   @PrimaryKey("changeId")
   Change get(Change.Id id) throws OrmException;
 
-  @Query("WHERE changeKey = ?")
-  ResultSet<Change> byKey(Change.Key key) throws OrmException;
-
-  @Query("WHERE changeKey >= ? AND changeKey <= ?")
-  ResultSet<Change> byKeyRange(Change.Key reva, Change.Key revb)
-      throws OrmException;
-
-  @Query("WHERE dest = ? AND changeKey = ?")
-  ResultSet<Change> byBranchKey(Branch.NameKey p, Change.Key key)
-      throws OrmException;
-
-  @Query("WHERE dest.projectName = ?")
-  ResultSet<Change> byProject(Project.NameKey p) throws OrmException;
-
-  @Query("WHERE dest = ? AND status = '" + Change.STATUS_SUBMITTED
-      + "' ORDER BY lastUpdatedOn")
-  ResultSet<Change> submitted(Branch.NameKey dest) throws OrmException;
-
-  @Query("WHERE status = '" + Change.STATUS_SUBMITTED + "'")
-  ResultSet<Change> allSubmitted() throws OrmException;
-
-  @Query("WHERE open = true AND dest.projectName = ?")
-  ResultSet<Change> byProjectOpenAll(Project.NameKey p) throws OrmException;
-
-  @Query("WHERE open = true AND dest = ?")
-  ResultSet<Change> byBranchOpenAll(Branch.NameKey p) throws OrmException;
-
-  @Query("WHERE open = true AND dest.projectName = ? AND sortKey < ?"
-      + " ORDER BY sortKey DESC LIMIT ?")
-  ResultSet<Change> byProjectOpenNext(Project.NameKey p, String sortKey,
-      int limit) throws OrmException;
-
   @Query
   ResultSet<Change> all() throws OrmException;
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java
index 0126a31..d291fd5 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java
@@ -25,6 +25,7 @@
 
 public interface ChangeMessageAccess extends
     Access<ChangeMessage, ChangeMessage.Key> {
+  @Override
   @PrimaryKey("key")
   ChangeMessage get(ChangeMessage.Key id) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java
index a5842de..81f3e57 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java
@@ -26,6 +26,7 @@
 
 public interface PatchLineCommentAccess extends
     Access<PatchLineComment, PatchLineComment.Key> {
+  @Override
   @PrimaryKey("key")
   PatchLineComment get(PatchLineComment.Key id) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
index 703edbb..36e969e 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
@@ -24,6 +24,7 @@
 import com.google.gwtorm.server.ResultSet;
 
 public interface PatchSetAccess extends Access<PatchSet, PatchSet.Id> {
+  @Override
   @PrimaryKey("id")
   PatchSet get(PatchSet.Id id) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
index ac2c849..02459d9 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.reviewdb.server;
 
-import com.google.gerrit.reviewdb.client.Change.Id;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetAncestor;
 import com.google.gerrit.reviewdb.client.RevId;
@@ -26,6 +26,7 @@
 
 public interface PatchSetAncestorAccess extends
     Access<PatchSetAncestor, PatchSetAncestor.Id> {
+  @Override
   @PrimaryKey("key")
   PatchSetAncestor get(PatchSetAncestor.Id key) throws OrmException;
 
@@ -33,7 +34,7 @@
   ResultSet<PatchSetAncestor> ancestorsOf(PatchSet.Id id) throws OrmException;
 
   @Query("WHERE key.patchSetId.changeId = ?")
-  ResultSet<PatchSetAncestor> byChange(Id id) throws OrmException;
+  ResultSet<PatchSetAncestor> byChange(Change.Id id) throws OrmException;
 
   @Query("WHERE key.patchSetId = ?")
   ResultSet<PatchSetAncestor> byPatchSet(PatchSet.Id id) throws OrmException;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
index e90e05a..cddee73 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
@@ -26,6 +26,7 @@
 
 public interface PatchSetApprovalAccess extends
     Access<PatchSetApproval, PatchSetApproval.Key> {
+  @Override
   @PrimaryKey("key")
   PatchSetApproval get(PatchSetApproval.Key key) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java
index 470f8c6..dda8d5c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java
@@ -22,6 +22,7 @@
 /** Access interface for {@link CurrentSchemaVersion}. */
 public interface SchemaVersionAccess extends
     Access<CurrentSchemaVersion, CurrentSchemaVersion.Key> {
+  @Override
   @PrimaryKey("singleton")
   CurrentSchemaVersion get(CurrentSchemaVersion.Key key) throws OrmException;
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java
index 4010dae..5f57fe7 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java
@@ -25,6 +25,7 @@
 
 public interface StarredChangeAccess extends
     Access<StarredChange, StarredChange.Key> {
+  @Override
   @PrimaryKey("key")
   StarredChange get(StarredChange.Key key) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
index 8c67009..b25e406 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
@@ -25,6 +25,7 @@
 
 public interface SubmoduleSubscriptionAccess extends
     Access<SubmoduleSubscription, SubmoduleSubscription.Key> {
+  @Override
   @PrimaryKey("key")
   SubmoduleSubscription get(SubmoduleSubscription.Key key) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java
index 4b2ed74..3bc49dd 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java
@@ -24,6 +24,7 @@
 /** Access interface for {@link SystemConfig}. */
 public interface SystemConfigAccess extends
     Access<SystemConfig, SystemConfig.Key> {
+  @Override
   @PrimaryKey("singleton")
   SystemConfig get(SystemConfig.Key key) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
index 0b7f2c1..a62c762 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -68,24 +68,6 @@
 
 
 -- *********************************************************************
--- ChangeAccess
---    covers:             submitted, allSubmitted
-CREATE INDEX changes_submitted
-ON changes (status, dest_project_name, dest_branch_name, last_updated_on);
-
---    covers:             byProjectOpenAll
-CREATE INDEX changes_byProjectOpen
-ON changes (open, dest_project_name, sort_key);
-
---    covers:             byProject
-CREATE INDEX changes_byProject
-ON changes (dest_project_name);
-
-CREATE INDEX changes_key
-ON changes (change_key);
-
-
--- *********************************************************************
 -- ChangeMessageAccess
 --    @PrimaryKey covers: byChange
 
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
index 5faa71b..f88c169 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
@@ -75,28 +75,6 @@
 
 
 -- *********************************************************************
--- ChangeAccess
---    covers:             submitted, allSubmitted
-CREATE INDEX changes_submitted
-ON changes (status, dest_project_name, dest_branch_name, last_updated_on)
-#
-
---    covers:             byProjectOpenPrev, byProjectOpenNext
-CREATE INDEX changes_byProjectOpen
-ON changes (open, dest_project_name, sort_key)
-#
-
---    covers:             byProject
-CREATE INDEX changes_byProject
-ON changes (dest_project_name)
-#
-
-CREATE INDEX changes_key
-ON changes (change_key)
-#
-
-
--- *********************************************************************
 -- ChangeMessageAccess
 --    @PrimaryKey covers: byChange
 
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index 25e3fae..a6b21ee 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -116,26 +116,6 @@
 
 
 -- *********************************************************************
--- ChangeAccess
---    covers:             submitted, allSubmitted
-CREATE INDEX changes_submitted
-ON changes (dest_project_name, dest_branch_name, last_updated_on)
-WHERE status = 's';
-
---    covers:             byProjectOpenAll
-CREATE INDEX changes_byProjectOpen
-ON changes (dest_project_name, sort_key)
-WHERE open = 'Y';
-
---    covers:             byProject
-CREATE INDEX changes_byProject
-ON changes (dest_project_name);
-
-CREATE INDEX changes_key
-ON changes (change_key);
-
-
--- *********************************************************************
 -- ChangeMessageAccess
 --    @PrimaryKey covers: byChange
 
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/AccountTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/AccountTest.java
new file mode 100644
index 0000000..e8dc5e0
--- /dev/null
+++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/AccountTest.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Test;
+
+public class AccountTest {
+  @Test
+  public void parseRefName() {
+    assertRef(1, "refs/users/01/1");
+    assertRef(1, "refs/users/01/1-drafts");
+    assertRef(1, "refs/users/01/1-drafts/2");
+    assertRef(1, "refs/users/01/1/edit/2");
+
+    assertNotRef(null);
+    assertNotRef("");
+
+    // Invalid characters.
+    assertNotRef("refs/users/01a/1");
+    assertNotRef("refs/users/01/a1");
+
+    // Mismatched shard.
+    assertNotRef("refs/users/01/23");
+
+    // Shard too short.
+    assertNotRef("refs/users/1/1");
+  }
+
+  @Test
+  public void parseRefNameParts() {
+    assertRefPart(1, "01/1");
+    assertRefPart(1, "01/1-drafts");
+    assertRefPart(1, "01/1-drafts/2");
+
+    assertNotRefPart(null);
+    assertNotRefPart("");
+
+    // This method assumes that the common prefix "refs/users/" will be removed.
+    assertNotRefPart("refs/users/01/1");
+
+    // Invalid characters.
+    assertNotRefPart("01a/1");
+    assertNotRefPart("01/a1");
+
+    // Mismatched shard.
+    assertNotRefPart("01/23");
+
+    // Shard too short.
+    assertNotRefPart("1/1");
+  }
+
+  private static void assertRef(int accountId, String refName) {
+    assertEquals(new Account.Id(accountId), Account.Id.fromRef(refName));
+  }
+
+  private static void assertNotRef(String refName) {
+    assertNull(Account.Id.fromRef(refName));
+  }
+
+  private static void assertRefPart(int accountId, String refName) {
+    assertEquals(new Account.Id(accountId), Account.Id.fromRefPart(refName));
+  }
+
+  private static void assertNotRefPart(String refName) {
+    assertNull(Account.Id.fromRefPart(refName));
+  }
+}
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java
new file mode 100644
index 0000000..218d04f
--- /dev/null
+++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/ChangeTest.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Test;
+
+public class ChangeTest {
+  @Test
+  public void parseInvalidRefNames() {
+    assertNotRef(null);
+    assertNotRef("");
+    assertNotRef("01/1/1");
+    assertNotRef("HEAD");
+    assertNotRef("refs/tags/v1");
+  }
+
+  @Test
+  public void parsePatchSetRefNames() {
+    assertRef(1, "refs/changes/01/1/1");
+    assertRef(1234, "refs/changes/34/1234/56");
+
+    // Invalid characters.
+    assertNotRef("refs/changes/0x/1/1");
+    assertNotRef("refs/changes/01/x/1");
+    assertNotRef("refs/changes/01/1/x");
+
+    // Truncations.
+    assertNotRef("refs/changes/");
+    assertNotRef("refs/changes/1");
+    assertNotRef("refs/changes/01");
+    assertNotRef("refs/changes/01/");
+    assertNotRef("refs/changes/01/1/");
+    assertNotRef("refs/changes/01/1/1/");
+    assertNotRef("refs/changes/01//1/1");
+
+    // Leading zeroes.
+    assertNotRef("refs/changes/01/01/1");
+    assertNotRef("refs/changes/01/1/01");
+
+    // Mismatched last 2 digits.
+    assertNotRef("refs/changes/35/1234/56");
+
+    // Something other than patch set after change.
+    assertNotRef("refs/changes/34/1234/0");
+    assertNotRef("refs/changes/34/1234/foo");
+    assertNotRef("refs/changes/34/1234|56");
+    assertNotRef("refs/changes/34/1234foo");
+  }
+
+  @Test
+  public void parseChangeMetaRefNames() {
+    assertRef(1, "refs/changes/01/1/meta");
+    assertRef(1234, "refs/changes/34/1234/meta");
+
+    assertNotRef("refs/changes/01/1/met");
+    assertNotRef("refs/changes/01/1/META");
+    assertNotRef("refs/changes/01/1/1/meta");
+  }
+
+  private static void assertRef(int changeId, String refName) {
+    assertEquals(new Change.Id(changeId), Change.Id.fromRef(refName));
+  }
+
+  private static void assertNotRef(String refName) {
+    assertNull(Change.Id.fromRef(refName));
+  }
+}
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetApprovalTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetApprovalTest.java
new file mode 100644
index 0000000..eba08c8
--- /dev/null
+++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetApprovalTest.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.client;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.server.StandardKeyEncoder;
+
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class PatchSetApprovalTest {
+  static {
+    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
+  }
+
+  @Test
+  public void keyEquality() {
+    PatchSetApproval.Key k1 = new PatchSetApproval.Key(
+        new PatchSet.Id(new Change.Id(1), 2),
+        new Account.Id(3),
+        new LabelId("My-Label"));
+    PatchSetApproval.Key k2 = new PatchSetApproval.Key(
+        new PatchSet.Id(new Change.Id(1), 2),
+        new Account.Id(3),
+        new LabelId("My-Label"));
+    PatchSetApproval.Key k3 = new PatchSetApproval.Key(
+        new PatchSet.Id(new Change.Id(1), 2),
+        new Account.Id(3),
+        new LabelId("Other-Label"));
+
+    assertThat(k2).isEqualTo(k1);
+    assertThat(k3).isNotEqualTo(k1);
+    assertThat(k2.hashCode()).isEqualTo(k1.hashCode());
+    assertThat(k3.hashCode()).isNotEqualTo(k1.hashCode());
+
+    Map<PatchSetApproval.Key, String> map = new HashMap<>();
+    map.put(k1, "k1");
+    map.put(k2, "k2");
+    map.put(k3, "k3");
+    assertThat(map).containsKey(k1);
+    assertThat(map).containsKey(k2);
+    assertThat(map).containsKey(k3);
+    assertThat(map).containsEntry(k1, "k2");
+    assertThat(map).containsEntry(k2, "k2");
+    assertThat(map).containsEntry(k3, "k3");
+  }
+}
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java
new file mode 100644
index 0000000..33da24a
--- /dev/null
+++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class PatchSetTest {
+  @Test
+  public void parseRefNames() {
+    assertRef(1, 1, "refs/changes/01/1/1");
+    assertRef(1234, 56, "refs/changes/34/1234/56");
+
+    // Not even close.
+    assertNotRef(null);
+    assertNotRef("");
+    assertNotRef("01/1/1");
+    assertNotRef("HEAD");
+    assertNotRef("refs/tags/v1");
+
+    // Invalid characters.
+    assertNotRef("refs/changes/0x/1/1");
+    assertNotRef("refs/changes/01/x/1");
+    assertNotRef("refs/changes/01/1/x");
+
+    // Truncations.
+    assertNotRef("refs/changes/");
+    assertNotRef("refs/changes/1");
+    assertNotRef("refs/changes/01");
+    assertNotRef("refs/changes/01/");
+    assertNotRef("refs/changes/01/1/");
+    assertNotRef("refs/changes/01/1/1/");
+    assertNotRef("refs/changes/01//1/1");
+
+    // Leading zeroes.
+    assertNotRef("refs/changes/01/01/1");
+    assertNotRef("refs/changes/01/1/01");
+
+    // Mismatched last 2 digits.
+    assertNotRef("refs/changes/35/1234/56");
+
+    // Something other than patch set after change.
+    assertNotRef("refs/changes/34/1234/0");
+    assertNotRef("refs/changes/34/1234/foo");
+    assertNotRef("refs/changes/34/1234|56");
+    assertNotRef("refs/changes/34/1234foo");
+  }
+
+  private static void assertRef(int changeId, int psId, String refName) {
+    assertTrue(PatchSet.isRef(refName));
+    assertEquals(new PatchSet.Id(new Change.Id(changeId), psId),
+        PatchSet.Id.fromRef(refName));
+  }
+
+  private static void assertNotRef(String refName) {
+    assertFalse(PatchSet.isRef(refName));
+    assertNull(PatchSet.Id.fromRef(refName));
+  }
+}
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index 7384cfd..7202dc3 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -41,14 +41,12 @@
     '//lib:jsch',
     '//lib:juniversalchardet',
     '//lib:mime-util',
-    '//lib/ow2:ow2-asm',
-    '//lib/ow2:ow2-asm-tree',
-    '//lib/ow2:ow2-asm-util',
     '//lib:parboiled-core',
     '//lib:pegdown',
     '//lib:protobuf',
     '//lib:velocity',
     '//lib/antlr:java_runtime',
+    '//lib/auto:auto-value',
     '//lib/commons:codec',
     '//lib/commons:dbcp',
     '//lib/commons:lang',
@@ -62,10 +60,13 @@
     '//lib/joda:joda-time',
     '//lib/log:api',
     '//lib/log:log4j',
-    '//lib/prolog:prolog-cafe',
     '//lib/lucene:analyzers-common',
     '//lib/lucene:core',
     '//lib/lucene:query-parser',
+    '//lib/ow2:ow2-asm',
+    '//lib/ow2:ow2-asm-tree',
+    '//lib/ow2:ow2-asm-util',
+    '//lib/prolog:prolog-cafe',
   ],
   provided_deps = [
     '//lib:servlet-api-3_1',
@@ -130,6 +131,7 @@
   srcs = PROLOG_TEST_CASE,
   deps = [
     ':server',
+    '//gerrit-common:server',
     '//lib:junit',
     '//lib/guice:guice',
     '//lib/prolog:prolog-cafe',
@@ -147,8 +149,10 @@
     '//gerrit-common:server',
     '//gerrit-reviewdb:server',
     '//gerrit-server/src/main/prolog:common',
+    '//lib:guava',
     '//lib:gwtorm',
     '//lib:junit',
+    '//lib:truth',
     '//lib/jgit:jgit',
     '//lib/guice:guice',
     '//lib/prolog:prolog-cafe',
@@ -175,6 +179,7 @@
     '//lib:guava',
     '//lib:gwtorm',
     '//lib:junit',
+    '//lib:truth',
     '//lib/antlr:java_runtime',
     '//lib/guice:guice',
     '//lib/jgit:jgit',
@@ -186,6 +191,7 @@
 
 java_test(
   name = 'server_tests',
+  labels = ['server'],
   srcs = glob(
     ['src/test/java/**/*.java'],
     excludes = TESTUTIL + PROLOG_TESTS + PROLOG_TEST_CASE + QUERY_TESTS
@@ -203,11 +209,14 @@
     '//lib:guava',
     '//lib:gwtorm',
     '//lib:junit',
+    '//lib:truth',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/jgit:jgit',
     '//lib/jgit:junit',
     '//lib/joda:joda-time',
+    '//lib:parboiled-core',
+    '//lib:parboiled-java',
     '//lib/prolog:prolog-cafe',
   ],
   source_under_test = [':server'],
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java
index cdb24e7..e25b7cb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java
@@ -14,12 +14,13 @@
 
 package com.google.gerrit.audit;
 
-import com.google.common.base.Objects;
+import com.google.auto.value.AutoValue;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Multimap;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.util.TimeUtil;
 
 public class AuditEvent {
 
@@ -36,41 +37,14 @@
   public final long elapsed;
   public final UUID uuid;
 
-  public static class UUID {
-
-    protected final String uuid;
-
-    protected UUID() {
-      uuid = String.format("audit:%s", java.util.UUID.randomUUID().toString());
+  @AutoValue
+  public abstract static class UUID {
+    private static UUID create() {
+      return new AutoValue_AuditEvent_UUID(
+          String.format("audit:%s", java.util.UUID.randomUUID().toString()));
     }
 
-    public UUID(final String n) {
-      uuid = n;
-    }
-
-    public String get() {
-      return uuid;
-    }
-
-    @Override
-    public int hashCode() {
-      return uuid.hashCode();
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-      if (this == obj) {
-        return true;
-      }
-      if (obj == null) {
-        return false;
-      }
-      if (!(obj instanceof UUID)) {
-        return false;
-      }
-
-      return uuid.equals(((UUID) obj).uuid);
-    }
+    public abstract String uuid();
   }
 
   /**
@@ -87,13 +61,13 @@
       Multimap<String, ?> params, Object result) {
     Preconditions.checkNotNull(what, "what is a mandatory not null param !");
 
-    this.sessionId = Objects.firstNonNull(sessionId, UNKNOWN_SESSION_ID);
+    this.sessionId = MoreObjects.firstNonNull(sessionId, UNKNOWN_SESSION_ID);
     this.who = who;
     this.what = what;
     this.when = when;
     this.timeAtStart = this.when;
-    this.params = Objects.firstNonNull(params, EMPTY_PARAMS);
-    this.uuid = new UUID();
+    this.params = MoreObjects.firstNonNull(params, EMPTY_PARAMS);
+    this.uuid = UUID.create();
     this.result = result;
     this.elapsed = TimeUtil.nowMs() - timeAtStart;
   }
@@ -116,6 +90,6 @@
   @Override
   public String toString() {
     return String.format("AuditEvent UUID:%s, SID:%s, TS:%d, who:%s, what:%s",
-        uuid.get(), sessionId, when, who, what);
+        uuid.uuid(), sessionId, when, who, what);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditModule.java b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditModule.java
index dc870ac..89b51f8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditModule.java
@@ -22,6 +22,7 @@
   @Override
   protected void configure() {
     DynamicSet.setOf(binder(), AuditListener.class);
+    DynamicSet.setOf(binder(), GroupMemberAuditListener.class);
     bind(AuditService.class);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditService.java b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditService.java
index a992aa1..4844045 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditService.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditService.java
@@ -15,16 +15,29 @@
 package com.google.gerrit.audit;
 
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+
 @Singleton
 public class AuditService {
+  private static final Logger log = LoggerFactory.getLogger(AuditService.class);
+
   private final DynamicSet<AuditListener> auditListeners;
+  private final DynamicSet<GroupMemberAuditListener> groupMemberAuditListeners;
 
   @Inject
-  public AuditService(DynamicSet<AuditListener> auditListeners) {
+  public AuditService(DynamicSet<AuditListener> auditListeners,
+      DynamicSet<GroupMemberAuditListener> groupMemberAuditListeners) {
     this.auditListeners = auditListeners;
+    this.groupMemberAuditListeners = groupMemberAuditListeners;
   }
 
   public void dispatch(AuditEvent action) {
@@ -32,4 +45,48 @@
       auditListener.onAuditableAction(action);
     }
   }
+
+  public void dispatchAddAccountsToGroup(Account.Id actor,
+      Collection<AccountGroupMember> added) {
+    for (GroupMemberAuditListener auditListener : groupMemberAuditListeners) {
+      try {
+        auditListener.onAddAccountsToGroup(actor, added);
+      } catch (RuntimeException e) {
+        log.error("failed to log add accounts to group event", e);
+      }
+    }
+  }
+
+  public void dispatchDeleteAccountsFromGroup(Account.Id actor,
+      Collection<AccountGroupMember> removed) {
+    for (GroupMemberAuditListener auditListener : groupMemberAuditListeners) {
+      try {
+        auditListener.onDeleteAccountsFromGroup(actor, removed);
+      } catch (RuntimeException e) {
+        log.error("failed to log delete accounts from group event", e);
+      }
+    }
+  }
+
+  public void dispatchAddGroupsToGroup(Account.Id actor,
+      Collection<AccountGroupById> added) {
+    for (GroupMemberAuditListener auditListener : groupMemberAuditListeners) {
+      try {
+        auditListener.onAddGroupsToGroup(actor, added);
+      } catch (RuntimeException e) {
+        log.error("failed to log add groups to group event", e);
+      }
+    }
+  }
+
+  public void dispatchDeleteGroupsFromGroup(Account.Id actor,
+      Collection<AccountGroupById> removed) {
+    for (GroupMemberAuditListener auditListener : groupMemberAuditListeners) {
+      try {
+        auditListener.onDeleteGroupsFromGroup(actor, removed);
+      } catch (RuntimeException e) {
+        log.error("failed to log delete groups from group event", e);
+      }
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/GroupMemberAuditListener.java b/gerrit-server/src/main/java/com/google/gerrit/audit/GroupMemberAuditListener.java
new file mode 100644
index 0000000..1269f4a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/GroupMemberAuditListener.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.audit;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+
+import java.util.Collection;
+
+@ExtensionPoint
+public interface GroupMemberAuditListener {
+
+  void onAddAccountsToGroup(Account.Id actor,
+      Collection<AccountGroupMember> added);
+
+  void onDeleteAccountsFromGroup(Account.Id actor,
+      Collection<AccountGroupMember> removed);
+
+  void onAddGroupsToGroup(Account.Id actor, Collection<AccountGroupById> added);
+
+  void onDeleteGroupsFromGroup(Account.Id actor,
+      Collection<AccountGroupById> deleted);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
index b8d45f6..8bd082d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.common;
 
+import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.LabelType;
@@ -27,7 +29,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.AnonymousCowardName;
@@ -35,12 +37,13 @@
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.data.ApprovalAttribute;
 import com.google.gerrit.server.events.ChangeAbandonedEvent;
-import com.google.gerrit.server.events.ChangeEvent;
 import com.google.gerrit.server.events.ChangeMergedEvent;
 import com.google.gerrit.server.events.ChangeRestoredEvent;
 import com.google.gerrit.server.events.CommentAddedEvent;
 import com.google.gerrit.server.events.DraftPublishedEvent;
+import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.EventFactory;
+import com.google.gerrit.server.events.HashtagsChangedEvent;
 import com.google.gerrit.server.events.MergeFailedEvent;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
@@ -73,6 +76,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
@@ -83,7 +87,8 @@
 
 /** Spawns local executables when a hook action occurs. */
 @Singleton
-public class ChangeHookRunner implements ChangeHooks, LifecycleListener {
+public class ChangeHookRunner implements ChangeHooks, EventDispatcher,
+  EventSource, LifecycleListener {
     /** A logger for this class. */
     private static final Logger log = LoggerFactory.getLogger(ChangeHookRunner.class);
 
@@ -92,15 +97,17 @@
       protected void configure() {
         bind(ChangeHookRunner.class);
         bind(ChangeHooks.class).to(ChangeHookRunner.class);
+        bind(EventDispatcher.class).to(ChangeHookRunner.class);
+        bind(EventSource.class).to(ChangeHookRunner.class);
         listener().to(ChangeHookRunner.class);
       }
     }
 
-    private static class ChangeListenerHolder {
-        final ChangeListener listener;
-        final IdentifiedUser user;
+    private static class EventListenerHolder {
+        final EventListener listener;
+        final CurrentUser user;
 
-        ChangeListenerHolder(ChangeListener l, IdentifiedUser u) {
+        EventListenerHolder(EventListener l, CurrentUser u) {
             listener = l;
             user = u;
         }
@@ -134,6 +141,7 @@
         return output;
       }
 
+      @Override
       public String toString() {
         StringBuilder sb = new StringBuilder();
 
@@ -155,11 +163,11 @@
 
     /** Listeners to receive changes as they happen (limited by visibility
      *  of holder's user). */
-    private final Map<ChangeListener, ChangeListenerHolder> listeners =
+    private final Map<EventListener, EventListenerHolder> listeners =
         new ConcurrentHashMap<>();
 
     /** Listeners to receive all changes as they happen. */
-    private final DynamicSet<ChangeListener> unrestrictedListeners;
+    private final DynamicSet<EventListener> unrestrictedListeners;
 
     /** Filename of the new patchset hook. */
     private final File patchsetCreatedHook;
@@ -197,6 +205,9 @@
     /** Filename of the update hook. */
     private final File refUpdateHook;
 
+    /** Filename of the hashtags changed hook */
+    private final File hashtagsChangedHook;
+
     private final String anonymousCowardName;
 
     /** Repository Manager. */
@@ -237,8 +248,7 @@
       final ProjectCache projectCache,
       final AccountCache accountCache,
       final EventFactory eventFactory,
-      final SitePaths sitePaths,
-      final DynamicSet<ChangeListener> unrestrictedListeners) {
+      final DynamicSet<EventListener> unrestrictedListeners) {
         this.anonymousCowardName = anonymousCowardName;
         this.repoManager = repoManager;
         this.hookQueue = queue.createQueue(1, "hook");
@@ -254,7 +264,7 @@
         draftPublishedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "draftPublishedHook", "draft-published")).getPath());
         commentAddedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "commentAddedHook", "comment-added")).getPath());
         changeMergedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeMergedHook", "change-merged")).getPath());
-        mergeFailedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "mergeFailed", "merge-failed")).getPath());
+        mergeFailedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "mergeFailedHook", "merge-failed")).getPath());
         changeAbandonedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeAbandonedHook", "change-abandoned")).getPath());
         changeRestoredHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeRestoredHook", "change-restored")).getPath());
         refUpdatedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "refUpdatedHook", "ref-updated")).getPath());
@@ -262,6 +272,7 @@
         topicChangedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "topicChangedHook", "topic-changed")).getPath());
         claSignedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "claSignedHook", "cla-signed")).getPath());
         refUpdateHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "refUpdateHook", "ref-update")).getPath());
+        hashtagsChangedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "hashtagsChangedHook", "hashtags-changed")).getPath());
         syncHookTimeout = config.getInt("hooks", "syncHookTimeout", 30);
         syncHookThreadPool = Executors.newCachedThreadPool(
             new ThreadFactoryBuilder()
@@ -269,11 +280,13 @@
               .build());
     }
 
-    public void addChangeListener(ChangeListener listener, IdentifiedUser user) {
-        listeners.put(listener, new ChangeListenerHolder(listener, user));
+    @Override
+    public void addEventListener(EventListener listener, CurrentUser user) {
+        listeners.put(listener, new EventListenerHolder(listener, user));
     }
 
-    public void removeChangeListener(ChangeListener listener) {
+    @Override
+    public void removeEventListener(EventListener listener) {
         listeners.remove(listener);
     }
 
@@ -288,7 +301,7 @@
      */
     private String getValue(final Config config, final String section, final String setting, final String fallback) {
         final String result = config.getString(section, null, setting);
-        return (result == null) ? fallback : result;
+        return Strings.isNullOrEmpty(result) ? fallback : result;
     }
 
     /**
@@ -317,6 +330,7 @@
      * Fire the update hook
      *
      */
+    @Override
     public HookResult doRefUpdateHook(final Project project, final String refname,
         final Account uploader, final ObjectId oldId, final ObjectId newId) {
 
@@ -327,15 +341,7 @@
       addArg(args, "--oldrev", oldId.getName());
       addArg(args, "--newrev", newId.getName());
 
-      HookResult hookResult;
-
-      try {
-        hookResult = runSyncHook(project.getNameKey(), refUpdateHook, args);
-      } catch (TimeoutException e) {
-        hookResult = new HookResult(-1, "Synchronous hook timed out");
-      }
-
-      return hookResult;
+      return runSyncHook(project.getNameKey(), refUpdateHook, args);
     }
 
     /**
@@ -345,6 +351,7 @@
      * @param patchSet The Patchset that was created.
      * @throws OrmException
      */
+    @Override
     public void doPatchsetCreatedHook(final Change change, final PatchSet patchSet,
           final ReviewDb db) throws OrmException {
         final PatchSetCreatedEvent event = new PatchSetCreatedEvent();
@@ -372,6 +379,7 @@
         runHook(change.getProject(), patchsetCreatedHook, args);
     }
 
+    @Override
     public void doDraftPublishedHook(final Change change, final PatchSet patchSet,
           final ReviewDb db) throws OrmException {
         final DraftPublishedEvent event = new DraftPublishedEvent();
@@ -397,6 +405,7 @@
         runHook(change.getProject(), draftPublishedHook, args);
     }
 
+    @Override
     public void doCommentAddedHook(final Change change, final Account account,
           final PatchSet patchSet, final String comment, final Map<String, Short> approvals,
           final ReviewDb db) throws OrmException {
@@ -440,14 +449,17 @@
         runHook(change.getProject(), commentAddedHook, args);
     }
 
-    public void doChangeMergedHook(final Change change, final Account account,
-          final PatchSet patchSet, final ReviewDb db) throws OrmException {
+    @Override
+  public void doChangeMergedHook(final Change change, final Account account,
+      final PatchSet patchSet, final ReviewDb db, String mergeResultRev)
+      throws OrmException {
         final ChangeMergedEvent event = new ChangeMergedEvent();
         final AccountState owner = accountCache.get(change.getOwner());
 
         event.change = eventFactory.asChangeAttribute(change);
         event.submitter = eventFactory.asAccountAttribute(account);
         event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+        event.newRev = mergeResultRev;
         fireEvent(change, event, db);
 
         final List<String> args = new ArrayList<>();
@@ -459,10 +471,12 @@
         addArg(args, "--topic", event.change.topic);
         addArg(args, "--submitter", getDisplayName(account));
         addArg(args, "--commit", event.patchSet.revision);
+        addArg(args, "--newrev", mergeResultRev);
 
         runHook(change.getProject(), changeMergedHook, args);
     }
 
+    @Override
     public void doMergeFailedHook(final Change change, final Account account,
           final PatchSet patchSet, final String reason,
           final ReviewDb db) throws OrmException {
@@ -489,6 +503,7 @@
         runHook(change.getProject(), mergeFailedHook, args);
     }
 
+    @Override
     public void doChangeAbandonedHook(final Change change, final Account account,
           final PatchSet patchSet, final String reason, final ReviewDb db)
           throws OrmException {
@@ -515,6 +530,7 @@
         runHook(change.getProject(), changeAbandonedHook, args);
     }
 
+    @Override
     public void doChangeRestoredHook(final Change change, final Account account,
           final PatchSet patchSet, final String reason, final ReviewDb db)
           throws OrmException {
@@ -541,10 +557,12 @@
         runHook(change.getProject(), changeRestoredHook, args);
     }
 
+    @Override
     public void doRefUpdatedHook(final Branch.NameKey refName, final RefUpdate refUpdate, final Account account) {
       doRefUpdatedHook(refName, refUpdate.getOldObjectId(), refUpdate.getNewObjectId(), account);
     }
 
+    @Override
     public void doRefUpdatedHook(final Branch.NameKey refName, final ObjectId oldId, final ObjectId newId, final Account account) {
       final RefUpdatedEvent event = new RefUpdatedEvent();
 
@@ -566,6 +584,7 @@
       runHook(refName.getParentKey(), refUpdatedHook, args);
     }
 
+    @Override
     public void doReviewerAddedHook(final Change change, final Account account,
         final PatchSet patchSet, final ReviewDb db) throws OrmException {
       final ReviewerAddedEvent event = new ReviewerAddedEvent();
@@ -587,6 +606,7 @@
       runHook(change.getProject(), reviewerAddedHook, args);
     }
 
+    @Override
     public void doTopicChangedHook(final Change change, final Account account,
         final String oldTopic, final ReviewDb db)
             throws OrmException {
@@ -610,6 +630,54 @@
       runHook(change.getProject(), topicChangedHook, args);
     }
 
+    String[] hashtagArray(Set<String> hashtags) {
+      if (hashtags != null && hashtags.size() > 0) {
+        return Sets.newHashSet(hashtags).toArray(
+            new String[hashtags.size()]);
+      }
+      return null;
+    }
+
+    @Override
+    public void doHashtagsChangedHook(Change change, Account account,
+        Set<String> added, Set<String> removed, Set<String> hashtags, ReviewDb db)
+            throws OrmException {
+      HashtagsChangedEvent event = new HashtagsChangedEvent();
+      AccountState owner = accountCache.get(change.getOwner());
+
+      event.change = eventFactory.asChangeAttribute(change);
+      event.editor = eventFactory.asAccountAttribute(account);
+      event.hashtags = hashtagArray(hashtags);
+      event.added = hashtagArray(added);
+      event.removed = hashtagArray(removed);
+
+      fireEvent(change, event, db);
+
+      final List<String> args = new ArrayList<>();
+      addArg(args, "--change", event.change.id);
+      addArg(args, "--change-owner", getDisplayName(owner.getAccount()));
+      addArg(args, "--project", event.change.project);
+      addArg(args, "--branch", event.change.branch);
+      addArg(args, "--editor", getDisplayName(account));
+      if (hashtags != null) {
+        for (String hashtag : hashtags) {
+          addArg(args, "--hashtag", hashtag);
+        }
+      }
+      if (added != null) {
+        for (String hashtag : added) {
+          addArg(args, "--added", hashtag);
+        }
+      }
+      if (removed != null) {
+        for (String hashtag : removed) {
+          addArg(args, "--removed", hashtag);
+        }
+      }
+      runHook(change.getProject(), hashtagsChangedHook, args);
+    }
+
+    @Override
     public void doClaSignupHook(Account account, ContributorAgreement cla) {
       if (account != null) {
         final List<String> args = new ArrayList<>();
@@ -622,44 +690,45 @@
     }
 
     @Override
-    public void postEvent(final Change change, final ChangeEvent event,
+    public void postEvent(final Change change, final Event event,
         final ReviewDb db) throws OrmException {
       fireEvent(change, event, db);
     }
 
     @Override
     public void postEvent(final Branch.NameKey branchName,
-        final ChangeEvent event) {
+        final Event event) {
       fireEvent(branchName, event);
     }
 
-    private void fireEventForUnrestrictedListeners(final ChangeEvent event) {
-      for (ChangeListener listener : unrestrictedListeners) {
-          listener.onChangeEvent(event);
+    private void fireEventForUnrestrictedListeners(final Event event) {
+      for (EventListener listener : unrestrictedListeners) {
+          listener.onEvent(event);
       }
     }
 
-    private void fireEvent(final Change change, final ChangeEvent event, final ReviewDb db) throws OrmException {
-      for (ChangeListenerHolder holder : listeners.values()) {
+    private void fireEvent(final Change change, final Event event,
+        final ReviewDb db) throws OrmException {
+      for (EventListenerHolder holder : listeners.values()) {
           if (isVisibleTo(change, holder.user, db)) {
-              holder.listener.onChangeEvent(event);
+              holder.listener.onEvent(event);
           }
       }
 
       fireEventForUnrestrictedListeners( event );
     }
 
-    private void fireEvent(Branch.NameKey branchName, final ChangeEvent event) {
-      for (ChangeListenerHolder holder : listeners.values()) {
+    private void fireEvent(Branch.NameKey branchName, final Event event) {
+      for (EventListenerHolder holder : listeners.values()) {
           if (isVisibleTo(branchName, holder.user)) {
-              holder.listener.onChangeEvent(event);
+              holder.listener.onEvent(event);
           }
       }
 
       fireEventForUnrestrictedListeners( event );
     }
 
-    private boolean isVisibleTo(Change change, IdentifiedUser user, ReviewDb db) throws OrmException {
+    private boolean isVisibleTo(Change change, CurrentUser user, ReviewDb db) throws OrmException {
         final ProjectState pe = projectCache.get(change.getProject());
         if (pe == null) {
           return false;
@@ -668,7 +737,7 @@
         return pc.controlFor(change).isVisible(db);
     }
 
-    private boolean isVisibleTo(Branch.NameKey branchName, IdentifiedUser user) {
+    private boolean isVisibleTo(Branch.NameKey branchName, CurrentUser user) {
         final ProjectState pe = projectCache.get(branchName.getParentKey());
         if (pe == null) {
           return false;
@@ -733,7 +802,7 @@
   }
 
   private HookResult runSyncHook(Project.NameKey project,
-      File hook, List<String> args) throws TimeoutException {
+      File hook, List<String> args) {
 
     if (!hook.exists()) {
       return null;
@@ -859,8 +928,7 @@
           while ((line = br.readLine()) != null) {
             log.info("hook[" + getName() + "] output: " + line);
           }
-        }
-        catch(IOException  iox) {
+        } catch (IOException iox) {
           log.error("Error writing hook output", iox);
         }
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
index 3399272..7f7e8b2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
@@ -22,21 +22,16 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.events.ChangeEvent;
 import com.google.gwtorm.server.OrmException;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
 
 import java.util.Map;
+import java.util.Set;
 
 /** Invokes hooks on server actions. */
 public interface ChangeHooks {
-  public void addChangeListener(ChangeListener listener, IdentifiedUser user);
-
-  public void removeChangeListener(ChangeListener listener);
-
   /**
    * Fire the Patchset Created Hook.
    *
@@ -78,10 +73,11 @@
    * @param change The change itself.
    * @param account The gerrit user who submitted the change.
    * @param patchSet The patchset that was merged.
+   * @param mergeResultRev The SHA-1 of the merge result revision.
    * @throws OrmException
    */
   public void doChangeMergedHook(Change change, Account account,
-      PatchSet patchSet, ReviewDb db) throws OrmException;
+      PatchSet patchSet, ReviewDb db, String mergeResultRev) throws OrmException;
 
   /**
    * Fire the Merge Failed Hook.
@@ -173,21 +169,16 @@
        Account uploader, ObjectId oldId, ObjectId newId);
 
   /**
-   * Post a stream event that is related to a change
-   *
-   * @param change The change that the event is related to
-   * @param event The event to post
+   * Fire the hashtags changed Hook.
+   * @param change The change
+   * @param account The gerrit user changing the hashtags
+   * @param added List of hashtags that were added to the change
+   * @param removed List of hashtags that were removed from the change
+   * @param hashtags List of hashtags on the change after adding or removing
    * @param db The database
    * @throws OrmException
    */
-  public void postEvent(Change change, ChangeEvent event, ReviewDb db)
-      throws OrmException;
-
-  /**
-   * Post a stream event that is related to a branch
-   *
-   * @param branchName The branch that the event is related to
-   * @param event The event to post
-   */
-  public void postEvent(Branch.NameKey branchName, ChangeEvent event);
+  public void doHashtagsChangedHook(Change change, Account account,
+      Set<String>added, Set<String> removed, Set<String> hashtags,
+      ReviewDb db) throws OrmException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
index dd68296..156672e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
@@ -17,24 +17,25 @@
 import com.google.gerrit.common.ChangeHookRunner.HookResult;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch.NameKey;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.events.ChangeEvent;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.events.Event;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
 
 import java.util.Map;
+import java.util.Set;
 
 /** Does not invoke hooks. */
-public final class DisabledChangeHooks implements ChangeHooks {
+public final class DisabledChangeHooks implements ChangeHooks, EventDispatcher,
+    EventSource {
   @Override
-  public void addChangeListener(ChangeListener listener, IdentifiedUser user) {
+  public void addEventListener(EventListener listener, CurrentUser user) {
   }
 
   @Override
@@ -44,7 +45,7 @@
 
   @Override
   public void doChangeMergedHook(Change change, Account account,
-      PatchSet patchSet, ReviewDb db) {
+      PatchSet patchSet, ReviewDb db, String mergeResultRev) {
   }
 
   @Override
@@ -78,13 +79,13 @@
   }
 
   @Override
-  public void doRefUpdatedHook(NameKey refName, RefUpdate refUpdate,
+  public void doRefUpdatedHook(Branch.NameKey refName, RefUpdate refUpdate,
       Account account) {
   }
 
   @Override
-  public void doRefUpdatedHook(NameKey refName, ObjectId oldId, ObjectId newId,
-      Account account) {
+  public void doRefUpdatedHook(Branch.NameKey refName, ObjectId oldId,
+      ObjectId newId, Account account) {
   }
 
   @Override
@@ -98,7 +99,12 @@
   }
 
   @Override
-  public void removeChangeListener(ChangeListener listener) {
+  public void doHashtagsChangedHook(Change change, Account account, Set<String> added,
+      Set<String> removed, Set<String> hashtags, ReviewDb db) {
+  }
+
+  @Override
+  public void removeEventListener(EventListener listener) {
   }
 
   @Override
@@ -108,10 +114,10 @@
   }
 
   @Override
-  public void postEvent(Change change, ChangeEvent event, ReviewDb db) {
+  public void postEvent(Change change, Event event, ReviewDb db) {
   }
 
   @Override
-  public void postEvent(Branch.NameKey branchName, ChangeEvent event) {
+  public void postEvent(Branch.NameKey branchName, Event event) {
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/EventDispatcher.java b/gerrit-server/src/main/java/com/google/gerrit/common/EventDispatcher.java
new file mode 100644
index 0000000..b74771f8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/EventDispatcher.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.events.Event;
+import com.google.gwtorm.server.OrmException;
+
+
+/** Interface for posting (dispatching) Events */
+public interface EventDispatcher {
+  /**
+   * Post a stream event that is related to a change
+   *
+   * @param change The change that the event is related to
+   * @param event The event to post
+   * @param db The database
+   * @throws OrmException
+   */
+  public void postEvent(Change change, Event event, ReviewDb db)
+      throws OrmException;
+
+  /**
+   * Post a stream event that is related to a branch
+   *
+   * @param branchName The branch that the event is related to
+   * @param event The event to post
+   */
+  public void postEvent(Branch.NameKey branchName, Event event);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeListener.java b/gerrit-server/src/main/java/com/google/gerrit/common/EventListener.java
similarity index 84%
rename from gerrit-server/src/main/java/com/google/gerrit/common/ChangeListener.java
rename to gerrit-server/src/main/java/com/google/gerrit/common/EventListener.java
index b34305e..7e8a794 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/EventListener.java
@@ -15,9 +15,9 @@
 package com.google.gerrit.common;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.server.events.ChangeEvent;
+import com.google.gerrit.server.events.Event;
 
 @ExtensionPoint
-public interface ChangeListener {
-    public void onChangeEvent(ChangeEvent event);
+public interface EventListener {
+  public void onEvent(Event event);
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/common/EventSource.java
similarity index 65%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-server/src/main/java/com/google/gerrit/common/EventSource.java
index 407b7c7..e2c4b34 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/EventSource.java
@@ -12,10 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.common;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+import com.google.gerrit.server.CurrentUser;
+
+/** Distributes Events to ChangeListeners.  Register listeners here. */
+public interface EventSource {
+  public void addEventListener(EventListener listener, CurrentUser user);
+
+  public void removeEventListener(EventListener listener);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/FooterConstants.java b/gerrit-server/src/main/java/com/google/gerrit/common/FooterConstants.java
new file mode 100644
index 0000000..3ec809c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/FooterConstants.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common;
+
+import org.eclipse.jgit.revwalk.FooterKey;
+
+public class FooterConstants {
+  /** The change ID as used to track patch sets. */
+  public static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
+
+  /** The footer telling us who reviewed the change. */
+  public static final FooterKey REVIEWED_BY = new FooterKey("Reviewed-by");
+
+  /** The footer telling us the URL where the review took place. */
+  public static final FooterKey REVIEWED_ON = new FooterKey("Reviewed-on");
+
+  /** The footer telling us who tested the change. */
+  public static final FooterKey TESTED_BY = new FooterKey("Tested-by");
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/Version.java b/gerrit-server/src/main/java/com/google/gerrit/common/Version.java
index e69360a..641ba03 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/Version.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/Version.java
@@ -37,7 +37,7 @@
   private static String loadVersion() {
     try (InputStream in = Version.class.getResourceAsStream("Version")) {
       if (in == null) {
-        return null;
+        return "(dev)";
       }
       try (BufferedReader r = new BufferedReader(new InputStreamReader(in, "UTF-8"))) {
         String vs = r.readLine();
@@ -51,7 +51,7 @@
       }
     } catch (IOException e) {
       log.error(e.getMessage(), e);
-      return null;
+      return "(unknown version)";
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologCompiler.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologCompiler.java
index 7f6bff4..4724bc2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologCompiler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologCompiler.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.rules;
 
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -78,6 +78,7 @@
     git = gitRepository;
   }
 
+  @Override
   public Status call() throws IOException, CompileException {
     ObjectId metaConfig = git.resolve(RefNames.REFS_CONFIG);
     if (metaConfig == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
index 029a5d7..2afdffc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -26,9 +27,11 @@
 import com.google.inject.assistedinject.Assisted;
 
 import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
+import com.googlecode.prolog_cafe.lang.Predicate;
 import com.googlecode.prolog_cafe.lang.Prolog;
 import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
 
+import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -65,6 +68,8 @@
 
   private final Args args;
   private final Map<StoredValue<Object>, Object> storedValues;
+  private int reductionLimit;
+  private int reductionsRemaining;
   private List<Runnable> cleanup;
 
   @Inject
@@ -74,6 +79,8 @@
     setEnabled(EnumSet.allOf(Prolog.Feature.class), false);
     args = a;
     storedValues = new HashMap<>();
+    reductionLimit = a.reductionLimit;
+    reductionsRemaining = reductionLimit;
     cleanup = new LinkedList<>();
   }
 
@@ -81,6 +88,28 @@
     return args;
   }
 
+  @Override
+  public boolean isEngineStopped() {
+    if (super.isEngineStopped()) {
+      return true;
+    } else if (--reductionsRemaining <= 0) {
+      throw new ReductionLimitException(reductionLimit);
+    }
+    return false;
+  }
+
+  @Override
+  public void setPredicate(Predicate goal) {
+    super.setPredicate(goal);
+    reductionLimit = args.reductionLimit(goal);
+    reductionsRemaining = reductionLimit;
+  }
+
+  /** @return number of reductions during execution. */
+  public int getReductions() {
+    return reductionLimit - reductionsRemaining;
+  }
+
   /**
    * Lookup a stored value in the interpreter's hash manager.
    *
@@ -154,6 +183,8 @@
     private final PatchSetInfoFactory patchSetInfoFactory;
     private final IdentifiedUser.GenericFactory userFactory;
     private final Provider<AnonymousUser> anonymousUser;
+    private final int reductionLimit;
+    private final int compileLimit;
 
     @Inject
     Args(ProjectCache projectCache,
@@ -161,13 +192,29 @@
         PatchListCache patchListCache,
         PatchSetInfoFactory patchSetInfoFactory,
         IdentifiedUser.GenericFactory userFactory,
-        Provider<AnonymousUser> anonymousUser) {
+        Provider<AnonymousUser> anonymousUser,
+        @GerritServerConfig Config config) {
       this.projectCache = projectCache;
       this.repositoryManager = repositoryManager;
       this.patchListCache = patchListCache;
       this.patchSetInfoFactory = patchSetInfoFactory;
       this.userFactory = userFactory;
       this.anonymousUser = anonymousUser;
+
+      int limit = config.getInt("rules", null, "reductionLimit", 100000);
+      reductionLimit = limit <= 0 ? Integer.MAX_VALUE : limit;
+
+      limit = config.getInt("rules", null, "compileReductionLimit",
+          (int) Math.min(10L * limit, Integer.MAX_VALUE));
+      compileLimit = limit <= 0 ? Integer.MAX_VALUE : limit;
+    }
+
+    private int reductionLimit(Predicate goal) {
+      if ("com.googlecode.prolog_cafe.builtin.PRED_consult_stream_2"
+          .equals(goal.getClass().getName())) {
+        return compileLimit;
+      }
+      return reductionLimit;
     }
 
     public ProjectCache getProjectCache() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PathConflictException.java b/gerrit-server/src/main/java/com/google/gerrit/rules/ReductionLimitException.java
similarity index 65%
copy from gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PathConflictException.java
copy to gerrit-server/src/main/java/com/google/gerrit/rules/ReductionLimitException.java
index 7e2f2d7..2c27240 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PathConflictException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/ReductionLimitException.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2014 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,13 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.changedetail;
+package com.google.gerrit.rules;
 
-/** Indicates a path conflict during rebase or merge */
-public class PathConflictException extends Exception {
+/** Thrown by {@link PrologEnvironment} if a script runs too long. */
+public class ReductionLimitException extends RuntimeException {
   private static final long serialVersionUID = 1L;
 
-  public PathConflictException(String msg) {
-    super(msg);
+  ReductionLimitException(int limit) {
+    super(String.format("exceeded reduction limit of %d", limit));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValue.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValue.java
index 132360b..d454e40 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValue.java
@@ -84,7 +84,12 @@
     env.set(this, obj);
   }
 
-  /** Creates a value to store, returns null by default. */
+  /**
+   * Creates a value to store, returns null by default.
+   *
+   * @param engine Prolog engine.
+   * @return new value.
+   */
   protected T createValue(Prolog engine) {
     return null;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
index 356b7d5..528fcc6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchList;
@@ -50,7 +51,13 @@
   public static final StoredValue<ReviewDb> REVIEW_DB = create(ReviewDb.class);
   public static final StoredValue<ChangeData> CHANGE_DATA = create(ChangeData.class);
   public static final StoredValue<PatchSet> PATCH_SET = create(PatchSet.class);
+
+  // Note: no guarantees are made about the user passed in the ChangeControl; do
+  // not depend on this directly. Either use .forUser(otherUser) to get a
+  // control for a specific known user, or use CURRENT_USER, which may be null
+  // for rule types that may not depend on the current user.
   public static final StoredValue<ChangeControl> CHANGE_CONTROL = create(ChangeControl.class);
+  public static final StoredValue<CurrentUser> CURRENT_USER = create(CurrentUser.class);
 
   public static Change getChange(Prolog engine) throws SystemException {
     ChangeData cd = CHANGE_DATA.get(engine);
@@ -81,12 +88,12 @@
     @Override
     public PatchList createValue(Prolog engine) {
       PrologEnvironment env = (PrologEnvironment) engine.control;
-      PatchSetInfo psInfo = StoredValues.PATCH_SET_INFO.get(engine);
+      PatchSet ps = StoredValues.PATCH_SET.get(engine);
       PatchListCache plCache = env.getArgs().getPatchListCache();
       Change change = getChange(engine);
       Project.NameKey projectKey = change.getProject();
       ObjectId a = null;
-      ObjectId b = ObjectId.fromString(psInfo.getRevId());
+      ObjectId b = ObjectId.fromString(ps.getRevision().get());
       Whitespace ws = Whitespace.IGNORE_NONE;
       PatchListKey plKey = new PatchListKey(projectKey, a, b, ws);
       PatchList patchList;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
index b74eb77..bd175a9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.server;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.change.ChangeKind.NO_CHANGE;
 import static com.google.gerrit.server.change.ChangeKind.NO_CODE_CHANGE;
 import static com.google.gerrit.server.change.ChangeKind.TRIVIAL_REBASE;
 
-import com.google.common.base.Objects;
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Maps;
@@ -47,6 +47,7 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.NavigableSet;
+import java.util.Objects;
 import java.util.SortedSet;
 import java.util.TreeMap;
 
@@ -150,14 +151,13 @@
   }
 
   private static boolean canCopy(ProjectState project, PatchSetApproval psa,
-      PatchSet.Id psId, NavigableSet<Integer> allPsIds, ChangeKind kind)
-      throws OrmException {
+      PatchSet.Id psId, NavigableSet<Integer> allPsIds, ChangeKind kind) {
     int n = psa.getKey().getParentKey().get();
     checkArgument(n != psId.get());
     LabelType type = project.getLabelTypes().byLabel(psa.getLabelId());
     if (type == null) {
       return false;
-    } else if (Objects.equal(n, previous(allPsIds, psId.get())) && (
+    } else if (Objects.equals(n, previous(allPsIds, psId.get())) && (
         type.isCopyMinScore() && type.isMaxNegative(psa)
         || type.isCopyMaxScore() && type.isMaxPositive(psa))) {
       // Copy min/max score only from the immediately preceding patch set (which
@@ -165,7 +165,8 @@
       return true;
     }
     return (type.isCopyAllScoresOnTrivialRebase() && kind == TRIVIAL_REBASE)
-        || (type.isCopyAllScoresIfNoCodeChange() && kind == NO_CODE_CHANGE);
+        || (type.isCopyAllScoresIfNoCodeChange() && kind == NO_CODE_CHANGE)
+        || (type.isCopyAllScoresIfNoChange() && kind == NO_CHANGE);
   }
 
   private static PatchSetApproval copy(PatchSetApproval src, PatchSet.Id psId) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index 900bbdd..7265196 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -18,7 +18,6 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
-import com.google.common.base.Objects;
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
@@ -30,15 +29,16 @@
 import com.google.common.collect.Ordering;
 import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -46,7 +46,6 @@
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.notedb.ReviewerState;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -57,6 +56,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -91,7 +91,7 @@
     return Iterables.filter(psas, new Predicate<PatchSetApproval>() {
       @Override
       public boolean apply(PatchSetApproval input) {
-        return Objects.equal(input.getAccountId(), accountId);
+        return Objects.equals(input.getAccountId(), accountId);
       }
     });
   }
@@ -119,7 +119,7 @@
    */
   public ImmutableSetMultimap<ReviewerState, Account.Id> getReviewers(
       ReviewDb db, ChangeNotes notes) throws OrmException {
-    if (!migration.readPatchSetApprovals()) {
+    if (!migration.readChanges()) {
       return getReviewers(db.patchSetApprovals().byChange(notes.getChangeId()));
     }
     return notes.load().getReviewers();
@@ -137,7 +137,7 @@
   public ImmutableSetMultimap<ReviewerState, Account.Id> getReviewers(
       ChangeNotes notes, Iterable<PatchSetApproval> allApprovals)
       throws OrmException {
-    if (!migration.readPatchSetApprovals()) {
+    if (!migration.readChanges()) {
       return getReviewers(allApprovals);
     }
     return notes.load().getReviewers();
@@ -221,11 +221,12 @@
     return Collections.unmodifiableList(cells);
   }
 
-  public void addApprovals(ReviewDb db, ChangeUpdate update, LabelTypes labelTypes,
-      PatchSet ps, PatchSetInfo info, Change change, ChangeControl changeCtl,
-      Map<String, Short> approvals) throws OrmException {
+  public void addApprovals(ReviewDb db, ChangeUpdate update,
+      LabelTypes labelTypes, PatchSet ps, PatchSetInfo info,
+      ChangeControl changeCtl, Map<String, Short> approvals)
+      throws OrmException {
     if (!approvals.isEmpty()) {
-      checkApprovals(approvals, labelTypes, change, changeCtl);
+      checkApprovals(approvals, changeCtl);
       List<PatchSetApproval> cells = new ArrayList<>(approvals.size());
       Timestamp ts = TimeUtil.nowTs();
       for (Map.Entry<String, Short> vote : approvals.entrySet()) {
@@ -254,8 +255,8 @@
     }
   }
 
-  private static void checkApprovals(Map<String, Short> approvals, LabelTypes labelTypes,
-      Change change, ChangeControl changeCtl) {
+  private static void checkApprovals(Map<String, Short> approvals,
+      ChangeControl changeCtl) {
     for (Map.Entry<String, Short> vote : approvals.entrySet()) {
       String name = vote.getKey();
       Short value = vote.getValue();
@@ -269,7 +270,7 @@
 
   public ListMultimap<PatchSet.Id, PatchSetApproval> byChange(ReviewDb db,
       ChangeNotes notes) throws OrmException {
-    if (!migration.readPatchSetApprovals()) {
+    if (!migration.readChanges()) {
       ImmutableListMultimap.Builder<PatchSet.Id, PatchSetApproval> result =
           ImmutableListMultimap.builder();
       for (PatchSetApproval psa
@@ -283,7 +284,7 @@
 
   public Iterable<PatchSetApproval> byPatchSet(ReviewDb db, ChangeControl ctl,
       PatchSet.Id psId) throws OrmException {
-    if (!migration.readPatchSetApprovals()) {
+    if (!migration.readChanges()) {
       return sortApprovals(db.patchSetApprovals().byPatchSet(psId));
     }
     return copier.getForPatchSet(db, ctl, psId);
@@ -292,7 +293,7 @@
   public Iterable<PatchSetApproval> byPatchSetUser(ReviewDb db,
       ChangeControl ctl, PatchSet.Id psId, Account.Id accountId)
       throws OrmException {
-    if (!migration.readPatchSetApprovals()) {
+    if (!migration.readChanges()) {
       return sortApprovals(
           db.patchSetApprovals().byPatchSetUser(psId, accountId));
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java
index 72fd1a1..6a44219 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -50,7 +50,7 @@
   }
 
   public List<ChangeMessage> byChange(ReviewDb db, ChangeNotes notes) throws OrmException {
-    if (!migration.readChangeMessages()) {
+    if (!migration.readChanges()) {
       return
           sortChangeMessages(db.changeMessages().byChange(notes.getChangeId()));
     } else {
@@ -60,7 +60,7 @@
 
   public List<ChangeMessage> byPatchSet(ReviewDb db, ChangeNotes notes,
       PatchSet.Id psId) throws OrmException {
-    if (!migration.readChangeMessages()) {
+    if (!migration.readChanges()) {
       return sortChangeMessages(db.changeMessages().byPatchSet(psId));
     }
     return notes.load().getChangeMessages().get(psId);
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 80213f6..9faa516 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
@@ -15,21 +15,23 @@
 package com.google.gerrit.server;
 
 import static com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy.RECEIVE_COMMITS;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.MINUTES;
-import static java.util.concurrent.TimeUnit.SECONDS;
+import static com.google.gerrit.server.query.change.ChangeData.asChanges;
 
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.primitives.Ints;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetAncestor;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeMessages;
+import com.google.gerrit.server.change.ChangeTriplet;
 import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
@@ -38,15 +40,14 @@
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.RevertedSender;
-import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.gerrit.server.util.MagicBranch;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmConcurrencyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -82,15 +83,6 @@
 
 @Singleton
 public class ChangeUtil {
-  /**
-   * Epoch for sort key calculations, Tue Sep 30 2008 17:00:00.
-   * <p>
-   * We overrun approximately 4,083 years later, so ~6092.
-   */
-  @VisibleForTesting
-  private static final long SORT_KEY_EPOCH_MINS =
-      MINUTES.convert(1222819200L, SECONDS);
-
   private static final Object uuidLock = new Object();
   private static final int SEED = 0x2418e6f9;
   private static int uuidPrefix;
@@ -147,7 +139,6 @@
 
   public static void updated(Change c) {
     c.setLastUpdatedOn(TimeUtil.nowTs());
-    computeSortKey(c);
   }
 
   public static void insertAncestors(ReviewDb db, PatchSet.Id id, RevCommit src)
@@ -163,29 +154,6 @@
     db.patchSetAncestors().insert(toInsert);
   }
 
-  public static String sortKey(long lastUpdatedMs, int id) {
-    long lastUpdatedMins = MINUTES.convert(lastUpdatedMs, MILLISECONDS);
-    long minsSinceEpoch = lastUpdatedMins - SORT_KEY_EPOCH_MINS;
-    StringBuilder r = new StringBuilder(16);
-    r.setLength(16);
-    formatHexInt(r, 0, Ints.checkedCast(minsSinceEpoch));
-    formatHexInt(r, 8, id);
-    return r.toString();
-  }
-
-  public static long parseSortKey(String sortKey) {
-    if ("z".equals(sortKey)) {
-      return Long.MAX_VALUE;
-    }
-    return Long.parseLong(sortKey, 16);
-  }
-
-  public static void computeSortKey(Change c) {
-    long lastUpdatedMs = c.getLastUpdatedOn().getTime();
-    int id = c.getId().get();
-    c.setSortKey(sortKey(lastUpdatedMs, id));
-  }
-
   public static PatchSet.Id nextPatchSetId(Map<String, Ref> allRefs,
       PatchSet.Id id) {
     PatchSet.Id next = nextPatchSetId(id);
@@ -217,6 +185,7 @@
   private final Provider<CurrentUser> userProvider;
   private final CommitValidators.Factory commitValidatorsFactory;
   private final Provider<ReviewDb> db;
+  private final Provider<InternalChangeQuery> queryProvider;
   private final RevertedSender.Factory revertedSenderFactory;
   private final ChangeInserter.Factory changeInserterFactory;
   private final PatchSetInserter.Factory patchSetInserterFactory;
@@ -228,6 +197,7 @@
   ChangeUtil(Provider<CurrentUser> userProvider,
       CommitValidators.Factory commitValidatorsFactory,
       Provider<ReviewDb> db,
+      Provider<InternalChangeQuery> queryProvider,
       RevertedSender.Factory revertedSenderFactory,
       ChangeInserter.Factory changeInserterFactory,
       PatchSetInserter.Factory patchSetInserterFactory,
@@ -237,6 +207,7 @@
     this.userProvider = userProvider;
     this.commitValidatorsFactory = commitValidatorsFactory;
     this.db = db;
+    this.queryProvider = queryProvider;
     this.revertedSenderFactory = revertedSenderFactory;
     this.changeInserterFactory = changeInserterFactory;
     this.patchSetInserterFactory = patchSetInserterFactory;
@@ -247,7 +218,7 @@
 
   public Change.Id revert(ChangeControl ctl, PatchSet.Id patchSetId,
       String message, PersonIdent myIdent, SshInfo sshInfo)
-      throws NoSuchChangeException, EmailException, OrmException,
+      throws NoSuchChangeException, OrmException,
       MissingObjectException, IncorrectObjectTypeException, IOException,
       InvalidChangeOperationException {
     Change.Id changeId = patchSetId.getParentKey();
@@ -257,215 +228,200 @@
     }
     Change changeToRevert = db.get().changes().get(changeId);
 
-    Repository git;
-    try {
-      git = gitManager.openRepository(ctl.getChange().getProject());
+    Project.NameKey project = ctl.getChange().getProject();
+    try (Repository git = gitManager.openRepository(project);
+        RevWalk revWalk = new RevWalk(git)) {
+      RevCommit commitToRevert =
+          revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
+
+      PersonIdent authorIdent =
+          user().newCommitterIdent(myIdent.getWhen(), myIdent.getTimeZone());
+
+      RevCommit parentToCommitToRevert = commitToRevert.getParent(0);
+      revWalk.parseHeaders(parentToCommitToRevert);
+
+      CommitBuilder revertCommitBuilder = new CommitBuilder();
+      revertCommitBuilder.addParentId(commitToRevert);
+      revertCommitBuilder.setTreeId(parentToCommitToRevert.getTree());
+      revertCommitBuilder.setAuthor(authorIdent);
+      revertCommitBuilder.setCommitter(authorIdent);
+
+      if (message == null) {
+        message = MessageFormat.format(
+            ChangeMessages.get().revertChangeDefaultMessage,
+            changeToRevert.getSubject(), patch.getRevision().get());
+      }
+
+      ObjectId computedChangeId =
+          ChangeIdUtil.computeChangeId(parentToCommitToRevert.getTree(),
+              commitToRevert, authorIdent, myIdent, message);
+      revertCommitBuilder.setMessage(
+          ChangeIdUtil.insertId(message, computedChangeId, true));
+
+      RevCommit revertCommit;
+      try (ObjectInserter oi = git.newObjectInserter()) {
+        ObjectId id = oi.insert(revertCommitBuilder);
+        oi.flush();
+        revertCommit = revWalk.parseCommit(id);
+      }
+
+      RefControl refControl = ctl.getRefControl();
+      Change change = new Change(
+          new Change.Key("I" + computedChangeId.name()),
+          new Change.Id(db.get().nextChangeId()),
+          user().getAccountId(),
+          changeToRevert.getDest(),
+          TimeUtil.nowTs());
+      change.setTopic(changeToRevert.getTopic());
+      ChangeInserter ins =
+          changeInserterFactory.create(refControl.getProjectControl(),
+              change, revertCommit);
+      PatchSet ps = ins.getPatchSet();
+
+      String ref = refControl.getRefName();
+      String cmdRef = MagicBranch.NEW_PUBLISH_CHANGE
+          + ref.substring(ref.lastIndexOf('/') + 1);
+      CommitReceivedEvent commitReceivedEvent = new CommitReceivedEvent(
+          new ReceiveCommand(ObjectId.zeroId(), revertCommit.getId(), cmdRef),
+          refControl.getProjectControl().getProject(),
+          refControl.getRefName(), revertCommit, user());
+
+      try {
+        commitValidatorsFactory.create(refControl, sshInfo, git)
+            .validateForGerritCommits(commitReceivedEvent);
+      } catch (CommitValidationException e) {
+        throw new InvalidChangeOperationException(e.getMessage());
+      }
+
+      RefUpdate ru = git.updateRef(ps.getRefName());
+      ru.setExpectedOldObjectId(ObjectId.zeroId());
+      ru.setNewObjectId(revertCommit);
+      ru.disableRefLog();
+      if (ru.update(revWalk) != RefUpdate.Result.NEW) {
+        throw new IOException(String.format(
+            "Failed to create ref %s in %s: %s", ps.getRefName(),
+            change.getDest().getParentKey().get(), ru.getResult()));
+      }
+
+      ChangeMessage cmsg = new ChangeMessage(
+          new ChangeMessage.Key(changeId, messageUUID(db.get())),
+          user().getAccountId(), TimeUtil.nowTs(), patchSetId);
+      StringBuilder msgBuf = new StringBuilder();
+      msgBuf.append("Patch Set ").append(patchSetId.get()).append(": Reverted");
+      msgBuf.append("\n\n");
+      msgBuf.append("This patchset was reverted in change: ")
+            .append(change.getKey().get());
+      cmsg.setMessage(msgBuf.toString());
+
+      ins.setMessage(cmsg).insert();
+
+      try {
+        RevertedSender cm = revertedSenderFactory.create(change);
+        cm.setFrom(user().getAccountId());
+        cm.setChangeMessage(cmsg);
+        cm.send();
+      } catch (Exception err) {
+        log.error("Cannot send email for revert change " + change.getId(),
+            err);
+      }
+
+      return change.getId();
     } catch (RepositoryNotFoundException e) {
       throw new NoSuchChangeException(changeId, e);
     }
-    try {
-      RevWalk revWalk = new RevWalk(git);
-      try {
-        RevCommit commitToRevert =
-            revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
-
-        PersonIdent authorIdent =
-            user().newCommitterIdent(myIdent.getWhen(), myIdent.getTimeZone());
-
-        RevCommit parentToCommitToRevert = commitToRevert.getParent(0);
-        revWalk.parseHeaders(parentToCommitToRevert);
-
-        CommitBuilder revertCommitBuilder = new CommitBuilder();
-        revertCommitBuilder.addParentId(commitToRevert);
-        revertCommitBuilder.setTreeId(parentToCommitToRevert.getTree());
-        revertCommitBuilder.setAuthor(authorIdent);
-        revertCommitBuilder.setCommitter(authorIdent);
-
-        if (message == null) {
-          message = MessageFormat.format(
-              ChangeMessages.get().revertChangeDefaultMessage,
-              changeToRevert.getSubject(), patch.getRevision().get());
-        }
-
-        ObjectId computedChangeId =
-            ChangeIdUtil.computeChangeId(parentToCommitToRevert.getTree(),
-                commitToRevert, authorIdent, myIdent, message);
-        revertCommitBuilder.setMessage(
-            ChangeIdUtil.insertId(message, computedChangeId, true));
-
-        RevCommit revertCommit;
-        ObjectInserter oi = git.newObjectInserter();
-        try {
-          ObjectId id = oi.insert(revertCommitBuilder);
-          oi.flush();
-          revertCommit = revWalk.parseCommit(id);
-        } finally {
-          oi.release();
-        }
-
-        RefControl refControl = ctl.getRefControl();
-        Change change = new Change(
-            new Change.Key("I" + computedChangeId.name()),
-            new Change.Id(db.get().nextChangeId()),
-            user().getAccountId(),
-            changeToRevert.getDest(),
-            TimeUtil.nowTs());
-        change.setTopic(changeToRevert.getTopic());
-        ChangeInserter ins =
-            changeInserterFactory.create(refControl, change, revertCommit);
-        PatchSet ps = ins.getPatchSet();
-
-        String ref = refControl.getRefName();
-        String cmdRef = MagicBranch.NEW_PUBLISH_CHANGE
-            + ref.substring(ref.lastIndexOf('/') + 1);
-        CommitReceivedEvent commitReceivedEvent = new CommitReceivedEvent(
-            new ReceiveCommand(ObjectId.zeroId(), revertCommit.getId(), cmdRef),
-            refControl.getProjectControl().getProject(),
-            refControl.getRefName(), revertCommit, user());
-
-        try {
-          commitValidatorsFactory.create(refControl, sshInfo, git)
-              .validateForGerritCommits(commitReceivedEvent);
-        } catch (CommitValidationException e) {
-          throw new InvalidChangeOperationException(e.getMessage());
-        }
-
-        RefUpdate ru = git.updateRef(ps.getRefName());
-        ru.setExpectedOldObjectId(ObjectId.zeroId());
-        ru.setNewObjectId(revertCommit);
-        ru.disableRefLog();
-        if (ru.update(revWalk) != RefUpdate.Result.NEW) {
-          throw new IOException(String.format(
-              "Failed to create ref %s in %s: %s", ps.getRefName(),
-              change.getDest().getParentKey().get(), ru.getResult()));
-        }
-
-        ChangeMessage cmsg = new ChangeMessage(
-            new ChangeMessage.Key(changeId, messageUUID(db.get())),
-            user().getAccountId(), TimeUtil.nowTs(), patchSetId);
-        StringBuilder msgBuf = new StringBuilder();
-        msgBuf.append("Patch Set ").append(patchSetId.get()).append(": Reverted");
-        msgBuf.append("\n\n");
-        msgBuf.append("This patchset was reverted in change: ")
-              .append(change.getKey().get());
-        cmsg.setMessage(msgBuf.toString());
-
-        ins.setMessage(cmsg).insert();
-
-        try {
-          RevertedSender cm = revertedSenderFactory.create(change);
-          cm.setFrom(user().getAccountId());
-          cm.setChangeMessage(cmsg);
-          cm.send();
-        } catch (Exception err) {
-          log.error("Cannot send email for revert change " + change.getId(),
-              err);
-        }
-
-        return change.getId();
-      } finally {
-        revWalk.release();
-      }
-    } finally {
-      git.close();
-    }
   }
 
-  public Change.Id editCommitMessage(ChangeControl ctl, PatchSet.Id patchSetId,
-      String message, PersonIdent myIdent)
-      throws NoSuchChangeException, EmailException, OrmException,
-      MissingObjectException, IncorrectObjectTypeException, IOException,
-      InvalidChangeOperationException, PatchSetInfoNotAvailableException {
-    Change.Id changeId = patchSetId.getParentKey();
-    PatchSet originalPS = db.get().patchSets().get(patchSetId);
-    if (originalPS == null) {
-      throw new NoSuchChangeException(changeId);
-    }
+  public Change.Id editCommitMessage(ChangeControl ctl, PatchSet ps,
+      String message, PersonIdent myIdent) throws NoSuchChangeException,
+      OrmException, MissingObjectException, IncorrectObjectTypeException,
+      IOException, InvalidChangeOperationException {
+    Change change = ctl.getChange();
+    Change.Id changeId = change.getId();
 
-    if (message == null || message.length() == 0) {
+    if (Strings.isNullOrEmpty(message)) {
       throw new InvalidChangeOperationException(
           "The commit message cannot be empty");
     }
 
-    Repository git;
-    try {
-      git = gitManager.openRepository(ctl.getChange().getProject());
+    Project.NameKey project = ctl.getChange().getProject();
+    try (Repository git = gitManager.openRepository(project);
+        RevWalk revWalk = new RevWalk(git)) {
+      RevCommit commit =
+          revWalk.parseCommit(ObjectId.fromString(ps.getRevision()
+              .get()));
+      if (commit.getFullMessage().equals(message)) {
+        throw new InvalidChangeOperationException(
+            "New commit message cannot be same as existing commit message");
+      }
+
+      Date now = myIdent.getWhen();
+      PersonIdent authorIdent =
+          user().newCommitterIdent(now, myIdent.getTimeZone());
+
+      CommitBuilder commitBuilder = new CommitBuilder();
+      commitBuilder.setTreeId(commit.getTree());
+      commitBuilder.setParentIds(commit.getParents());
+      commitBuilder.setAuthor(commit.getAuthorIdent());
+      commitBuilder.setCommitter(authorIdent);
+      commitBuilder.setMessage(message);
+
+      RevCommit newCommit;
+      try (ObjectInserter oi = git.newObjectInserter()) {
+        ObjectId id = oi.insert(commitBuilder);
+        oi.flush();
+        newCommit = revWalk.parseCommit(id);
+      }
+
+      PatchSet.Id id = nextPatchSetId(git, change.currentPatchSetId());
+      PatchSet newPatchSet = new PatchSet(id);
+      newPatchSet.setCreatedOn(new Timestamp(now.getTime()));
+      newPatchSet.setUploader(user().getAccountId());
+      newPatchSet.setRevision(new RevId(newCommit.name()));
+
+      String msg = "Patch Set " + newPatchSet.getPatchSetId()
+          + ": Commit message was updated";
+
+      change = patchSetInserterFactory
+          .create(git, revWalk, ctl, newCommit)
+          .setPatchSet(newPatchSet)
+          .setMessage(msg)
+          .setValidatePolicy(RECEIVE_COMMITS)
+          .setDraft(ps.isDraft())
+          .insert();
+
+      return change.getId();
     } catch (RepositoryNotFoundException e) {
       throw new NoSuchChangeException(changeId, e);
     }
-    try {
-      RevWalk revWalk = new RevWalk(git);
-      try {
-        RevCommit commit =
-            revWalk.parseCommit(ObjectId.fromString(originalPS.getRevision()
-                .get()));
-        if (commit.getFullMessage().equals(message)) {
-          throw new InvalidChangeOperationException(
-              "New commit message cannot be same as existing commit message");
-        }
-
-        Date now = myIdent.getWhen();
-        Change change = db.get().changes().get(changeId);
-        PersonIdent authorIdent =
-            user().newCommitterIdent(now, myIdent.getTimeZone());
-
-        CommitBuilder commitBuilder = new CommitBuilder();
-        commitBuilder.setTreeId(commit.getTree());
-        commitBuilder.setParentIds(commit.getParents());
-        commitBuilder.setAuthor(commit.getAuthorIdent());
-        commitBuilder.setCommitter(authorIdent);
-        commitBuilder.setMessage(message);
-
-        RevCommit newCommit;
-        ObjectInserter oi = git.newObjectInserter();
-        try {
-          ObjectId id = oi.insert(commitBuilder);
-          oi.flush();
-          newCommit = revWalk.parseCommit(id);
-        } finally {
-          oi.release();
-        }
-
-        PatchSet.Id id = nextPatchSetId(git, change.currentPatchSetId());
-        PatchSet newPatchSet = new PatchSet(id);
-        newPatchSet.setCreatedOn(new Timestamp(now.getTime()));
-        newPatchSet.setUploader(user().getAccountId());
-        newPatchSet.setRevision(new RevId(newCommit.name()));
-
-        String msg = "Patch Set " + newPatchSet.getPatchSetId()
-            + ": Commit message was updated";
-
-        change = patchSetInserterFactory
-            .create(git, revWalk, ctl, newCommit)
-            .setPatchSet(newPatchSet)
-            .setMessage(msg)
-            .setCopyLabels(true)
-            .setValidatePolicy(RECEIVE_COMMITS)
-            .setDraft(originalPS.isDraft())
-            .insert();
-
-        return change.getId();
-      } finally {
-        revWalk.release();
-      }
-    } finally {
-      git.close();
-    }
   }
 
-  public void deleteDraftChange(PatchSet.Id patchSetId)
-      throws NoSuchChangeException, OrmException, IOException {
-    deleteDraftChange(patchSetId.getParentKey());
-  }
-
-  public void deleteDraftChange(Change.Id changeId)
-      throws NoSuchChangeException, OrmException, IOException {
-    ReviewDb db = this.db.get();
-    Change change = db.changes().get(changeId);
-    if (change == null || change.getStatus() != Change.Status.DRAFT) {
+  public String getMessage(Change change)
+      throws NoSuchChangeException, OrmException,
+      MissingObjectException, IncorrectObjectTypeException, IOException {
+    Change.Id changeId = change.getId();
+    PatchSet ps = db.get().patchSets().get(change.currentPatchSetId());
+    if (ps == null) {
       throw new NoSuchChangeException(changeId);
     }
 
+    try (Repository git = gitManager.openRepository(change.getProject());
+        RevWalk revWalk = new RevWalk(git)) {
+      RevCommit commit = revWalk.parseCommit(
+          ObjectId.fromString(ps.getRevision().get()));
+      return commit.getFullMessage();
+    } catch (RepositoryNotFoundException e) {
+      throw new NoSuchChangeException(changeId, e);
+    }
+  }
+
+  public void deleteDraftChange(Change change)
+      throws NoSuchChangeException, OrmException, IOException {
+    Change.Id changeId = change.getId();
+    if (change.getStatus() != Change.Status.DRAFT) {
+      throw new NoSuchChangeException(changeId);
+    }
+
+    ReviewDb db = this.db.get();
     for (PatchSet ps : db.patchSets().byChange(changeId)) {
       // These should all be draft patch sets.
       deleteOnlyDraftPatchSet(ps, change);
@@ -474,7 +430,7 @@
     db.changeMessages().delete(db.changeMessages().byChange(changeId));
     db.starredChanges().delete(db.starredChanges().byChange(changeId));
     db.changes().delete(Collections.singleton(change));
-    indexer.delete(db, change);
+    indexer.delete(change.getId());
   }
 
   public void deleteOnlyDraftPatchSet(PatchSet patch, Change change)
@@ -508,34 +464,54 @@
     ReviewDb db = this.db.get();
     db.accountPatchReviews().delete(db.accountPatchReviews().byPatchSet(patchSetId));
     db.changeMessages().delete(db.changeMessages().byPatchSet(patchSetId));
-    db.patchComments().delete(db.patchComments().byPatchSet(patchSetId));
     // No need to delete from notedb; draft patch sets will be filtered out.
+    db.patchComments().delete(db.patchComments().byPatchSet(patchSetId));
     db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(patchSetId));
     db.patchSetAncestors().delete(db.patchSetAncestors().byPatchSet(patchSetId));
 
     db.patchSets().delete(Collections.singleton(patch));
   }
 
+  /**
+   * Find changes matching the given identifier.
+   *
+   * @param id change identifier, either a numeric ID, a Change-Id, or
+   *     project~branch~id triplet.
+   * @return all matching changes, even if they are not visible to the current
+   *     user.
+   */
+  public List<Change> findChanges(String id)
+      throws OrmException, ResourceNotFoundException {
+    // Try legacy id
+    if (id.matches("^[1-9][0-9]*$")) {
+      Change c = db.get().changes().get(Change.Id.parse(id));
+      if (c != null) {
+        return ImmutableList.of(c);
+      }
+      return Collections.emptyList();
+    }
+
+    // Try isolated changeId
+    if (!id.contains("~")) {
+      return asChanges(queryProvider.get().byKeyPrefix(id));
+    }
+
+    // Try change triplet
+    Optional<ChangeTriplet> triplet = ChangeTriplet.parse(id);
+    if (triplet.isPresent()) {
+      return asChanges(queryProvider.get().byBranchKey(
+          triplet.get().branch(),
+          triplet.get().id()));
+    }
+
+    throw new ResourceNotFoundException(id);
+  }
+
   private IdentifiedUser user() {
     return (IdentifiedUser) userProvider.get();
   }
 
-  private static PatchSet.Id nextPatchSetId(PatchSet.Id id) {
+  public static PatchSet.Id nextPatchSetId(PatchSet.Id id) {
     return new PatchSet.Id(id.getParentKey(), id.get() + 1);
   }
-
-  private static final char[] hexchar =
-      {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', //
-          'a', 'b', 'c', 'd', 'e', 'f'};
-
-  private static void formatHexInt(final StringBuilder dst, final int p, int w) {
-    int o = p + 7;
-    while (o >= p && w != 0) {
-      dst.setCharAt(o--, hexchar[w & 0xf]);
-      w >>>= 4;
-    }
-    while (o >= p) {
-      dst.setCharAt(o--, '0');
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
index d10366e..956a0d1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gerrit.util.cli.OptionHandlerUtil;
 import com.google.gerrit.util.cli.OptionHandlers;
+
 import org.eclipse.jgit.lib.ObjectId;
 import org.kohsuke.args4j.spi.OptionHandler;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CommonConverters.java b/gerrit-server/src/main/java/com/google/gerrit/server/CommonConverters.java
new file mode 100644
index 0000000..be07bde
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CommonConverters.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import com.google.gerrit.extensions.common.GitPerson;
+
+import org.eclipse.jgit.lib.PersonIdent;
+
+import java.sql.Timestamp;
+
+/**
+ * Converters to classes in {@code com.google.gerrit.extensions.common}.
+ * <p>
+ * The server frequently needs to convert internal types to types exposed in the
+ * extension API, but the converters themselves are not part of this API. This
+ * class contains such converters as static utility methods.
+ */
+public class CommonConverters {
+  public static GitPerson toGitPerson(PersonIdent ident) {
+    GitPerson result = new GitPerson();
+    result.name = ident.getName();
+    result.email = ident.getEmailAddress();
+    result.date = new Timestamp(ident.getWhen().getTime());
+    result.tz = ident.getTimeZoneOffset();
+    return result;
+  }
+
+  private CommonConverters() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
index 6d798cc..be5e000 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -30,9 +30,11 @@
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
+import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.DisableReverseDnsLookup;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
@@ -66,25 +68,31 @@
   public static class GenericFactory {
     private final CapabilityControl.Factory capabilityControlFactory;
     private final AuthConfig authConfig;
+    private final Realm realm;
     private final String anonymousCowardName;
     private final Provider<String> canonicalUrl;
     private final AccountCache accountCache;
     private final GroupBackend groupBackend;
+    private final Boolean disableReverseDnsLookup;
 
     @Inject
     public GenericFactory(
         @Nullable CapabilityControl.Factory capabilityControlFactory,
         AuthConfig authConfig,
+        Realm realm,
         @AnonymousCowardName String anonymousCowardName,
         @CanonicalWebUrl Provider<String> canonicalUrl,
+        @DisableReverseDnsLookup Boolean disableReverseDnsLookup,
         AccountCache accountCache,
         GroupBackend groupBackend) {
       this.capabilityControlFactory = capabilityControlFactory;
       this.authConfig = authConfig;
+      this.realm = realm;
       this.anonymousCowardName = anonymousCowardName;
       this.canonicalUrl = canonicalUrl;
       this.accountCache = accountCache;
       this.groupBackend = groupBackend;
+      this.disableReverseDnsLookup = disableReverseDnsLookup;
     }
 
     public IdentifiedUser create(final Account.Id id) {
@@ -92,22 +100,22 @@
     }
 
     public IdentifiedUser create(Provider<ReviewDb> db, Account.Id id) {
-      return new IdentifiedUser(capabilityControlFactory,
-          authConfig, anonymousCowardName, canonicalUrl, accountCache,
-          groupBackend, null, db, id, null);
+      return new IdentifiedUser(capabilityControlFactory, authConfig, realm,
+          anonymousCowardName, canonicalUrl, accountCache, groupBackend,
+          disableReverseDnsLookup, null, db, id, null);
     }
 
     public IdentifiedUser create(SocketAddress remotePeer, Account.Id id) {
-      return new IdentifiedUser(capabilityControlFactory,
-          authConfig, anonymousCowardName, canonicalUrl, accountCache,
-          groupBackend, Providers.of(remotePeer), null, id,  null);
+      return new IdentifiedUser(capabilityControlFactory, authConfig, realm,
+          anonymousCowardName, canonicalUrl, accountCache, groupBackend,
+          disableReverseDnsLookup, Providers.of(remotePeer), null, id, null);
     }
 
     public CurrentUser runAs(SocketAddress remotePeer, Account.Id id,
         @Nullable CurrentUser caller) {
-      return new IdentifiedUser(capabilityControlFactory,
-          authConfig, anonymousCowardName, canonicalUrl, accountCache,
-          groupBackend, Providers.of(remotePeer), null, id, caller);
+      return new IdentifiedUser(capabilityControlFactory, authConfig, realm,
+          anonymousCowardName, canonicalUrl, accountCache, groupBackend,
+          disableReverseDnsLookup, Providers.of(remotePeer), null, id, caller);
     }
   }
 
@@ -121,10 +129,12 @@
   public static class RequestFactory {
     private final CapabilityControl.Factory capabilityControlFactory;
     private final AuthConfig authConfig;
+    private final Realm realm;
     private final String anonymousCowardName;
     private final Provider<String> canonicalUrl;
     private final AccountCache accountCache;
     private final GroupBackend groupBackend;
+    private final Boolean disableReverseDnsLookup;
 
     private final Provider<SocketAddress> remotePeerProvider;
     private final Provider<ReviewDb> dbProvider;
@@ -133,34 +143,38 @@
     RequestFactory(
         CapabilityControl.Factory capabilityControlFactory,
         final AuthConfig authConfig,
+        Realm realm,
         final @AnonymousCowardName String anonymousCowardName,
         final @CanonicalWebUrl Provider<String> canonicalUrl,
         final AccountCache accountCache,
         final GroupBackend groupBackend,
+        final @DisableReverseDnsLookup Boolean disableReverseDnsLookup,
 
         final @RemotePeer Provider<SocketAddress> remotePeerProvider,
         final Provider<ReviewDb> dbProvider) {
       this.capabilityControlFactory = capabilityControlFactory;
       this.authConfig = authConfig;
+      this.realm = realm;
       this.anonymousCowardName = anonymousCowardName;
       this.canonicalUrl = canonicalUrl;
       this.accountCache = accountCache;
       this.groupBackend = groupBackend;
+      this.disableReverseDnsLookup = disableReverseDnsLookup;
 
       this.remotePeerProvider = remotePeerProvider;
       this.dbProvider = dbProvider;
     }
 
     public IdentifiedUser create(Account.Id id) {
-      return new IdentifiedUser(capabilityControlFactory,
-          authConfig, anonymousCowardName, canonicalUrl, accountCache,
-          groupBackend, remotePeerProvider, dbProvider, id, null);
+      return new IdentifiedUser(capabilityControlFactory, authConfig, realm,
+          anonymousCowardName, canonicalUrl, accountCache, groupBackend,
+          disableReverseDnsLookup, remotePeerProvider, dbProvider, id, null);
     }
 
     public IdentifiedUser runAs(Account.Id id, CurrentUser caller) {
-      return new IdentifiedUser(capabilityControlFactory,
-          authConfig, anonymousCowardName, canonicalUrl, accountCache,
-          groupBackend, remotePeerProvider, dbProvider, id, caller);
+      return new IdentifiedUser(capabilityControlFactory, authConfig, realm,
+          anonymousCowardName, canonicalUrl, accountCache, groupBackend,
+          disableReverseDnsLookup, remotePeerProvider, dbProvider, id, caller);
     }
   }
 
@@ -175,8 +189,11 @@
   private final Provider<String> canonicalUrl;
   private final AccountCache accountCache;
   private final AuthConfig authConfig;
+  private final Realm realm;
   private final GroupBackend groupBackend;
   private final String anonymousCowardName;
+  private final Boolean disableReverseDnsLookup;
+  private final Set<String> validEmails = Sets.newHashSetWithExpectedSize(4);
 
   @Nullable
   private final Provider<SocketAddress> remotePeerProvider;
@@ -187,7 +204,8 @@
   private final Account.Id accountId;
 
   private AccountState state;
-  private Set<String> emailAddresses;
+  private boolean loadedAllEmails;
+  private Set<String> invalidEmails;
   private GroupMembership effectiveGroups;
   private Set<Change.Id> starredChanges;
   private ResultSet<StarredChange> starredQuery;
@@ -197,10 +215,12 @@
   private IdentifiedUser(
       CapabilityControl.Factory capabilityControlFactory,
       final AuthConfig authConfig,
+      Realm realm,
       final String anonymousCowardName,
       final Provider<String> canonicalUrl,
       final AccountCache accountCache,
       final GroupBackend groupBackend,
+      final Boolean disableReverseDnsLookup,
       @Nullable final Provider<SocketAddress> remotePeerProvider,
       @Nullable final Provider<ReviewDb> dbProvider,
       final Account.Id id,
@@ -210,7 +230,9 @@
     this.accountCache = accountCache;
     this.groupBackend = groupBackend;
     this.authConfig = authConfig;
+    this.realm = realm;
     this.anonymousCowardName = anonymousCowardName;
+    this.disableReverseDnsLookup = disableReverseDnsLookup;
     this.remotePeerProvider = remotePeerProvider;
     this.dbProvider = dbProvider;
     this.accountId = id;
@@ -259,11 +281,27 @@
     return diffPref;
   }
 
-  public Set<String> getEmailAddresses() {
-    if (emailAddresses == null) {
-      emailAddresses = state().getEmailAddresses();
+  public boolean hasEmailAddress(String email) {
+    if (validEmails.contains(email)) {
+      return true;
+    } else if (invalidEmails != null && invalidEmails.contains(email)) {
+      return false;
+    } else if (realm.hasEmailAddress(this, email)) {
+      validEmails.add(email);
+      return true;
+    } else if (invalidEmails == null) {
+      invalidEmails = Sets.newHashSetWithExpectedSize(4);
     }
-    return emailAddresses;
+    invalidEmails.add(email);
+    return false;
+  }
+
+  public Set<String> getEmailAddresses() {
+    if (!loadedAllEmails) {
+      validEmails.addAll(realm.getEmailAddresses(this));
+      loadedAllEmails = true;
+    }
+    return validEmails;
   }
 
   public String getName() {
@@ -383,7 +421,7 @@
         final InetSocketAddress sa = (InetSocketAddress) remotePeer;
         final InetAddress in = sa.getAddress();
 
-        host = in != null ? in.getCanonicalHostName() : sa.getHostName();
+        host = in != null ? getHost(in) : sa.getHostName();
       }
     }
     if (host == null || host.isEmpty()) {
@@ -444,4 +482,11 @@
   public boolean isIdentifiedUser() {
     return true;
   }
+
+  private String getHost(final InetAddress in) {
+    if (Boolean.FALSE.equals(disableReverseDnsLookup)) {
+      return in.getCanonicalHostName();
+    }
+    return in.getHostAddress();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
index 6f5618b..ef28ed8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.CapabilityControl;
@@ -39,8 +40,9 @@
     InternalUser create();
   }
 
+  @VisibleForTesting
   @Inject
-  protected InternalUser(CapabilityControl.Factory capabilityControlFactory) {
+  public InternalUser(CapabilityControl.Factory capabilityControlFactory) {
     super(capabilityControlFactory);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
index 4918546..f67fa75 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
@@ -12,25 +12,46 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-
 package com.google.gerrit.server;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.DraftCommentNotes;
 import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Utility functions to manipulate PatchLineComments.
@@ -40,44 +61,232 @@
  */
 @Singleton
 public class PatchLineCommentsUtil {
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsers;
+  private final DraftCommentNotes.Factory draftFactory;
   private final NotesMigration migration;
 
   @VisibleForTesting
   @Inject
-  public PatchLineCommentsUtil(NotesMigration migration) {
+  public PatchLineCommentsUtil(GitRepositoryManager repoManager,
+      AllUsersNameProvider allUsersProvider,
+      DraftCommentNotes.Factory draftFactory,
+      NotesMigration migration) {
+    this.repoManager = repoManager;
+    this.allUsers = allUsersProvider.get();
+    this.draftFactory = draftFactory;
     this.migration = migration;
   }
 
+  public Optional<PatchLineComment> get(ReviewDb db, ChangeNotes notes,
+      PatchLineComment.Key key) throws OrmException {
+    if (!migration.readChanges()) {
+      return Optional.fromNullable(db.patchComments().get(key));
+    }
+    for (PatchLineComment c : publishedByChange(db, notes)) {
+      if (key.equals(c.getKey())) {
+        return Optional.of(c);
+      }
+    }
+    for (PatchLineComment c : draftByChange(db, notes)) {
+      if (key.equals(c.getKey())) {
+        return Optional.of(c);
+      }
+    }
+    return Optional.absent();
+  }
+
+  public List<PatchLineComment> publishedByChange(ReviewDb db,
+      ChangeNotes notes) throws OrmException {
+    if (!migration.readChanges()) {
+      return sort(byCommentStatus(
+          db.patchComments().byChange(notes.getChangeId()), Status.PUBLISHED));
+    }
+
+    notes.load();
+    List<PatchLineComment> comments = Lists.newArrayList();
+    comments.addAll(notes.getBaseComments().values());
+    comments.addAll(notes.getPatchSetComments().values());
+    return sort(comments);
+  }
+
+  public List<PatchLineComment> draftByChange(ReviewDb db,
+      ChangeNotes notes) throws OrmException {
+    if (!migration.readChanges()) {
+      return sort(byCommentStatus(
+          db.patchComments().byChange(notes.getChangeId()), Status.DRAFT));
+    }
+
+    List<PatchLineComment> comments = Lists.newArrayList();
+    Iterable<String> filtered = getDraftRefs(notes.getChangeId());
+    for (String refName : filtered) {
+      Account.Id account = Account.Id.fromRefPart(refName);
+      if (account != null) {
+        comments.addAll(draftByChangeAuthor(db, notes, account));
+      }
+    }
+    return sort(comments);
+  }
+
+  private static List<PatchLineComment> byCommentStatus(
+      ResultSet<PatchLineComment> comments,
+      final PatchLineComment.Status status) {
+    return Lists.newArrayList(
+      Iterables.filter(comments, new Predicate<PatchLineComment>() {
+        @Override
+        public boolean apply(PatchLineComment input) {
+          return (input.getStatus() == status);
+        }
+      })
+    );
+  }
+
+  public List<PatchLineComment> byPatchSet(ReviewDb db,
+      ChangeNotes notes, PatchSet.Id psId) throws OrmException {
+    if (!migration.readChanges()) {
+      return sort(db.patchComments().byPatchSet(psId).toList());
+    }
+    List<PatchLineComment> comments = Lists.newArrayList();
+    comments.addAll(publishedByPatchSet(db, notes, psId));
+
+    Iterable<String> filtered = getDraftRefs(notes.getChangeId());
+    for (String refName : filtered) {
+      Account.Id account = Account.Id.fromRefPart(refName);
+      if (account != null) {
+        comments.addAll(draftByPatchSetAuthor(db, psId, account, notes));
+      }
+    }
+    return sort(comments);
+  }
+
   public List<PatchLineComment> publishedByChangeFile(ReviewDb db,
       ChangeNotes notes, Change.Id changeId, String file) throws OrmException {
-    if (!migration.readPublishedComments()) {
-      return db.patchComments().publishedByChangeFile(changeId, file).toList();
+    if (!migration.readChanges()) {
+      return sort(
+          db.patchComments().publishedByChangeFile(changeId, file).toList());
     }
     notes.load();
-    List<PatchLineComment> commentsOnFile = new ArrayList<PatchLineComment>();
+    List<PatchLineComment> comments = Lists.newArrayList();
 
-    // We must iterate through all comments to find the ones on this file.
-    addCommentsInFile(commentsOnFile, notes.getBaseComments().values(), file);
-    addCommentsInFile(commentsOnFile, notes.getPatchSetComments().values(),
+    addCommentsOnFile(comments, notes.getBaseComments().values(), file);
+    addCommentsOnFile(comments, notes.getPatchSetComments().values(),
         file);
-
-    Collections.sort(commentsOnFile, ChangeNotes.PatchLineCommentComparator);
-    return commentsOnFile;
+    return sort(comments);
   }
 
   public List<PatchLineComment> publishedByPatchSet(ReviewDb db,
       ChangeNotes notes, PatchSet.Id psId) throws OrmException {
-    if (!migration.readPublishedComments()) {
-      return db.patchComments().publishedByPatchSet(psId).toList();
+    if (!migration.readChanges()) {
+      return sort(
+          db.patchComments().publishedByPatchSet(psId).toList());
     }
     notes.load();
-    List<PatchLineComment> commentsOnPs = new ArrayList<PatchLineComment>();
-    commentsOnPs.addAll(notes.getPatchSetComments().get(psId));
-    commentsOnPs.addAll(notes.getBaseComments().get(psId));
-    return commentsOnPs;
+    List<PatchLineComment> comments = new ArrayList<>();
+    comments.addAll(notes.getPatchSetComments().get(psId));
+    comments.addAll(notes.getBaseComments().get(psId));
+    return sort(comments);
   }
 
-  private static Collection<PatchLineComment> addCommentsInFile(
+  public List<PatchLineComment> draftByPatchSetAuthor(ReviewDb db,
+      PatchSet.Id psId, Account.Id author, ChangeNotes notes)
+      throws OrmException {
+    if (!migration.readChanges()) {
+      return sort(
+          db.patchComments().draftByPatchSetAuthor(psId, author).toList());
+    }
+
+    List<PatchLineComment> comments = Lists.newArrayList();
+    comments.addAll(notes.getDraftBaseComments(author).row(psId).values());
+    comments.addAll(notes.getDraftPsComments(author).row(psId).values());
+    return sort(comments);
+  }
+
+  public List<PatchLineComment> draftByChangeFileAuthor(ReviewDb db,
+      ChangeNotes notes, String file, Account.Id author)
+      throws OrmException {
+    if (!migration.readChanges()) {
+      return sort(
+          db.patchComments()
+            .draftByChangeFileAuthor(notes.getChangeId(), file, author)
+            .toList());
+    }
+    List<PatchLineComment> comments = Lists.newArrayList();
+    addCommentsOnFile(comments, notes.getDraftBaseComments(author).values(),
+        file);
+    addCommentsOnFile(comments, notes.getDraftPsComments(author).values(),
+        file);
+    return sort(comments);
+  }
+
+  public List<PatchLineComment> draftByChangeAuthor(ReviewDb db,
+      ChangeNotes notes, Account.Id author)
+      throws OrmException {
+    if (!migration.readChanges()) {
+      return sort(db.patchComments().byChange(notes.getChangeId()).toList());
+    }
+    List<PatchLineComment> comments = Lists.newArrayList();
+    comments.addAll(notes.getDraftBaseComments(author).values());
+    comments.addAll(notes.getDraftPsComments(author).values());
+    return sort(comments);
+  }
+
+  public List<PatchLineComment> draftByAuthor(ReviewDb db,
+      Account.Id author) throws OrmException {
+    if (!migration.readChanges()) {
+      return sort(db.patchComments().draftByAuthor(author).toList());
+    }
+
+    Set<String> refNames =
+        getRefNamesAllUsers(RefNames.REFS_DRAFT_COMMENTS);
+
+    List<PatchLineComment> comments = Lists.newArrayList();
+    for (String refName : refNames) {
+      Account.Id id = Account.Id.fromRefPart(refName);
+      if (!author.equals(id)) {
+        continue;
+      }
+      Change.Id changeId = Change.Id.parse(refName);
+      DraftCommentNotes draftNotes =
+          draftFactory.create(changeId, author).load();
+      comments.addAll(draftNotes.getDraftBaseComments().values());
+      comments.addAll(draftNotes.getDraftPsComments().values());
+    }
+    return sort(comments);
+  }
+
+  public void insertComments(ReviewDb db, ChangeUpdate update,
+      Iterable<PatchLineComment> comments) throws OrmException {
+    for (PatchLineComment c : comments) {
+      update.insertComment(c);
+    }
+    db.patchComments().insert(comments);
+  }
+
+  public void upsertComments(ReviewDb db, ChangeUpdate update,
+      Iterable<PatchLineComment> comments) throws OrmException {
+    for (PatchLineComment c : comments) {
+      update.upsertComment(c);
+    }
+    db.patchComments().upsert(comments);
+  }
+
+  public void updateComments(ReviewDb db, ChangeUpdate update,
+      Iterable<PatchLineComment> comments) throws OrmException {
+    for (PatchLineComment c : comments) {
+      update.updateComment(c);
+    }
+    db.patchComments().update(comments);
+  }
+
+  public void deleteComments(ReviewDb db, ChangeUpdate update,
+      Iterable<PatchLineComment> comments) throws OrmException {
+    for (PatchLineComment c : comments) {
+      update.deleteComment(c);
+    }
+    db.patchComments().delete(comments);
+  }
+
+  private static Collection<PatchLineComment> addCommentsOnFile(
       Collection<PatchLineComment> commentsOnFile,
       Collection<PatchLineComment> allComments,
       String file) {
@@ -90,11 +299,53 @@
     return commentsOnFile;
   }
 
-  public void addPublishedComments(ReviewDb db, ChangeUpdate update,
-      Iterable<PatchLineComment> comments) throws OrmException {
-    for (PatchLineComment c : comments) {
-      update.putComment(c);
+  public static void setCommentRevId(PatchLineComment c,
+      PatchListCache cache, Change change, PatchSet ps) throws OrmException {
+    if (c.getRevId() != null) {
+      return;
     }
-    db.patchComments().upsert(comments);
+    PatchList patchList;
+    try {
+      patchList = cache.get(change, ps);
+    } catch (PatchListNotAvailableException e) {
+      throw new OrmException(e);
+    }
+    c.setRevId((c.getSide() == (short) 0)
+      ? new RevId(ObjectId.toString(patchList.getOldId()))
+      : new RevId(ObjectId.toString(patchList.getNewId())));
+  }
+
+  private Set<String> getRefNamesAllUsers(String prefix) throws OrmException {
+    Repository repo;
+    try {
+      repo = repoManager.openRepository(allUsers);
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+    try {
+      RefDatabase refDb = repo.getRefDatabase();
+      return refDb.getRefs(prefix).keySet();
+    } catch (IOException e) {
+      throw new OrmException(e);
+    } finally {
+      repo.close();
+    }
+  }
+
+  private Iterable<String> getDraftRefs(final Change.Id changeId)
+      throws OrmException {
+    Set<String> refNames = getRefNamesAllUsers(RefNames.REFS_DRAFT_COMMENTS);
+    final String suffix = "-" + changeId.get();
+    return Iterables.filter(refNames, new Predicate<String>() {
+      @Override
+      public boolean apply(String input) {
+        return input.endsWith(suffix);
+      }
+    });
+  }
+
+  private static List<PatchLineComment> sort(List<PatchLineComment> comments) {
+    Collections.sort(comments, ChangeNotes.PatchLineCommentComparator);
+    return comments;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java b/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java
index b8c0888..6e12346 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java
@@ -45,6 +45,7 @@
     }
   }
 
+  @Override
   public void run() {
     synchronized (cleanup) {
       ran = true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java b/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
index fe07100..1403e60 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
@@ -14,40 +14,164 @@
 
 package com.google.gerrit.server;
 
-import com.google.common.collect.Lists;
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.base.Strings;
+import com.google.common.collect.FluentIterable;
+import com.google.gerrit.extensions.common.DiffWebLinkInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.webui.BranchWebLink;
+import com.google.gerrit.extensions.webui.DiffWebLink;
+import com.google.gerrit.extensions.webui.FileWebLink;
 import com.google.gerrit.extensions.webui.PatchSetWebLink;
 import com.google.gerrit.extensions.webui.ProjectWebLink;
+import com.google.gerrit.extensions.webui.WebLink;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
-import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+@Singleton
 public class WebLinks {
+  private static final Logger log = LoggerFactory.getLogger(WebLinks.class);
+  private static final Predicate<WebLinkInfo> INVALID_WEBLINK =
+      new Predicate<WebLinkInfo>() {
+
+        @Override
+        public boolean apply(WebLinkInfo link) {
+          if (link == null){
+            return false;
+          } else if (Strings.isNullOrEmpty(link.name)
+              || Strings.isNullOrEmpty(link.url)) {
+            log.warn(String.format("%s is missing name and/or url",
+                link.getClass().getName()));
+            return false;
+          }
+          return true;
+        }
+      };
 
   private final DynamicSet<PatchSetWebLink> patchSetLinks;
+  private final DynamicSet<FileWebLink> fileLinks;
+  private final DynamicSet<DiffWebLink> diffLinks;
   private final DynamicSet<ProjectWebLink> projectLinks;
+  private final DynamicSet<BranchWebLink> branchLinks;
 
+  @Inject
   public WebLinks(DynamicSet<PatchSetWebLink> patchSetLinks,
-      DynamicSet<ProjectWebLink> projectLinks) {
+      DynamicSet<FileWebLink> fileLinks,
+      DynamicSet<DiffWebLink> diffLinks,
+      DynamicSet<ProjectWebLink> projectLinks,
+      DynamicSet<BranchWebLink> branchLinks) {
     this.patchSetLinks = patchSetLinks;
+    this.fileLinks = fileLinks;
+    this.diffLinks = diffLinks;
     this.projectLinks = projectLinks;
+    this.branchLinks = branchLinks;
   }
 
-  public Iterable<WebLinkInfo> getPatchSetLinks(String project, String commit) {
-    List<WebLinkInfo> links = Lists.newArrayList();
-    for (PatchSetWebLink webLink : patchSetLinks) {
-      links.add(new WebLinkInfo(webLink.getLinkName(),
-          webLink.getPatchSetUrl(project, commit)));
-    }
-    return links;
+  /**
+   *
+   * @param project Project name.
+   * @param commit SHA1 of commit.
+   * @return Links for patch sets.
+   */
+  public FluentIterable<WebLinkInfo> getPatchSetLinks(final Project.NameKey project,
+      final String commit) {
+    return filterLinks(patchSetLinks, new Function<WebLink, WebLinkInfo>() {
+
+      @Override
+      public WebLinkInfo apply(WebLink webLink) {
+        return ((PatchSetWebLink)webLink).getPatchSetWebLink(project.get(), commit);
+      }
+    });
   }
 
-  public Iterable<WebLinkInfo> getProjectLinks(String project) {
-    List<WebLinkInfo> links = Lists.newArrayList();
-    for (ProjectWebLink webLink : projectLinks) {
-      links.add(new WebLinkInfo(webLink.getLinkName(),
-          webLink.getProjectUrl(project)));
-    }
-    return links;
+  /**
+   *
+   * @param project Project name.
+   * @param revision SHA1 of revision.
+   * @param file File name.
+   * @return Links for files.
+   */
+  public FluentIterable<WebLinkInfo> getFileLinks(final String project, final String revision,
+      final String file) {
+    return filterLinks(fileLinks, new Function<WebLink, WebLinkInfo>() {
+
+      @Override
+      public WebLinkInfo apply(WebLink webLink) {
+        return ((FileWebLink)webLink).getFileWebLink(project, revision, file);
+      }
+    });
+  }
+
+  /**
+   *
+   * @param project Project name.
+   * @param patchSetIdA Patch set ID of side A, <code>null</code> if no base
+   *        patch set was selected.
+   * @param revisionA SHA1 of revision of side A.
+   * @param fileA File name of side A.
+   * @param patchSetIdB Patch set ID of side B.
+   * @param revisionB SHA1 of revision of side B.
+   * @param fileB File name of side B.
+   * @return Links for file diffs.
+   */
+  public FluentIterable<DiffWebLinkInfo> getDiffLinks(final String project, final int changeId,
+      final Integer patchSetIdA, final String revisionA, final String fileA,
+      final int patchSetIdB, final String revisionB, final String fileB) {
+   return FluentIterable
+       .from(diffLinks)
+       .transform(new Function<WebLink, DiffWebLinkInfo>() {
+         @Override
+         public DiffWebLinkInfo apply(WebLink webLink) {
+            return ((DiffWebLink) webLink).getDiffLink(project, changeId,
+                patchSetIdA, revisionA, fileA,
+                patchSetIdB, revisionB, fileB);
+          }
+       })
+       .filter(INVALID_WEBLINK);
+ }
+
+  /**
+   *
+   * @param project Project name.
+   * @return Links for projects.
+   */
+  public FluentIterable<WebLinkInfo> getProjectLinks(final String project) {
+    return filterLinks(projectLinks, new Function<WebLink, WebLinkInfo>() {
+
+      @Override
+      public WebLinkInfo apply(WebLink webLink) {
+        return ((ProjectWebLink)webLink).getProjectWeblink(project);
+      }
+    });
+  }
+
+  /**
+   *
+   * @param project Project name
+   * @param branch Branch name
+   * @return Links for branches.
+   */
+  public FluentIterable<WebLinkInfo> getBranchLinks(final String project, final String branch) {
+    return filterLinks(branchLinks, new Function<WebLink, WebLinkInfo>() {
+
+      @Override
+      public WebLinkInfo apply(WebLink webLink) {
+        return ((BranchWebLink)webLink).getBranchWebLink(project, branch);
+      }
+    });
+  }
+
+  private FluentIterable<WebLinkInfo> filterLinks(DynamicSet<? extends WebLink> links,
+      Function<WebLink, WebLinkInfo> transformer) {
+    return FluentIterable
+        .from(links)
+        .transform(transformer)
+        .filter(INVALID_WEBLINK);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/WebLinksProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/WebLinksProvider.java
deleted file mode 100644
index e3ffa62..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/WebLinksProvider.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2014 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server;
-
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.webui.PatchSetWebLink;
-import com.google.gerrit.extensions.webui.ProjectWebLink;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class WebLinksProvider implements Provider<WebLinks> {
-
-  private final DynamicSet<PatchSetWebLink> patchSetLinks;
-  private final DynamicSet<ProjectWebLink> projectLinks;
-
-  @Inject
-  public WebLinksProvider(DynamicSet<PatchSetWebLink> patchSetLinks,
-      DynamicSet<ProjectWebLink> projectLinks) {
-    this.patchSetLinks = patchSetLinks;
-    this.projectLinks = projectLinks;
-  }
-
-  @Override
-  public WebLinks get() {
-    WebLinks webLinks = new WebLinks(patchSetLinks, projectLinks);
-    return webLinks;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
index 5d651ae..580897f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
@@ -37,7 +37,6 @@
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.group.GroupJson;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
@@ -78,7 +77,7 @@
       ProjectCache projectCache, ProjectJson projectJson,
       MetaDataUpdate.Server metaDataUpdateFactory,
       GroupControl.Factory groupControlFactory, GroupBackend groupBackend,
-      GroupJson groupJson, AllProjectsName allProjectsName) {
+      AllProjectsName allProjectsName) {
     this.self = self;
     this.projectControlFactory = projectControlFactory;
     this.projectCache = projectCache;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractGroupBackend.java
similarity index 65%
copy from gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractGroupBackend.java
index 2514272..b50b003 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractGroupBackend.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2015 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,13 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd.plugins;
+package com.google.gerrit.server.account;
 
-import com.google.common.cache.Weigher;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 
-class ResourceWeigher implements Weigher<ResourceKey, Resource> {
+public abstract class AbstractGroupBackend implements GroupBackend {
   @Override
-  public int weigh(ResourceKey key, Resource value) {
-    return key.weigh() + value.weigh();
+  public boolean isVisibleToAll(AccountGroup.UUID uuid) {
+    return false;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
new file mode 100644
index 0000000..7031672
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.server.IdentifiedUser;
+
+import java.util.Collection;
+import java.util.Objects;
+import java.util.Set;
+
+/** Basic implementation of {@link Realm}.  */
+public abstract class AbstractRealm implements Realm {
+  @Override
+  public boolean hasEmailAddress(IdentifiedUser user, String email) {
+    for (AccountExternalId ext : user.state().getExternalIds()) {
+      if (Objects.equals(ext.getEmailAddress(), email)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public Set<String> getEmailAddresses(IdentifiedUser user) {
+    Collection<AccountExternalId> ids = user.state().getExternalIds();
+    Set<String> emails = Sets.newHashSetWithExpectedSize(ids.size());
+    for (AccountExternalId ext : ids) {
+      if (!Strings.isNullOrEmpty(ext.getEmailAddress())) {
+        emails.add(ext.getEmailAddress());
+      }
+    }
+    return emails;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
index 4827ed5..45d3d1f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
@@ -65,6 +65,7 @@
     this.cache = cache;
   }
 
+  @Override
   public Set<Account.Id> get(final String email) {
     try {
       return cache.get(email);
@@ -74,6 +75,7 @@
     }
   }
 
+  @Override
   public void evict(final String email) {
     if (email != null) {
       cache.invalidate(email);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
index a521840..cc62b2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -18,13 +18,13 @@
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -79,6 +79,7 @@
     this.byName = byUsername;
   }
 
+  @Override
   public AccountState get(Account.Id accountId) {
     try {
       return byId.get(accountId);
@@ -88,6 +89,7 @@
     }
   }
 
+  @Override
   public AccountState getIfPresent(Account.Id accountId) {
     return byId.getIfPresent(accountId);
   }
@@ -103,12 +105,14 @@
     }
   }
 
+  @Override
   public void evict(Account.Id accountId) {
     if (accountId != null) {
       byId.invalidate(accountId);
     }
   }
 
+  @Override
   public void evictByUsername(String username) {
     if (username != null) {
       byName.invalidate(username);
@@ -117,6 +121,7 @@
 
   private static AccountState missing(Account.Id accountId) {
     Account account = new Account(accountId, TimeUtil.nowTs());
+    account.setActive(false);
     Collection<AccountExternalId> ids = Collections.emptySet();
     Set<AccountGroup.UUID> anon = ImmutableSet.of();
     return new AccountState(account, anon, ids);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java
index 4dc9d79..445ac6e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.extensions.common.AccountInfo;
+
 import java.util.Set;
 
 /**
@@ -34,7 +36,10 @@
     AVATARS,
 
     /** Unique user identity to login to Gerrit, may be deprecated. */
-    USERNAME
+    USERNAME,
+
+    /** Numeric account ID, may be deprecated. */
+    ID;
   }
 
   public abstract void fillAccountInfo(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfo.java
deleted file mode 100644
index 97abbf6..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfo.java
+++ /dev/null
@@ -1,130 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.account;
-
-import com.google.common.base.Throwables;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.account.AccountDirectory.DirectoryException;
-import com.google.gerrit.server.account.AccountDirectory.FillOptions;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-public class AccountInfo {
-  public static class Loader {
-    private static final Set<FillOptions> DETAILED_OPTIONS =
-        Collections.unmodifiableSet(EnumSet.of(
-            FillOptions.NAME,
-            FillOptions.EMAIL,
-            FillOptions.USERNAME,
-            FillOptions.AVATARS));
-
-    public interface Factory {
-      Loader create(boolean detailed);
-    }
-
-    private final InternalAccountDirectory directory;
-    private final boolean detailed;
-    private final Map<Account.Id, AccountInfo> created;
-    private final List<AccountInfo> provided;
-
-    @Inject
-    Loader(InternalAccountDirectory directory, @Assisted boolean detailed) {
-      this.directory = directory;
-      this.detailed = detailed;
-      created = Maps.newHashMap();
-      provided = Lists.newArrayList();
-    }
-
-    public AccountInfo get(Account.Id id) {
-      if (id == null) {
-        return null;
-      }
-      AccountInfo info = created.get(id);
-      if (info == null) {
-        info = new AccountInfo(id);
-        if (detailed) {
-          info._accountId = id.get();
-        }
-        created.put(id, info);
-      }
-      return info;
-    }
-
-    public void put(AccountInfo info) {
-      if (detailed) {
-        info._accountId = info._id.get();
-      }
-      provided.add(info);
-    }
-
-    public void fill() throws OrmException {
-      try {
-        directory.fillAccountInfo(
-            Iterables.concat(created.values(), provided),
-            detailed ? DETAILED_OPTIONS : EnumSet.of(FillOptions.NAME));
-      } catch (DirectoryException e) {
-        Throwables.propagateIfPossible(e.getCause(), OrmException.class);
-        throw new OrmException(e);
-      }
-    }
-
-    public void fill(Collection<? extends AccountInfo> infos)
-        throws OrmException {
-      for (AccountInfo info : infos) {
-        put(info);
-      }
-      fill();
-    }
-  }
-
-  public transient Account.Id _id;
-
-  public AccountInfo(Account.Id id) {
-    _id = id;
-  }
-
-  public Integer _accountId;
-  public String name;
-  public String email;
-  public String username;
-  public List<AvatarInfo> avatars;
-
-  public static class AvatarInfo {
-    /**
-     * Size in pixels the UI prefers an avatar image to be.
-     *
-     * The web UI prefers avatar images to be square, both
-     * the height and width of the image should be this size.
-     * The height is the more important dimension to match
-     * than the width.
-     */
-    public static final int DEFAULT_SIZE = 26;
-
-    public String url;
-    public Integer height;
-    public Integer width;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountLoader.java
new file mode 100644
index 0000000..3e9b575
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountLoader.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.AccountDirectory.DirectoryException;
+import com.google.gerrit.server.account.AccountDirectory.FillOptions;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class AccountLoader {
+  private static final Set<FillOptions> DETAILED_OPTIONS =
+      Collections.unmodifiableSet(EnumSet.of(
+          FillOptions.ID,
+          FillOptions.NAME,
+          FillOptions.EMAIL,
+          FillOptions.USERNAME,
+          FillOptions.AVATARS));
+
+  public interface Factory {
+    AccountLoader create(boolean detailed);
+  }
+
+  private final InternalAccountDirectory directory;
+  private final Set<FillOptions> options;
+  private final Map<Account.Id, AccountInfo> created;
+  private final List<AccountInfo> provided;
+
+  @Inject
+  AccountLoader(InternalAccountDirectory directory, @Assisted boolean detailed) {
+    this.directory = directory;
+    options = detailed ? DETAILED_OPTIONS : InternalAccountDirectory.ID_ONLY;
+    created = Maps.newHashMap();
+    provided = Lists.newArrayList();
+  }
+
+  public AccountInfo get(Account.Id id) {
+    if (id == null) {
+      return null;
+    }
+    AccountInfo info = created.get(id);
+    if (info == null) {
+      info = new AccountInfo(id.get());
+      created.put(id, info);
+    }
+    return info;
+  }
+
+  public void put(AccountInfo info) {
+    checkArgument(info._accountId != null, "_accountId field required");
+    provided.add(info);
+  }
+
+  public void fill() throws OrmException {
+    try {
+      directory.fillAccountInfo(
+          Iterables.concat(created.values(), provided), options);
+    } catch (DirectoryException e) {
+      Throwables.propagateIfPossible(e.getCause(), OrmException.class);
+      throw new OrmException(e);
+    }
+  }
+
+  public void fill(Collection<? extends AccountInfo> infos)
+      throws OrmException {
+    for (AccountInfo info : infos) {
+      put(info);
+    }
+    fill();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index 77ebe0f..62615c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.audit.AuditService;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
@@ -23,12 +25,11 @@
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -36,7 +37,9 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /** Tracks authentication related details for user accounts. */
@@ -53,14 +56,17 @@
   private final ChangeUserName.Factory changeUserNameFactory;
   private final ProjectCache projectCache;
   private final AtomicBoolean awaitsFirstAccountCheck;
+  private final AuditService auditService;
 
   @Inject
-  AccountManager(final SchemaFactory<ReviewDb> schema,
-      final AccountCache byIdCache, final AccountByEmailCache byEmailCache,
-      final Realm accountMapper,
-      final IdentifiedUser.GenericFactory userFactory,
-      final ChangeUserName.Factory changeUserNameFactory,
-      final ProjectCache projectCache) throws OrmException {
+  AccountManager(SchemaFactory<ReviewDb> schema,
+      AccountCache byIdCache,
+      AccountByEmailCache byEmailCache,
+      Realm accountMapper,
+      IdentifiedUser.GenericFactory userFactory,
+      ChangeUserName.Factory changeUserNameFactory,
+      ProjectCache projectCache,
+      AuditService auditService) {
     this.schema = schema;
     this.byIdCache = byIdCache;
     this.byEmailCache = byEmailCache;
@@ -69,16 +75,17 @@
     this.changeUserNameFactory = changeUserNameFactory;
     this.projectCache = projectCache;
     this.awaitsFirstAccountCheck = new AtomicBoolean(true);
+    this.auditService = auditService;
   }
 
   /**
    * @return user identified by this external identity string, or null.
    */
-  public Account.Id lookup(final String externalId) throws AccountException {
+  public Account.Id lookup(String externalId) throws AccountException {
     try {
-      final ReviewDb db = schema.open();
+      ReviewDb db = schema.open();
       try {
-        final AccountExternalId ext =
+        AccountExternalId ext =
             db.accountExternalIds().get(new AccountExternalId.Key(externalId));
         return ext != null ? ext.getAccountId() : null;
       } finally {
@@ -100,10 +107,10 @@
   public AuthResult authenticate(AuthRequest who) throws AccountException {
     who = realm.authenticate(who);
     try {
-      final ReviewDb db = schema.open();
+      ReviewDb db = schema.open();
       try {
-        final AccountExternalId.Key key = id(who);
-        final AccountExternalId id = db.accountExternalIds().get(key);
+        AccountExternalId.Key key = id(who);
+        AccountExternalId id = db.accountExternalIds().get(key);
         if (id == null) {
           // New account, automatically create and return.
           //
@@ -129,16 +136,16 @@
     }
   }
 
-  private void update(final ReviewDb db, final AuthRequest who,
-      final AccountExternalId extId) throws OrmException {
-    final IdentifiedUser user = userFactory.create(extId.getAccountId());
+  private void update(ReviewDb db, AuthRequest who, AccountExternalId extId)
+      throws OrmException {
+    IdentifiedUser user = userFactory.create(extId.getAccountId());
     Account toUpdate = null;
 
     // If the email address was modified by the authentication provider,
     // update our records to match the changed email.
     //
-    final String newEmail = who.getEmailAddress();
-    final String oldEmail = extId.getEmailAddress();
+    String newEmail = who.getEmailAddress();
+    String oldEmail = extId.getEmailAddress();
     if (newEmail != null && !newEmail.equals(oldEmail)) {
       if (oldEmail != null
           && oldEmail.equals(user.getAccount().getPreferredEmail())) {
@@ -185,21 +192,21 @@
     return toUpdate;
   }
 
-  private static boolean eq(final String a, final String b) {
+  private static boolean eq(String a, String b) {
     return (a == null && b == null) || (a != null && a.equals(b));
   }
 
-  private AuthResult create(final ReviewDb db, final AuthRequest who)
+  private AuthResult create(ReviewDb db, AuthRequest who)
       throws OrmException, AccountException {
-    final Account.Id newId = new Account.Id(db.nextAccountId());
-    final Account account = new Account(newId, TimeUtil.nowTs());
-    final AccountExternalId extId = createId(newId, who);
+    Account.Id newId = new Account.Id(db.nextAccountId());
+    Account account = new Account(newId, TimeUtil.nowTs());
+    AccountExternalId extId = createId(newId, who);
 
     extId.setEmailAddress(who.getEmailAddress());
     account.setFullName(who.getDisplayName());
     account.setPreferredEmail(extId.getEmailAddress());
 
-    final boolean isFirstAccount = awaitsFirstAccountCheck.getAndSet(false)
+    boolean isFirstAccount = awaitsFirstAccountCheck.getAndSet(false)
       && db.accounts().anyAccounts().toList().isEmpty();
 
     try {
@@ -222,13 +229,12 @@
           .getAccessSection(AccessSection.GLOBAL_CAPABILITIES)
           .getPermission(GlobalCapability.ADMINISTRATE_SERVER);
 
-      final AccountGroup.UUID uuid = admin.getRules().get(0).getGroup().getUUID();
-      final AccountGroup g = db.accountGroups().byUUID(uuid).iterator().next();
-      final AccountGroup.Id adminId = g.getId();
-      final AccountGroupMember m =
+      AccountGroup.UUID uuid = admin.getRules().get(0).getGroup().getUUID();
+      AccountGroup g = db.accountGroups().byUUID(uuid).iterator().next();
+      AccountGroup.Id adminId = g.getId();
+      AccountGroupMember m =
           new AccountGroupMember(new AccountGroupMember.Key(newId, adminId));
-      db.accountGroupMembersAudit().insert(Collections.singleton(
-          new AccountGroupMemberAudit(m, newId, TimeUtil.nowTs())));
+      auditService.dispatchAddAccountsToGroup(newId, Collections.singleton(m));
       db.accountGroupMembers().insert(Collections.singleton(m));
     }
 
@@ -239,17 +245,17 @@
       try {
         changeUserNameFactory.create(db, user, who.getUserName()).call();
       } catch (NameAlreadyUsedException e) {
-        final String message =
+        String message =
             "Cannot assign user name \"" + who.getUserName() + "\" to account "
                 + newId + "; name already in use.";
         handleSettingUserNameFailure(db, account, extId, message, e, false);
       } catch (InvalidUserNameException e) {
-        final String message =
+        String message =
             "Cannot assign user name \"" + who.getUserName() + "\" to account "
                 + newId + "; name does not conform.";
         handleSettingUserNameFailure(db, account, extId, message, e, false);
       } catch (OrmException e) {
-        final String message = "Cannot assign user name";
+        String message = "Cannot assign user name";
         handleSettingUserNameFailure(db, account, extId, message, e, true);
       }
     }
@@ -278,9 +284,9 @@
    *         user to manually set the user name
    * @throws OrmException thrown if cleaning the database failed
    */
-  private void handleSettingUserNameFailure(final ReviewDb db,
-      final Account account, final AccountExternalId extId,
-      final String errorMessage, final Exception e, final boolean logException)
+  private void handleSettingUserNameFailure(ReviewDb db, Account account,
+      AccountExternalId extId, String errorMessage, Exception e,
+      boolean logException)
       throws AccountUserNameException, OrmException {
     if (logException) {
       log.error(errorMessage, e);
@@ -302,9 +308,8 @@
     }
   }
 
-  private static AccountExternalId createId(final Account.Id newId,
-      final AuthRequest who) {
-    final String ext = who.getExternalId();
+  private static AccountExternalId createId(Account.Id newId, AuthRequest who) {
+    String ext = who.getExternalId();
     return new AccountExternalId(newId, new AccountExternalId.Key(ext));
   }
 
@@ -317,13 +322,13 @@
    * @throws AccountException the identity belongs to a different account, or it
    *         cannot be linked at this time.
    */
-  public AuthResult link(final Account.Id to, AuthRequest who)
+  public AuthResult link(Account.Id to, AuthRequest who)
       throws AccountException, OrmException {
-    final ReviewDb db = schema.open();
+    ReviewDb db = schema.open();
     try {
       who = realm.link(db, to, who);
 
-      final AccountExternalId.Key key = id(who);
+      AccountExternalId.Key key = id(who);
       AccountExternalId extId = db.accountExternalIds().get(key);
       if (extId != null) {
         if (!extId.getAccountId().equals(to)) {
@@ -337,7 +342,7 @@
         db.accountExternalIds().insert(Collections.singleton(extId));
 
         if (who.getEmailAddress() != null) {
-          final Account a = db.accounts().get(to);
+          Account a = db.accounts().get(to);
           if (a.getPreferredEmail() == null) {
             a.setPreferredEmail(who.getEmailAddress());
             db.accounts().update(Collections.singleton(a));
@@ -358,6 +363,50 @@
   }
 
   /**
+   * Update the link to another unique authentication identity to an existing account.
+   *
+   * Existing external identities with the same scheme will be removed and replaced
+   * with the new one.
+   *
+   * @param to account to link the identity onto.
+   * @param who the additional identity.
+   * @return the result of linking the identity to the user.
+   * @throws OrmException
+   * @throws AccountException the identity belongs to a different account, or it
+   *         cannot be linked at this time.
+   */
+  public AuthResult updateLink(Account.Id to, AuthRequest who) throws OrmException,
+      AccountException {
+    ReviewDb db = schema.open();
+    try {
+      AccountExternalId.Key key = id(who);
+      List<AccountExternalId.Key> filteredKeysByScheme =
+          filterKeysByScheme(key.getScheme(), db.accountExternalIds()
+              .byAccount(to));
+      if (!filteredKeysByScheme.isEmpty()
+          && (filteredKeysByScheme.size() > 1 || !filteredKeysByScheme
+              .contains(key))) {
+        db.accountExternalIds().deleteKeys(filteredKeysByScheme);
+      }
+      byIdCache.evict(to);
+      return link(to, who);
+    } finally {
+      db.close();
+    }
+  }
+
+  private List<AccountExternalId.Key> filterKeysByScheme(
+      String keyScheme, ResultSet<AccountExternalId> externalIds) {
+    List<AccountExternalId.Key> filteredExternalIds = new ArrayList<>();
+    for (AccountExternalId accountExternalId : externalIds) {
+      if (accountExternalId.isScheme(keyScheme)) {
+        filteredExternalIds.add(accountExternalId.getKey());
+      }
+    }
+    return filteredExternalIds;
+  }
+
+  /**
    * Unlink an authentication identity from an existing account.
    *
    * @param from account to unlink the identity from.
@@ -366,13 +415,13 @@
    * @throws AccountException the identity belongs to a different account, or it
    *         cannot be unlinked at this time.
    */
-  public AuthResult unlink(final Account.Id from, AuthRequest who)
+  public AuthResult unlink(Account.Id from, AuthRequest who)
       throws AccountException, OrmException {
-    final ReviewDb db = schema.open();
+    ReviewDb db = schema.open();
     try {
       who = realm.unlink(db, from, who);
 
-      final AccountExternalId.Key key = id(who);
+      AccountExternalId.Key key = id(who);
       AccountExternalId extId = db.accountExternalIds().get(key);
       if (extId != null) {
         if (!extId.getAccountId().equals(from)) {
@@ -381,7 +430,7 @@
         db.accountExternalIds().delete(Collections.singleton(extId));
 
         if (who.getEmailAddress() != null) {
-          final Account a = db.accounts().get(from);
+          Account a = db.accounts().get(from);
           if (a.getPreferredEmail() != null
               && a.getPreferredEmail().equals(who.getEmailAddress())) {
             a.setPreferredEmail(null);
@@ -403,7 +452,7 @@
   }
 
 
-  private static AccountExternalId.Key id(final AuthRequest who) {
+  private static AccountExternalId.Key id(AuthRequest who) {
     return new AccountExternalId.Key(who.getExternalId());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResource.java
index 106c033..75e5ae5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResource.java
@@ -48,11 +48,11 @@
     return user;
   }
 
-  static class Capability implements RestResource {
+  public static class Capability implements RestResource {
     private final IdentifiedUser user;
     private final String capability;
 
-    Capability(IdentifiedUser user, String capability) {
+    public Capability(IdentifiedUser user, String capability) {
       this.user = user;
       this.capability = capability;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
index 66607e2..815b519 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 
 import java.util.Collection;
-import java.util.HashSet;
 import java.util.Set;
 
 public class AccountState {
@@ -64,25 +63,6 @@
     return null;
   }
 
-  /**
-   * All email addresses registered to this account.
-   * <p>
-   * Gerrit is "reasonably certain" that the returned email addresses actually
-   * belong to the user of the account. Some emails may have been obtained from
-   * the authentication provider, which in the case of OpenID may be trusting
-   * the provider to have validated the address. Other emails may have been
-   * validated by Gerrit directly.
-   */
-  public Set<String> getEmailAddresses() {
-    final Set<String> emails = new HashSet<>();
-    for (final AccountExternalId e : externalIds) {
-      if (e.getEmailAddress() != null && !e.getEmailAddress().isEmpty()) {
-        emails.add(e.getEmailAddress());
-      }
-    }
-    return emails;
-  }
-
   /** The external identities that identify the account holder. */
   public Collection<AccountExternalId> getExternalIds() {
     return externalIds;
@@ -93,7 +73,7 @@
     return internalGroups;
   }
 
-  private static String getUserName(Collection<AccountExternalId> ids) {
+  public static String getUserName(Collection<AccountExternalId> ids) {
     for (AccountExternalId id : ids) {
       if (id.isScheme(SCHEME_USERNAME)) {
         return id.getSchemeRest();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
index 5ef745b..efe7322 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
@@ -40,6 +40,7 @@
   private final AccountResolver resolver;
   private final AccountControl.Factory accountControlFactory;
   private final IdentifiedUser.GenericFactory userFactory;
+  private final Provider<SuggestAccounts> list;
   private final DynamicMap<RestView<AccountResource>> views;
   private final CreateAccount.Factory createAccountFactory;
 
@@ -48,12 +49,14 @@
       AccountResolver resolver,
       AccountControl.Factory accountControlFactory,
       IdentifiedUser.GenericFactory userFactory,
+      Provider<SuggestAccounts> list,
       DynamicMap<RestView<AccountResource>> views,
       CreateAccount.Factory createAccountFactory) {
     this.self = self;
     this.resolver = resolver;
     this.accountControlFactory = accountControlFactory;
     this.userFactory = userFactory;
+    this.list = list;
     this.views = views;
     this.createAccountFactory = createAccountFactory;
   }
@@ -128,7 +131,7 @@
 
   @Override
   public RestView<TopLevelResource> list() throws ResourceNotFoundException {
-    throw new ResourceNotFoundException();
+    return list.get();
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java
index f60c794..3017f73 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java
@@ -34,6 +34,7 @@
   private final Map<String, List<PermissionRule>> permissions;
 
   public final List<PermissionRule> administrateServer;
+  public final List<PermissionRule> batchChangesLimit;
   public final List<PermissionRule> emailReviewers;
   public final List<PermissionRule> priority;
   public final List<PermissionRule> queryLimit;
@@ -74,6 +75,7 @@
     permissions = Collections.unmodifiableMap(res);
 
     administrateServer = getPermission(GlobalCapability.ADMINISTRATE_SERVER);
+    batchChangesLimit = getPermission(GlobalCapability.BATCH_CHANGES_LIMIT);
     emailReviewers = getPermission(GlobalCapability.EMAIL_REVIEWERS);
     priority = getPermission(GlobalCapability.PRIORITY);
     queryLimit = getPermission(GlobalCapability.QUERY_LIMIT);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index f0f22b5..2721057 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -110,6 +110,12 @@
       || canAdministrateServer();
   }
 
+  /** @return true if the user can modify an account for another user. */
+  public boolean canModifyAccount() {
+    return canPerform(GlobalCapability.MODIFY_ACCOUNT)
+      || canAdministrateServer();
+  }
+
   /** @return true if the user can view all accounts. */
   public boolean canViewAllAccounts() {
     return canPerform(GlobalCapability.VIEW_ALL_ACCOUNTS)
@@ -163,12 +169,6 @@
         || canAdministrateServer();
   }
 
-  /** @return true if the user can generate HTTP passwords for users other than self. */
-  public boolean canGenerateHttpPassword() {
-    return canPerform(GlobalCapability.GENERATE_HTTP_PASSWORD)
-        || canAdministrateServer();
-  }
-
   /** @return true if the user can impersonate another user. */
   public boolean canRunAs() {
     return canPerform(GlobalCapability.RUN_AS);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java
index 6b68032..1cb27f1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.CapabilityControl;
 import com.google.inject.Provider;
 
 import org.slf4j.Logger;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
index 6dd51e1..1005569 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
@@ -87,6 +87,7 @@
     this.newUsername = newUsername;
   }
 
+  @Override
   public VoidResult call() throws OrmException, NameAlreadyUsedException,
       InvalidUserNameException {
     final Collection<AccountExternalId> old = old();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
index 2d42f0d..38fda4c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
@@ -15,10 +15,13 @@
 package com.google.gerrit.server.account;
 
 import com.google.common.collect.Sets;
+import com.google.gerrit.audit.AuditService;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -30,14 +33,12 @@
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.CreateAccount.Input;
 import com.google.gerrit.server.group.GroupsCollection;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -72,15 +73,16 @@
   private final SshKeyCache sshKeyCache;
   private final AccountCache accountCache;
   private final AccountByEmailCache byEmailCache;
-  private final AccountInfo.Loader.Factory infoLoader;
+  private final AccountLoader.Factory infoLoader;
   private final String username;
+  private final AuditService auditService;
 
   @Inject
   CreateAccount(ReviewDb db, Provider<IdentifiedUser> currentUser,
       GroupsCollection groupsCollection, SshKeyCache sshKeyCache,
       AccountCache accountCache, AccountByEmailCache byEmailCache,
-      AccountInfo.Loader.Factory infoLoader,
-      @Assisted String username) {
+      AccountLoader.Factory infoLoader,
+      @Assisted String username, AuditService auditService) {
     this.db = db;
     this.currentUser = currentUser;
     this.groupsCollection = groupsCollection;
@@ -89,6 +91,7 @@
     this.byEmailCache = byEmailCache;
     this.infoLoader = infoLoader;
     this.username = username;
+    this.auditService = auditService;
   }
 
   @Override
@@ -169,9 +172,8 @@
     for (AccountGroup.Id groupId : groups) {
       AccountGroupMember m =
           new AccountGroupMember(new AccountGroupMember.Key(id, groupId));
-      db.accountGroupMembersAudit().insert(Collections.singleton(
-          new AccountGroupMemberAudit(
-              m, currentUser.get().getAccountId(), TimeUtil.nowTs())));
+      auditService.dispatchAddAccountsToGroup(currentUser.get().getAccountId(),
+          Collections.singleton(m));
       db.accountGroupMembers().insert(Collections.singleton(m));
     }
 
@@ -179,7 +181,7 @@
     accountCache.evictByUsername(username);
     byEmailCache.evict(input.email);
 
-    AccountInfo.Loader loader = infoLoader.create(true);
+    AccountLoader loader = infoLoader.create(true);
     AccountInfo info = loader.get(id);
     loader.fill();
     return Response.created(info);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
index 4be8067..441213d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
@@ -23,8 +23,8 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.CreateEmail.Input;
@@ -85,7 +85,7 @@
       ResourceNotFoundException, OrmException, EmailException,
       MethodNotAllowedException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
+        && !self.get().getCapabilities().canModifyAccount()) {
       throw new AuthException("not allowed to add email address");
     }
 
@@ -98,8 +98,8 @@
     }
 
     if (input.noConfirmation
-        && !self.get().getCapabilities().canAdministrateServer()) {
-      throw new AuthException("must be administrator to use no_confirmation");
+        && !self.get().getCapabilities().canModifyAccount()) {
+      throw new AuthException("not allowed to use no_confirmation");
     }
 
     return apply(rsrc.getUser(), input);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
index 938d940..362a39f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -25,7 +25,7 @@
 import java.util.Set;
 
 @Singleton
-public class DefaultRealm implements Realm {
+public class DefaultRealm extends AbstractRealm {
   private final EmailExpander emailExpander;
   private final AccountByEmailCache byEmail;
   private final AuthConfig authConfig;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
index 52ab651..abdaf23 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
@@ -29,7 +29,7 @@
 
 import java.util.Collections;
 
-@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@RequiresCapability(GlobalCapability.MODIFY_ACCOUNT)
 @Singleton
 public class DeleteActive implements RestModifyView<AccountResource, Input> {
   public static class Input {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
index 6048586..f1e02bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
@@ -55,7 +55,7 @@
       throws AuthException, ResourceNotFoundException,
       ResourceConflictException, MethodNotAllowedException, OrmException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
+        && !self.get().getCapabilities().canModifyAccount()) {
       throw new AuthException("not allowed to delete email address");
     }
     return apply(rsrc.getUser(), rsrc.getEmail());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java
index 7df1848..9066858 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.DeleteSshKey.Input;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gwtorm.server.OrmException;
@@ -32,18 +34,26 @@
   public static class Input {
   }
 
+  private final Provider<CurrentUser> self;
   private final Provider<ReviewDb> dbProvider;
   private final SshKeyCache sshKeyCache;
 
   @Inject
-  DeleteSshKey(Provider<ReviewDb> dbProvider, SshKeyCache sshKeyCache) {
+  DeleteSshKey(Provider<ReviewDb> dbProvider,
+      Provider<CurrentUser> self,
+      SshKeyCache sshKeyCache) {
+    this.self = self;
     this.dbProvider = dbProvider;
     this.sshKeyCache = sshKeyCache;
   }
 
   @Override
   public Response<?> apply(AccountResource.SshKey rsrc, Input input)
-      throws OrmException {
+      throws AuthException, OrmException {
+    if (self.get() != rsrc.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("not allowed to delete SSH keys");
+    }
     dbProvider.get().accountSshKeys()
         .deleteKeys(Collections.singleton(rsrc.getSshKey().getKey()));
     sshKeyCache.evict(rsrc.getUser().getUserName());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/EmailExpander.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/EmailExpander.java
index c664377..f60492e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/EmailExpander.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/EmailExpander.java
@@ -65,6 +65,7 @@
       return !user.contains(" ");
     }
 
+    @Override
     public String expand(final String user) {
       return lhs + user + rhs;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java
index a178562..733cf5b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java
@@ -66,7 +66,7 @@
         throw new ResourceNotFoundException();
       }
       return new AccountResource.Email(rsrc.getUser(), email);
-    } else if (rsrc.getUser().getEmailAddresses().contains(id.get())) {
+    } else if (rsrc.getUser().hasEmailAddress(id.get())) {
       return new AccountResource.Email(rsrc.getUser(), id.get());
     } else {
       throw new ResourceNotFoundException();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/FakeRealm.java
similarity index 84%
rename from gerrit-server/src/test/java/com/google/gerrit/testutil/FakeRealm.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/account/FakeRealm.java
index 7214a5c..f8d6bd1 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/FakeRealm.java
@@ -12,16 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.testutil;
+package com.google.gerrit.server.account;
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.account.AuthRequest;
-import com.google.gerrit.server.account.Realm;
 
-/** Fake implementation of {@link Realm} for testing. */
-public class FakeRealm implements Realm {
+/** Fake implementation of {@link Realm} that does not communicate. */
+public class FakeRealm extends AbstractRealm {
   @Override
   public boolean allowsEdit(FieldName field) {
     return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAccount.java
index 200595f..05f8300 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAccount.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -21,16 +22,16 @@
 
 @Singleton
 public class GetAccount implements RestReadView<AccountResource> {
-  private final AccountInfo.Loader.Factory infoFactory;
+  private final AccountLoader.Factory infoFactory;
 
   @Inject
-  GetAccount(AccountInfo.Loader.Factory infoFactory) {
+  GetAccount(AccountLoader.Factory infoFactory) {
     this.infoFactory = infoFactory;
   }
 
   @Override
   public AccountInfo apply(AccountResource rsrc) throws OrmException {
-    AccountInfo.Loader loader = infoFactory.create(true);
+    AccountLoader loader = infoFactory.create(true);
     AccountInfo info = loader.get(rsrc.getUser().getAccountId());
     loader.fill();
     return info;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
index 47047ed..43b76e2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
@@ -21,6 +21,7 @@
 import static com.google.gerrit.common.data.GlobalCapability.EMAIL_REVIEWERS;
 import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES;
 import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK;
+import static com.google.gerrit.common.data.GlobalCapability.MODIFY_ACCOUNT;
 import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
 import static com.google.gerrit.common.data.GlobalCapability.RUN_GC;
 import static com.google.gerrit.common.data.GlobalCapability.STREAM_EVENTS;
@@ -114,6 +115,7 @@
     have.put(EMAIL_REVIEWERS, cc.canEmailReviewers());
     have.put(FLUSH_CACHES, cc.canFlushCaches());
     have.put(KILL_TASK, cc.canKillTask());
+    have.put(MODIFY_ACCOUNT, cc.canModifyAccount());
     have.put(RUN_GC, cc.canRunGC());
     have.put(STREAM_EVENTS, cc.canStreamEvents());
     have.put(VIEW_ALL_ACCOUNTS, cc.canViewAllAccounts());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
index 5959fac..19aeefc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.extensions.client.Theme;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Theme;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
@@ -70,6 +70,7 @@
       info.skipDeleted = p.isSkipDeleted() ? true : null;
       info.skipUncommented = p.isSkipUncommented() ? true : null;
       info.hideTopMenu = p.isHideTopMenu() ? true : null;
+      info.autoHideDiffTableHeader = p.isAutoHideDiffTableHeader() ? true : null;
       info.hideLineNumbers = p.isHideLineNumbers() ? true : null;
       info.syntaxHighlighting = p.isSyntaxHighlighting() ? true : null;
       info.tabSize = p.getTabSize();
@@ -93,6 +94,7 @@
     public Boolean skipUncommented;
     public Boolean syntaxHighlighting;
     public Boolean hideTopMenu;
+    public Boolean autoHideDiffTableHeader;
     public Boolean hideLineNumbers;
     public Boolean renderEntireFile;
     public Boolean hideEmptyPane;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java
index 64991e6..bf9c9ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java
@@ -40,7 +40,7 @@
   public List<EmailInfo> apply(AccountResource rsrc) throws AuthException,
       OrmException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
+        && !self.get().getCapabilities().canModifyAccount()) {
       throw new AuthException("not allowed to list email addresses");
     }
 
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 ccc6e48..e45e7cc 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
@@ -21,13 +21,11 @@
 import com.google.gerrit.extensions.webui.TopMenu;
 import com.google.gerrit.reviewdb.client.Account;
 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.ReviewCategoryStrategy;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
@@ -108,14 +106,12 @@
     Boolean copySelfOnEmail;
     DateFormat dateFormat;
     TimeFormat timeFormat;
-    Boolean reversePatchSetOrder;
     Boolean relativeDateInChangeTable;
     Boolean sizeBarInChangeTable;
     Boolean legacycidInChangeTable;
+    Boolean muteCommonPathPrefixes;
     ReviewCategoryStrategy reviewCategoryStrategy;
-    CommentVisibilityStrategy commentVisibilityStrategy;
     DiffView diffView;
-    ChangeScreen changeScreen;
     List<TopMenu.MenuItem> my;
 
     public PreferenceInfo(AccountGeneralPreferences p,
@@ -129,14 +125,12 @@
         copySelfOnEmail = p.isCopySelfOnEmails() ? true : null;
         dateFormat = p.getDateFormat();
         timeFormat = p.getTimeFormat();
-        reversePatchSetOrder = p.isReversePatchSetOrder() ? true : null;
         relativeDateInChangeTable = p.isRelativeDateInChangeTable() ? true : null;
         sizeBarInChangeTable = p.isSizeBarInChangeTable() ? true : null;
         legacycidInChangeTable = p.isLegacycidInChangeTable() ? true : null;
+        muteCommonPathPrefixes = p.isMuteCommonPathPrefixes() ? true : null;
         reviewCategoryStrategy = p.getReviewCategoryStrategy();
-        commentVisibilityStrategy = p.getCommentVisibilityStrategy();
         diffView = p.getDiffView();
-        changeScreen = p.getChangeScreen();
       }
       my = my(v, allUsers);
     }
@@ -155,7 +149,7 @@
       }
       if (my.isEmpty()) {
         my.add(new TopMenu.MenuItem("Changes", "#/dashboard/self", null));
-        my.add(new TopMenu.MenuItem("Drafts", "#/q/is:draft", null));
+        my.add(new TopMenu.MenuItem("Drafts", "#/q/owner:self+is:draft", null));
         my.add(new TopMenu.MenuItem("Draft Comments", "#/q/has:draft", null));
         my.add(new TopMenu.MenuItem("Watched Changes", "#/q/is:watched+is:open", null));
         my.add(new TopMenu.MenuItem("Starred Changes", "#/q/is:starred", null));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
index 9266c3a..6846470 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
@@ -45,7 +45,7 @@
   public List<SshKeyInfo> apply(AccountResource rsrc) throws AuthException,
       OrmException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
+        && !self.get().getCapabilities().canModifyAccount()) {
       throw new AuthException("not allowed to get SSH keys");
     }
     return apply(rsrc.getUser());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java
index 43b94f3..c65f6d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java
@@ -50,4 +50,10 @@
 
   /** @return the group membership checker for the backend. */
   GroupMembership membershipsOf(IdentifiedUser user);
+
+  /**
+   * @return {@code true} if the group with the given UUID is visible to all
+   *         registered users.
+   */
+  boolean isVisibleToAll(AccountGroup.UUID uuid);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
index ede2431..084cfe8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
@@ -45,7 +45,7 @@
       if (group == null) {
         throw new NoSuchGroupException(groupId);
       }
-      return new GroupControl(who, group);
+      return new GroupControl(who, group, groupBackend);
     }
   }
 
@@ -85,7 +85,7 @@
     }
 
     public GroupControl controlFor(GroupDescription.Basic group) {
-      return new GroupControl(user.get(), group);
+      return new GroupControl(user.get(), group, groupBackend);
     }
 
     public GroupControl validateFor(final AccountGroup.Id groupId)
@@ -110,10 +110,12 @@
   private final CurrentUser user;
   private final GroupDescription.Basic group;
   private Boolean isOwner;
+  private final GroupBackend groupBackend;
 
-  GroupControl(CurrentUser who, GroupDescription.Basic gd) {
+  GroupControl(CurrentUser who, GroupDescription.Basic gd, GroupBackend gb) {
     user = who;
     group =  gd;
+    groupBackend = gb;
   }
 
   public GroupDescription.Basic getGroup() {
@@ -126,16 +128,15 @@
 
   /** Can this user see this group exists? */
   public boolean isVisible() {
-    AccountGroup accountGroup = GroupDescriptions.toAccountGroup(group);
     /* Check for canAdministrateServer may seem redundant, but allows
      * for visibility of all groups that are not an internal group to
      * server administrators.
      */
-    return (accountGroup != null && accountGroup.isVisibleToAll())
-      || user instanceof InternalUser
+    return user instanceof InternalUser
+      || groupBackend.isVisibleToAll(group.getGroupUUID())
       || user.getEffectiveGroups().contains(group.getGroupUUID())
-      || isOwner()
-      || user.getCapabilities().canAdministrateServer();
+      || user.getCapabilities().canAdministrateServer()
+      || isOwner();
   }
 
   public boolean isOwner() {
@@ -150,11 +151,11 @@
     return isOwner;
   }
 
-  public boolean canAddMember(Account.Id id) {
+  public boolean canAddMember() {
     return isOwner();
   }
 
-  public boolean canRemoveMember(Account.Id id) {
+  public boolean canRemoveMember() {
     return isOwner();
   }
 
@@ -166,15 +167,15 @@
     return canSeeMembers();
   }
 
-  public boolean canAddGroup(AccountGroup.UUID uuid) {
+  public boolean canAddGroup() {
     return isOwner();
   }
 
-  public boolean canRemoveGroup(AccountGroup.UUID uuid) {
+  public boolean canRemoveGroup() {
     return isOwner();
   }
 
-  public boolean canSeeGroup(AccountGroup.UUID uuid) {
+  public boolean canSeeGroup() {
     return canSeeMembers();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
index 889addf..9c806de 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
@@ -92,6 +92,7 @@
     }
 
     Collections.sort(members, new Comparator<AccountGroupMember>() {
+      @Override
       public int compare(final AccountGroupMember o1,
           final AccountGroupMember o2) {
         final Account a = aic.get(o1.getAccountId());
@@ -120,13 +121,14 @@
     List<AccountGroupById> groups = new ArrayList<>();
 
     for (final AccountGroupById m : db.accountGroupById().byGroup(groupId)) {
-      if (control.canSeeGroup(m.getIncludeUUID())) {
+      if (control.canSeeGroup()) {
         gic.want(m.getIncludeUUID());
         groups.add(m);
       }
     }
 
     Collections.sort(groups, new Comparator<AccountGroupById>() {
+      @Override
       public int compare(final AccountGroupById o1,
           final AccountGroupById o2) {
         GroupDescription.Basic a = gic.get(o1.getIncludeUUID());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
index 1e3e4b1..4f8eacd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -19,11 +19,12 @@
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.AvatarInfo;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountInfo.AvatarInfo;
 import com.google.gerrit.server.avatar.AvatarProvider;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.AbstractModule;
@@ -31,10 +32,15 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import java.util.Collections;
+import java.util.EnumSet;
 import java.util.Set;
 
 @Singleton
 public class InternalAccountDirectory extends AccountDirectory {
+  static final Set<FillOptions> ID_ONLY =
+      Collections.unmodifiableSet(EnumSet.of(FillOptions.ID));
+
   public static class Module extends AbstractModule {
     @Override
     protected void configure() {
@@ -63,18 +69,26 @@
       Iterable<? extends AccountInfo> in,
       Set<FillOptions> options)
       throws DirectoryException {
+    if (options.equals(ID_ONLY)) {
+      return;
+    }
     Multimap<Account.Id, AccountInfo> missing = ArrayListMultimap.create();
     for (AccountInfo info : in) {
-      AccountState state = accountCache.getIfPresent(info._id);
+      Account.Id id = new Account.Id(info._accountId);
+      AccountState state = accountCache.getIfPresent(id);
       if (state != null) {
         fill(info, state.getAccount(), options);
       } else {
-        missing.put(info._id, info);
+        missing.put(id, info);
       }
     }
     if (!missing.isEmpty()) {
       try {
         for (Account account : db.get().accounts().get(missing.keySet())) {
+          if (options.contains(FillOptions.USERNAME)) {
+            account.setUserName(AccountState.getUserName(
+                db.get().accountExternalIds().byAccount(account.getId()).toList()));
+          }
           for (AccountInfo info : missing.get(account.getId())) {
             fill(info, account, options);
           }
@@ -88,6 +102,12 @@
   private void fill(AccountInfo info,
       Account account,
       Set<FillOptions> options) {
+    if (options.contains(FillOptions.ID)) {
+      info._accountId = account.getId().get();
+    } else {
+      // Was previously set to look up account for filling.
+      info._accountId = null;
+    }
     if (options.contains(FillOptions.NAME)) {
       info.name = Strings.emptyToNull(account.getFullName());
       if (info.name == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
index a70f942..861d3e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -90,4 +90,10 @@
   public GroupMembership membershipsOf(IdentifiedUser user) {
     return groupMembershipFactory.create(user);
   }
+
+  @Override
+  public boolean isVisibleToAll(AccountGroup.UUID uuid) {
+    GroupDescription.Internal g = get(uuid);
+    return g != null && g.getAccountGroup().isVisibleToAll();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
index 11f2e91..14c224f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
@@ -62,7 +62,7 @@
     child(ACCOUNT_KIND, "capabilities").to(Capabilities.class);
     get(ACCOUNT_KIND, "groups").to(GetGroups.class);
     get(ACCOUNT_KIND, "preferences").to(GetPreferences.class);
-    post(ACCOUNT_KIND, "preferences").to(SetPreferences.class);
+    put(ACCOUNT_KIND, "preferences").to(SetPreferences.class);
     get(ACCOUNT_KIND, "preferences.diff").to(GetDiffPreferences.class);
     put(ACCOUNT_KIND, "preferences.diff").to(SetDiffPreferences.class);
     get(CAPABILITY_KIND).to(GetCapabilities.CheckOne.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
index 86c840b..fd1f33e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
@@ -14,19 +14,17 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupById;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
 import com.google.gerrit.reviewdb.client.AccountGroupName;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -52,12 +50,13 @@
   private final PersonIdent serverIdent;
   private final GroupCache groupCache;
   private final CreateGroupArgs createGroupArgs;
+  private final AuditService auditService;
 
   @Inject
   PerformCreateGroup(ReviewDb db, AccountCache accountCache,
       GroupIncludeCache groupIncludeCache, IdentifiedUser currentUser,
       @GerritPersonIdent PersonIdent serverIdent, GroupCache groupCache,
-      @Assisted CreateGroupArgs createGroupArgs) {
+      @Assisted CreateGroupArgs createGroupArgs, AuditService auditService) {
     this.db = db;
     this.accountCache = accountCache;
     this.groupIncludeCache = groupIncludeCache;
@@ -65,6 +64,7 @@
     this.serverIdent = serverIdent;
     this.groupCache = groupCache;
     this.createGroupArgs = createGroupArgs;
+    this.auditService = auditService;
   }
 
   /**
@@ -127,18 +127,13 @@
   private void addMembers(final AccountGroup.Id groupId,
       final Collection<? extends Account.Id> members) throws OrmException {
     List<AccountGroupMember> memberships = new ArrayList<>();
-    List<AccountGroupMemberAudit> membershipsAudit = new ArrayList<>();
     for (Account.Id accountId : members) {
       final AccountGroupMember membership =
           new AccountGroupMember(new AccountGroupMember.Key(accountId, groupId));
       memberships.add(membership);
-
-      final AccountGroupMemberAudit audit = new AccountGroupMemberAudit(
-          membership, currentUser.getAccountId(), TimeUtil.nowTs());
-      membershipsAudit.add(audit);
     }
     db.accountGroupMembers().insert(memberships);
-    db.accountGroupMembersAudit().insert(membershipsAudit);
+    auditService.dispatchAddAccountsToGroup(currentUser.getAccountId(), memberships);
 
     for (Account.Id accountId : members) {
       accountCache.evict(accountId);
@@ -148,18 +143,13 @@
   private void addGroups(final AccountGroup.Id groupId,
       final Collection<? extends AccountGroup.UUID> groups) throws OrmException {
     List<AccountGroupById> includeList = new ArrayList<>();
-    List<AccountGroupByIdAud> includesAudit = new ArrayList<>();
     for (AccountGroup.UUID includeUUID : groups) {
       final AccountGroupById groupInclude =
         new AccountGroupById(new AccountGroupById.Key(groupId, includeUUID));
       includeList.add(groupInclude);
-
-      final AccountGroupByIdAud audit = new AccountGroupByIdAud(
-          groupInclude, currentUser.getAccountId(), TimeUtil.nowTs());
-      includesAudit.add(audit);
     }
     db.accountGroupById().insert(includeList);
-    db.accountGroupByIdAud().insert(includesAudit);
+    auditService.dispatchAddGroupsToGroup(currentUser.getAccountId(), includeList);
 
     for (AccountGroup.UUID uuid : groups) {
       groupIncludeCache.evictParentGroupsOf(uuid);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
index 69d16d8..c7a63e5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
@@ -29,7 +29,7 @@
 
 import java.util.Collections;
 
-@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@RequiresCapability(GlobalCapability.MODIFY_ACCOUNT)
 @Singleton
 public class PutActive implements RestModifyView<AccountResource, Input> {
   public static class Input {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
index 63c5420..8cba72e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
@@ -79,7 +79,7 @@
     String newPassword;
     if (input.generate) {
       if (self.get() != rsrc.getUser()
-          && !self.get().getCapabilities().canGenerateHttpPassword()) {
+          && !self.get().getCapabilities().canAdministrateServer()) {
         throw new AuthException("not allowed to generate HTTP password");
       }
       newPassword = generate();
@@ -93,7 +93,7 @@
     } else {
       if (!self.get().getCapabilities().canAdministrateServer()) {
         throw new AuthException("not allowed to set HTTP password directly, "
-            + "need to be Gerrit administrator");
+            + "requires the Generate HTTP Password permission");
       }
       newPassword = input.httpPassword;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
index 554bae7..601ee76 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
@@ -64,7 +64,7 @@
       throws AuthException, MethodNotAllowedException,
       ResourceNotFoundException, OrmException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
+        && !self.get().getCapabilities().canModifyAccount()) {
       throw new AuthException("not allowed to change name");
     }
     return apply(rsrc.getUser(), input);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
index 7ac987d..c49e3be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
@@ -52,7 +52,7 @@
   public Response<String> apply(AccountResource.Email rsrc, Input input)
       throws AuthException, ResourceNotFoundException, OrmException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
+        && !self.get().getCapabilities().canModifyAccount()) {
       throw new AuthException("not allowed to set preferred email address");
     }
     return apply(rsrc.getUser(), rsrc.getEmail());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
index e44d46e..8dd8de7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
@@ -16,6 +16,9 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+
+import java.util.Set;
 
 public interface Realm {
   /** Can the end-user modify this field of their own account? */
@@ -31,6 +34,12 @@
 
   public void onCreateAccount(AuthRequest who, Account account);
 
+  /** @return true if the user has the given email address. */
+  public boolean hasEmailAddress(IdentifiedUser who, String email);
+
+  /** @return all known email addresses for the identified user. */
+  public Set<String> getEmailAddresses(IdentifiedUser who);
+
   /**
    * Locate an account whose local username is the given account name.
    * <p>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
index 9b971e4..4e08756 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.extensions.client.Theme;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Theme;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
@@ -48,6 +48,7 @@
     Boolean skipUncommented;
     Boolean syntaxHighlighting;
     Boolean hideTopMenu;
+    Boolean autoHideDiffTableHeader;
     Boolean hideLineNumbers;
     Boolean renderEntireFile;
     Integer tabSize;
@@ -68,8 +69,8 @@
   public DiffPreferencesInfo apply(AccountResource rsrc, Input input)
       throws AuthException, OrmException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
-      throw new AuthException("restricted to administrator");
+        && !self.get().getCapabilities().canModifyAccount()) {
+      throw new AuthException("restricted to members of Modify Accounts");
     }
     if (input == null) {
       input = new Input();
@@ -127,6 +128,9 @@
       if (input.hideTopMenu != null) {
         p.setHideTopMenu(input.hideTopMenu);
       }
+      if (input.autoHideDiffTableHeader != null) {
+        p.setAutoHideDiffTableHeader(input.autoHideDiffTableHeader);
+      }
       if (input.hideLineNumbers != null) {
         p.setHideLineNumbers(input.hideLineNumbers);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
index c3cc636..d75c5a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
@@ -26,13 +26,11 @@
 import com.google.gerrit.extensions.webui.TopMenu;
 import com.google.gerrit.reviewdb.client.Account;
 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.ReviewCategoryStrategy;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
@@ -62,14 +60,12 @@
     public Boolean copySelfOnEmail;
     public DateFormat dateFormat;
     public TimeFormat timeFormat;
-    public Boolean reversePatchSetOrder;
     public Boolean relativeDateInChangeTable;
     public Boolean sizeBarInChangeTable;
     public Boolean legacycidInChangeTable;
-    public CommentVisibilityStrategy commentVisibilityStrategy;
+    public Boolean muteCommonPathPrefixes;
     public ReviewCategoryStrategy reviewCategoryStrategy;
     public DiffView diffView;
-    public ChangeScreen changeScreen;
     public List<TopMenu.MenuItem> my;
   }
 
@@ -95,8 +91,8 @@
       throws AuthException, ResourceNotFoundException, OrmException,
       IOException, ConfigInvalidException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
-      throw new AuthException("restricted to administrator");
+        && !self.get().getCapabilities().canModifyAccount()) {
+      throw new AuthException("restricted to members of Modify Accounts");
     }
     if (i == null) {
       i = new Input();
@@ -146,9 +142,6 @@
       if (i.timeFormat != null) {
         p.setTimeFormat(i.timeFormat);
       }
-      if (i.reversePatchSetOrder != null) {
-        p.setReversePatchSetOrder(i.reversePatchSetOrder);
-      }
       if (i.relativeDateInChangeTable != null) {
         p.setRelativeDateInChangeTable(i.relativeDateInChangeTable);
       }
@@ -158,18 +151,15 @@
       if (i.legacycidInChangeTable != null) {
         p.setLegacycidInChangeTable(i.legacycidInChangeTable);
       }
+      if (i.muteCommonPathPrefixes != null) {
+        p.setMuteCommonPathPrefixes(i.muteCommonPathPrefixes);
+      }
       if (i.reviewCategoryStrategy != null) {
         p.setReviewCategoryStrategy(i.reviewCategoryStrategy);
       }
-      if (i.commentVisibilityStrategy != null) {
-        p.setCommentVisibilityStrategy(i.commentVisibilityStrategy);
-      }
       if (i.diffView != null) {
         p.setDiffView(i.diffView);
       }
-      if (i.changeScreen != null) {
-        p.setChangeScreen(i.changeScreen);
-      }
 
       db.get().accounts().update(Collections.singleton(a));
       db.get().commit();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
index b94158f..b35c03e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
@@ -55,7 +55,7 @@
   public AccountResource.SshKey parse(AccountResource rsrc, IdString id)
       throws ResourceNotFoundException, OrmException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
+        && !self.get().getCapabilities().canModifyAccount()) {
       throw new ResourceNotFoundException();
     }
     return parse(rsrc.getUser(), id);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SuggestAccounts.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SuggestAccounts.java
new file mode 100644
index 0000000..07936d9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SuggestAccounts.java
@@ -0,0 +1,156 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.Ordering;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+import org.kohsuke.args4j.Option;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+class SuggestAccounts implements RestReadView<TopLevelResource> {
+  private static final int MAX_RESULTS = 100;
+  private static final String MAX_SUFFIX = "\u9fa5";
+
+  private final AccountControl accountControl;
+  private final AccountLoader accountLoader;
+  private final ReviewDb db;
+  private final boolean suggest;
+  private final int suggestFrom;
+
+  private int limit = 10;
+
+  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of users to return")
+  void setLimit(int n) {
+    if (n < 0) {
+      limit = 10;
+    } else if (n == 0) {
+      limit = MAX_RESULTS;
+    } else {
+      limit = Math.min(n, MAX_RESULTS);
+    }
+  }
+
+  @Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", usage = "match users")
+  private String query;
+
+  @Inject
+  SuggestAccounts(AccountControl.Factory accountControlFactory,
+      AccountLoader.Factory accountLoaderFactory,
+      ReviewDb db,
+      @GerritServerConfig Config cfg) {
+    accountControl = accountControlFactory.get();
+    accountLoader = accountLoaderFactory.create(true);
+    this.db = db;
+    this.suggestFrom = cfg.getInt("suggest", null, "from", 0);
+
+    if ("off".equalsIgnoreCase(cfg.getString("suggest", null, "accounts"))) {
+      suggest = false;
+    } else {
+      boolean suggest;
+      try {
+        AccountVisibility av =
+            cfg.getEnum("suggest", null, "accounts", AccountVisibility.ALL);
+        suggest = (av != AccountVisibility.NONE);
+      } catch (IllegalArgumentException err) {
+        suggest = cfg.getBoolean("suggest", null, "accounts", true);
+      }
+      this.suggest = suggest;
+    }
+  }
+
+  @Override
+  public List<AccountInfo> apply(TopLevelResource rsrc)
+      throws OrmException, BadRequestException {
+    if (Strings.isNullOrEmpty(query)) {
+      throw new BadRequestException("missing query field");
+    }
+
+    if (!suggest || query.length() < suggestFrom) {
+      return Collections.emptyList();
+    }
+
+    String a = query;
+    String b = a + MAX_SUFFIX;
+
+    Map<Account.Id, AccountInfo> matches = new LinkedHashMap<>();
+    Map<Account.Id, String> queryEmail = new HashMap<>();
+
+    for (Account p : db.accounts().suggestByFullName(a, b, limit)) {
+      addSuggestion(matches, p.getId());
+    }
+    if (matches.size() < limit) {
+      for (Account p : db.accounts()
+          .suggestByPreferredEmail(a, b, limit - matches.size())) {
+        addSuggestion(matches, p.getId());
+      }
+    }
+    if (matches.size() < limit) {
+      for (AccountExternalId e : db.accountExternalIds()
+          .suggestByEmailAddress(a, b, limit - matches.size())) {
+        if (addSuggestion(matches, e.getAccountId())) {
+          queryEmail.put(e.getAccountId(), e.getEmailAddress());
+        }
+      }
+    }
+
+    accountLoader.fill();
+    for (Map.Entry<Account.Id, String> p : queryEmail.entrySet()) {
+      AccountInfo info = matches.get(p.getKey());
+      if (info != null) {
+        info.email = p.getValue();
+      }
+    }
+
+    List<AccountInfo> m = new ArrayList<>(matches.values());
+    Collections.sort(m, new Comparator<AccountInfo>() {
+      @Override
+      public int compare(AccountInfo a, AccountInfo b) {
+        return ComparisonChain.start()
+          .compare(a.name, b.name, Ordering.natural().nullsLast())
+          .compare(a.email, b.email, Ordering.natural().nullsLast())
+          .result();
+      }
+    });
+    return m;
+  }
+
+  private boolean addSuggestion(Map<Account.Id, AccountInfo> map, Account.Id id) {
+    if (!map.containsKey(id) && accountControl.canSee(id)) {
+      map.put(id, accountLoader.get(id));
+      return true;
+    }
+    return false;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
index 1748395d..9b32ca9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -185,4 +185,14 @@
       return groups;
     }
   }
+
+  @Override
+  public boolean isVisibleToAll(AccountGroup.UUID uuid) {
+    for (GroupBackend g : backends) {
+      if (g.handles(uuid)) {
+        return g.isVisibleToAll(uuid);
+      }
+    }
+    return false;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index b1fd979..44413b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -15,10 +15,11 @@
 package com.google.gerrit.server.api.accounts;
 
 import com.google.gerrit.extensions.api.accounts.AccountApi;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.StarredChanges;
 import com.google.gerrit.server.change.ChangeResource;
@@ -34,12 +35,12 @@
 
   private final AccountResource account;
   private final ChangesCollection changes;
-  private final AccountInfo.Loader.Factory accountLoaderFactory;
+  private final AccountLoader.Factory accountLoaderFactory;
   private final StarredChanges.Create starredChangesCreate;
   private final StarredChanges.Delete starredChangesDelete;
 
   @Inject
-  AccountApiImpl(AccountInfo.Loader.Factory ailf,
+  AccountApiImpl(AccountLoader.Factory ailf,
       ChangesCollection changes,
       StarredChanges.Create starredChangesCreate,
       StarredChanges.Delete starredChangesDelete,
@@ -54,11 +55,11 @@
   @Override
   public com.google.gerrit.extensions.common.AccountInfo get()
       throws RestApiException {
-    AccountInfo.Loader accountLoader = accountLoaderFactory.create(true);
+    AccountLoader accountLoader = accountLoaderFactory.create(true);
     try {
       AccountInfo ai = accountLoader.get(account.getUser().getAccountId());
       accountLoader.fill();
-      return AccountInfoMapper.fromAcountInfo(ai);
+      return ai;
     } catch (OrmException e) {
       throw new RestApiException("Cannot parse change", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountInfoMapper.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountInfoMapper.java
deleted file mode 100644
index 10a9116..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountInfoMapper.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2014 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.api.accounts;
-
-import com.google.gerrit.extensions.common.AccountInfo;
-
-public class AccountInfoMapper {
-  public static AccountInfo fromAcountInfo(
-      com.google.gerrit.server.account.AccountInfo i) {
-    if (i == null) {
-      return null;
-    }
-    AccountInfo ai = new AccountInfo();
-    fromAccount(i, ai);
-    return ai;
-  }
-
-  public static void fromAccount(
-      com.google.gerrit.server.account.AccountInfo i, AccountInfo ai) {
-    ai._accountId = i._accountId;
-    ai.email = i.email;
-    ai.name = i.name;
-    ai.username = i.username;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 5f90d34..161461d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -19,20 +19,33 @@
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.Changes;
+import com.google.gerrit.extensions.api.changes.FixInput;
+import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.api.changes.RestoreInput;
 import com.google.gerrit.extensions.api.changes.RevertInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.common.EditInfo;
+import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.change.Abandon;
+import com.google.gerrit.server.change.ChangeEdits;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.Check;
+import com.google.gerrit.server.change.GetHashtags;
+import com.google.gerrit.server.change.GetTopic;
+import com.google.gerrit.server.change.PostHashtags;
 import com.google.gerrit.server.change.PostReviewers;
+import com.google.gerrit.server.change.PutTopic;
 import com.google.gerrit.server.change.Restore;
 import com.google.gerrit.server.change.Revert;
 import com.google.gerrit.server.change.Revisions;
+import com.google.gerrit.server.change.SuggestReviewers;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -40,6 +53,8 @@
 
 import java.io.IOException;
 import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
 
 class ChangeApiImpl extends ChangeApi.NotImplemented implements ChangeApi {
   interface Factory {
@@ -49,31 +64,52 @@
   private final Changes changeApi;
   private final Revisions revisions;
   private final RevisionApiImpl.Factory revisionApi;
+  private final Provider<SuggestReviewers> suggestReviewers;
   private final ChangeResource change;
   private final Abandon abandon;
   private final Revert revert;
   private final Restore restore;
-  private final Provider<PostReviewers> postReviewers;
+  private final GetTopic getTopic;
+  private final PutTopic putTopic;
+  private final PostReviewers postReviewers;
   private final Provider<ChangeJson> changeJson;
+  private final PostHashtags postHashtags;
+  private final GetHashtags getHashtags;
+  private final Check check;
+  private final ChangeEdits.Detail editDetail;
 
   @Inject
   ChangeApiImpl(Changes changeApi,
       Revisions revisions,
       RevisionApiImpl.Factory revisionApi,
+      Provider<SuggestReviewers> suggestReviewers,
       Abandon abandon,
       Revert revert,
       Restore restore,
-      Provider<PostReviewers> postReviewers,
+      GetTopic getTopic,
+      PutTopic putTopic,
+      PostReviewers postReviewers,
       Provider<ChangeJson> changeJson,
+      PostHashtags postHashtags,
+      GetHashtags getHashtags,
+      Check check,
+      ChangeEdits.Detail editDetail,
       @Assisted ChangeResource change) {
     this.changeApi = changeApi;
     this.revert = revert;
     this.revisions = revisions;
     this.revisionApi = revisionApi;
+    this.suggestReviewers = suggestReviewers;
     this.abandon = abandon;
     this.restore = restore;
+    this.getTopic = getTopic;
+    this.putTopic = putTopic;
     this.postReviewers = postReviewers;
     this.changeJson = changeJson;
+    this.postHashtags = postHashtags;
+    this.getHashtags = getHashtags;
+    this.check = check;
+    this.editDetail = editDetail;
     this.change = change;
   }
 
@@ -97,7 +133,7 @@
     try {
       return revisionApi.create(
           revisions.parse(change, IdString.fromDecoded(id)));
-    } catch (OrmException e) {
+    } catch (OrmException | IOException e) {
       throw new RestApiException("Cannot parse revision", e);
     }
   }
@@ -145,6 +181,22 @@
   }
 
   @Override
+  public String topic() throws RestApiException {
+    return getTopic.apply(change);
+  }
+
+  @Override
+  public void topic(String topic) throws RestApiException {
+    PutTopic.Input in = new PutTopic.Input();
+    in.topic = topic;
+    try {
+      putTopic.apply(change, in);
+    } catch (OrmException | IOException e) {
+      throw new RestApiException("Cannot set topic", e);
+    }
+  }
+
+  @Override
   public void addReviewer(String reviewer) throws RestApiException {
     AddReviewerInput in = new AddReviewerInput();
     in.reviewer = reviewer;
@@ -154,18 +206,45 @@
   @Override
   public void addReviewer(AddReviewerInput in) throws RestApiException {
     try {
-      postReviewers.get().apply(change, in);
+      postReviewers.apply(change, in);
     } catch (OrmException | EmailException | IOException e) {
       throw new RestApiException("Cannot add change reviewer", e);
     }
   }
 
   @Override
+  public SuggestedReviewersRequest suggestReviewers() throws RestApiException {
+    return new SuggestedReviewersRequest() {
+      @Override
+      public List<SuggestedReviewerInfo> get() throws RestApiException {
+        return ChangeApiImpl.this.suggestReviewers(this);
+      }
+    };
+  }
+
+  @Override
+  public SuggestedReviewersRequest suggestReviewers(String query)
+      throws RestApiException {
+    return suggestReviewers().withQuery(query);
+  }
+
+  private List<SuggestedReviewerInfo> suggestReviewers(SuggestedReviewersRequest r)
+      throws RestApiException {
+    try {
+      SuggestReviewers mySuggestReviewers = suggestReviewers.get();
+      mySuggestReviewers.setQuery(r.getQuery());
+      mySuggestReviewers.setLimit(r.getLimit());
+      return mySuggestReviewers.apply(change);
+    } catch (OrmException | IOException e) {
+      throw new RestApiException("Cannot retrieve suggested reviewers", e);
+    }
+  }
+
+  @Override
   public ChangeInfo get(EnumSet<ListChangesOption> s)
       throws RestApiException {
     try {
-      return ChangeInfoMapper.INSTANCE.apply(
-          changeJson.get().addOptions(s).format(change));
+      return changeJson.get().addOptions(s).format(change);
     } catch (OrmException e) {
       throw new RestApiException("Cannot retrieve change", e);
     }
@@ -173,11 +252,57 @@
 
   @Override
   public ChangeInfo get() throws RestApiException {
-    return get(EnumSet.allOf(ListChangesOption.class));
+    return get(EnumSet.complementOf(EnumSet.of(ListChangesOption.CHECK)));
+  }
+
+  @Override
+  public EditInfo getEdit() throws RestApiException {
+    try {
+      Response<EditInfo> edit = editDetail.apply(change);
+      return edit.isNone() ? null : edit.value();
+    } catch (IOException | OrmException | InvalidChangeOperationException e) {
+      throw new RestApiException("Cannot retrieve change edit", e);
+    }
   }
 
   @Override
   public ChangeInfo info() throws RestApiException {
     return get(EnumSet.noneOf(ListChangesOption.class));
   }
+
+  @Override
+  public void setHashtags(HashtagsInput input) throws RestApiException {
+    try {
+      postHashtags.apply(change, input);
+    } catch (IOException | OrmException e) {
+      throw new RestApiException("Cannot post hashtags", e);
+    }
+  }
+
+  @Override
+  public Set<String> getHashtags() throws RestApiException {
+    try {
+      return getHashtags.apply(change).value();
+    } catch (IOException | OrmException e) {
+      throw new RestApiException("Cannot get hashtags", e);
+    }
+  }
+
+  @Override
+  public ChangeInfo check() throws RestApiException {
+    try {
+      return check.apply(change).value();
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot check change", e);
+    }
+  }
+
+  @Override
+  public ChangeInfo check(FixInput fix) throws RestApiException {
+    try {
+      return check.apply(change, fix).value();
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot check change", e);
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeInfoMapper.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeInfoMapper.java
deleted file mode 100644
index 8e6c20b..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeInfoMapper.java
+++ /dev/null
@@ -1,142 +0,0 @@
-// Copyright (C) 2014 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.api.changes;
-
-import com.google.common.base.Function;
-import com.google.common.collect.EnumBiMap;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.gerrit.extensions.common.ApprovalInfo;
-import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.ChangeMessageInfo;
-import com.google.gerrit.extensions.common.ChangeStatus;
-import com.google.gerrit.extensions.common.LabelInfo;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Change.Status;
-import com.google.gerrit.server.api.accounts.AccountInfoMapper;
-import com.google.gerrit.server.change.ChangeJson;
-
-import java.util.List;
-import java.util.Map;
-
-public class ChangeInfoMapper
-    implements Function<ChangeJson.ChangeInfo, ChangeInfo> {
-  public static final ChangeInfoMapper INSTANCE = new ChangeInfoMapper();
-
-  private final static EnumBiMap<Change.Status, ChangeStatus> STATUS_MAP =
-      EnumBiMap.create(Change.Status.class, ChangeStatus.class);
-  static {
-    STATUS_MAP.put(Status.DRAFT, ChangeStatus.DRAFT);
-    STATUS_MAP.put(Status.NEW, ChangeStatus.NEW);
-    STATUS_MAP.put(Status.SUBMITTED, ChangeStatus.SUBMITTED);
-    STATUS_MAP.put(Status.MERGED, ChangeStatus.MERGED);
-    STATUS_MAP.put(Status.ABANDONED, ChangeStatus.ABANDONED);
-  }
-
-  public static Status changeStatus2Status(ChangeStatus status) {
-    if (status != null) {
-      return STATUS_MAP.inverse().get(status);
-    }
-    return Change.Status.NEW;
-  }
-
-  private ChangeInfoMapper() {
-  }
-
-  @Override
-  public ChangeInfo apply(ChangeJson.ChangeInfo i) {
-    ChangeInfo o = new ChangeInfo();
-    mapCommon(i, o);
-    mapLabels(i, o);
-    mapMessages(i, o);
-    o.revisions = i.revisions;
-    o.actions = i.actions;
-    return o;
-  }
-
-  private void mapCommon(ChangeJson.ChangeInfo i, ChangeInfo o) {
-    o.id = i.id;
-    o.project = i.project;
-    o.branch = i.branch;
-    o.topic = i.topic;
-    o.changeId = i.changeId;
-    o.subject = i.subject;
-    o.status = STATUS_MAP.get(i.status);
-    o.created = i.created;
-    o.updated = i.updated;
-    o.starred = i.starred;
-    o.reviewed = i.reviewed;
-    o.mergeable = i.mergeable;
-    o.insertions = i.insertions;
-    o.deletions = i.deletions;
-    o.owner = AccountInfoMapper.fromAcountInfo(i.owner);
-    o.currentRevision = i.currentRevision;
-    o._number = i._number;
-  }
-
-  private void mapMessages(ChangeJson.ChangeInfo i, ChangeInfo o) {
-    if (i.messages == null) {
-      return;
-    }
-    List<ChangeMessageInfo> r =
-        Lists.newArrayListWithCapacity(i.messages.size());
-    for (ChangeJson.ChangeMessageInfo m : i.messages) {
-      ChangeMessageInfo cmi = new ChangeMessageInfo();
-      cmi.id = m.id;
-      cmi.author = AccountInfoMapper.fromAcountInfo(m.author);
-      cmi.date = m.date;
-      cmi.message = m.message;
-      cmi._revisionNumber = m._revisionNumber;
-      r.add(cmi);
-    }
-    o.messages = r;
-  }
-
-  private void mapLabels(ChangeJson.ChangeInfo i, ChangeInfo o) {
-    if (i.labels == null) {
-      return;
-    }
-    Map<String, LabelInfo> r = Maps.newLinkedHashMap();
-    for (Map.Entry<String, ChangeJson.LabelInfo> e : i.labels.entrySet()) {
-      ChangeJson.LabelInfo li = e.getValue();
-      LabelInfo lo = new LabelInfo();
-      lo.approved = AccountInfoMapper.fromAcountInfo(li.approved);
-      lo.rejected = AccountInfoMapper.fromAcountInfo(li.rejected);
-      lo.recommended = AccountInfoMapper.fromAcountInfo(li.recommended);
-      lo.disliked = AccountInfoMapper.fromAcountInfo(li.disliked);
-      lo.value = li.value;
-      lo.defaultValue = li.defaultValue;
-      lo.optional = li.optional;
-      lo.blocking = li.blocking;
-      lo.values = li.values;
-      if (li.all != null) {
-        lo.all = Lists.newArrayListWithExpectedSize(li.all.size());
-        for (ChangeJson.ApprovalInfo ai : li.all) {
-          lo.all.add(fromApprovalInfo(ai));
-        }
-      }
-      r.put(e.getKey(), lo);
-    }
-    o.labels = r;
-  }
-
-  private static ApprovalInfo fromApprovalInfo(ChangeJson.ApprovalInfo ai) {
-    ApprovalInfo ao = new ApprovalInfo();
-    ao.value = ai.value;
-    ao.date = ai.date;
-    AccountInfoMapper.fromAccount(ai, ao);
-    return ao;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
index db72c9c..91809ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
@@ -19,18 +19,16 @@
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.Changes;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.ListChangesOption;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.CreateChange;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
@@ -89,7 +87,7 @@
   @Override
   public ChangeApi create(ChangeInfo in) throws RestApiException {
     try {
-      ChangeJson.ChangeInfo out = createChange.apply(
+      ChangeInfo out = createChange.apply(
           TopLevelResource.INSTANCE, in).value();
       return api.create(changes.parse(TopLevelResource.INSTANCE,
           IdString.fromUrl(out.changeId)));
@@ -133,12 +131,11 @@
       // Check type safety of result; the extension API should be safer than the
       // REST API in this case, since it's intended to be used in Java.
       Object first = checkNotNull(result.iterator().next());
-      checkState(first instanceof ChangeJson.ChangeInfo);
+      checkState(first instanceof ChangeInfo);
       @SuppressWarnings("unchecked")
-      List<ChangeJson.ChangeInfo> infos = (List<ChangeJson.ChangeInfo>) result;
+      List<ChangeInfo> infos = (List<ChangeInfo>) result;
 
-      return ImmutableList.copyOf(
-          Lists.transform(infos, ChangeInfoMapper.INSTANCE));
+      return ImmutableList.copyOf(infos);
     } catch (BadRequestException | AuthException | OrmException e) {
       throw new RestApiException("Cannot query changes", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/CommentApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/CommentApiImpl.java
new file mode 100644
index 0000000..0352aff
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/CommentApiImpl.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.api.changes;
+
+import com.google.gerrit.extensions.api.changes.CommentApi;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.change.CommentResource;
+import com.google.gerrit.server.change.GetComment;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+class CommentApiImpl implements CommentApi {
+  interface Factory {
+    CommentApiImpl create(CommentResource c);
+  }
+
+  private final GetComment getComment;
+  private final CommentResource comment;
+
+  @Inject
+  CommentApiImpl(GetComment getComment,
+      @Assisted CommentResource comment) {
+    this.getComment = getComment;
+    this.comment = comment;
+  }
+
+  @Override
+  public CommentInfo get() throws RestApiException {
+    try {
+      return getComment.apply(comment);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot retrieve comment", e);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/DraftApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/DraftApiImpl.java
new file mode 100644
index 0000000..647f577
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/DraftApiImpl.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.api.changes;
+
+import com.google.gerrit.extensions.api.changes.DraftApi;
+import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.change.DeleteDraftComment;
+import com.google.gerrit.server.change.DraftCommentResource;
+import com.google.gerrit.server.change.GetDraftComment;
+import com.google.gerrit.server.change.PutDraftComment;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.io.IOException;
+
+class DraftApiImpl implements DraftApi {
+  interface Factory {
+    DraftApiImpl create(DraftCommentResource d);
+  }
+
+  private final DeleteDraftComment deleteDraft;
+  private final GetDraftComment getDraft;
+  private final PutDraftComment putDraft;
+  private final DraftCommentResource draft;
+
+  @Inject
+  DraftApiImpl(DeleteDraftComment deleteDraft,
+      GetDraftComment getDraft,
+      PutDraftComment putDraft,
+      @Assisted DraftCommentResource draft) {
+    this.deleteDraft = deleteDraft;
+    this.getDraft = getDraft;
+    this.putDraft = putDraft;
+    this.draft = draft;
+  }
+
+  @Override
+  public CommentInfo get() throws RestApiException {
+    try {
+      return getDraft.apply(draft);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot retrieve draft", e);
+    }
+  }
+
+  @Override
+  public CommentInfo update(DraftInput in) throws RestApiException {
+    try {
+      return putDraft.apply(draft, in).value();
+    } catch (IOException | OrmException e) {
+      throw new RestApiException("Cannot update draft", e);
+    }
+  }
+
+  @Override
+  public void delete() throws RestApiException {
+    try {
+      deleteDraft.apply(draft, null);
+    } catch (IOException | OrmException e) {
+      throw new RestApiException("Cannot delete draft", e);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
new file mode 100644
index 0000000..42c1e23
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.api.changes;
+
+import com.google.gerrit.extensions.api.changes.FileApi;
+import com.google.gerrit.extensions.common.DiffInfo;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.change.FileResource;
+import com.google.gerrit.server.change.GetContent;
+import com.google.gerrit.server.change.GetDiff;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+
+import java.io.IOException;
+
+class FileApiImpl extends FileApi.NotImplemented implements FileApi {
+  interface Factory {
+    FileApiImpl create(FileResource r);
+  }
+
+  private final GetContent getContent;
+  private final Provider<GetDiff> getDiff;
+  private final FileResource file;
+
+  @Inject
+  FileApiImpl(GetContent getContent,
+      Provider<GetDiff> getDiff,
+      @Assisted FileResource file) {
+    this.getContent = getContent;
+    this.getDiff = getDiff;
+    this.file = file;
+  }
+
+  @Override
+  public BinaryResult content() throws RestApiException {
+    try {
+      return getContent.apply(file);
+    } catch (NoSuchChangeException | IOException | OrmException e) {
+      throw new RestApiException("Cannot retrieve file content", e);
+    }
+  }
+
+  @Override
+  public DiffInfo diff() throws RestApiException {
+    try {
+      return getDiff.get().apply(file).value();
+    } catch (IOException | InvalidChangeOperationException | OrmException e) {
+      throw new RestApiException("Cannot retrieve diff", e);
+    }
+  }
+
+  @Override
+  public DiffInfo diff(String base) throws RestApiException {
+    try {
+      return getDiff.get().setBase(base).apply(file).value();
+    } catch (IOException | InvalidChangeOperationException | OrmException e) {
+      throw new RestApiException("Cannot retrieve diff", e);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java
index dbf5f27..a37f8be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/Module.java
@@ -23,6 +23,9 @@
     bind(Changes.class).to(ChangesImpl.class);
 
     factory(ChangeApiImpl.Factory.class);
+    factory(CommentApiImpl.Factory.class);
+    factory(DraftApiImpl.Factory.class);
     factory(RevisionApiImpl.Factory.class);
+    factory(FileApiImpl.Factory.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index c709b34..c36faa2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -19,18 +19,32 @@
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.Changes;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
+import com.google.gerrit.extensions.api.changes.CommentApi;
+import com.google.gerrit.extensions.api.changes.DraftApi;
+import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.api.changes.FileApi;
+import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.common.FileInfo;
+import com.google.gerrit.extensions.common.MergeableInfo;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.change.CherryPick;
+import com.google.gerrit.server.change.Comments;
+import com.google.gerrit.server.change.CreateDraftComment;
 import com.google.gerrit.server.change.DeleteDraftPatchSet;
+import com.google.gerrit.server.change.DraftComments;
 import com.google.gerrit.server.change.FileResource;
 import com.google.gerrit.server.change.Files;
+import com.google.gerrit.server.change.ListComments;
+import com.google.gerrit.server.change.ListDraftComments;
+import com.google.gerrit.server.change.Mergeable;
 import com.google.gerrit.server.change.PostReview;
-import com.google.gerrit.server.change.Publish;
+import com.google.gerrit.server.change.PublishDraftPatchSet;
 import com.google.gerrit.server.change.Rebase;
 import com.google.gerrit.server.change.Reviewed;
 import com.google.gerrit.server.change.RevisionResource;
@@ -42,6 +56,8 @@
 import com.google.inject.assistedinject.Assisted;
 
 import java.io.IOException;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 class RevisionApiImpl extends RevisionApi.NotImplemented implements RevisionApi {
@@ -55,13 +71,22 @@
   private final Rebase rebase;
   private final RebaseChange rebaseChange;
   private final Submit submit;
-  private final Publish publish;
+  private final PublishDraftPatchSet publish;
   private final Reviewed.PutReviewed putReviewed;
   private final Reviewed.DeleteReviewed deleteReviewed;
   private final RevisionResource revision;
   private final Provider<Files> files;
   private final Provider<Files.ListFiles> listFiles;
   private final Provider<PostReview> review;
+  private final Provider<Mergeable> mergeable;
+  private final FileApiImpl.Factory fileApi;
+  private final ListComments listComments;
+  private final ListDraftComments listDrafts;
+  private final CreateDraftComment createDraft;
+  private final DraftComments drafts;
+  private final DraftApiImpl.Factory draftFactory;
+  private final Comments comments;
+  private final CommentApiImpl.Factory commentFactory;
 
   @Inject
   RevisionApiImpl(Changes changes,
@@ -70,12 +95,21 @@
       Rebase rebase,
       RebaseChange rebaseChange,
       Submit submit,
-      Publish publish,
+      PublishDraftPatchSet publish,
       Reviewed.PutReviewed putReviewed,
       Reviewed.DeleteReviewed deleteReviewed,
       Provider<Files> files,
       Provider<Files.ListFiles> listFiles,
       Provider<PostReview> review,
+      Provider<Mergeable> mergeable,
+      FileApiImpl.Factory fileApi,
+      ListComments listComments,
+      ListDraftComments listDrafts,
+      CreateDraftComment createDraft,
+      DraftComments drafts,
+      DraftApiImpl.Factory draftFactory,
+      Comments comments,
+      CommentApiImpl.Factory commentFactory,
       @Assisted RevisionResource r) {
     this.changes = changes;
     this.cherryPick = cherryPick;
@@ -89,6 +123,15 @@
     this.putReviewed = putReviewed;
     this.deleteReviewed = deleteReviewed;
     this.listFiles = listFiles;
+    this.mergeable = mergeable;
+    this.fileApi = fileApi;
+    this.listComments = listComments;
+    this.listDrafts = listDrafts;
+    this.createDraft = createDraft;
+    this.drafts = drafts;
+    this.draftFactory = draftFactory;
+    this.comments = comments;
+    this.commentFactory = commentFactory;
     this.revision = r;
   }
 
@@ -120,7 +163,7 @@
   @Override
   public void publish() throws RestApiException {
     try {
-      publish.apply(revision, new Publish.Input());
+      publish.apply(revision, new PublishDraftPatchSet.Input());
     } catch (OrmException | IOException e) {
       throw new RestApiException("Cannot publish draft patch set", e);
     }
@@ -137,8 +180,14 @@
 
   @Override
   public ChangeApi rebase() throws RestApiException {
+    RebaseInput in = new RebaseInput();
+    return rebase(in);
+  }
+
+  @Override
+  public ChangeApi rebase(RebaseInput in) throws RestApiException {
     try {
-      return changes.id(rebase.apply(revision, null)._number);
+      return changes.id(rebase.apply(revision, in)._number);
     } catch (OrmException | EmailException e) {
       throw new RestApiException("Cannot rebase ps", e);
     }
@@ -182,8 +231,102 @@
       return ImmutableSet.copyOf((Iterable<String>) listFiles
           .get().setReviewed(true)
           .apply(revision).value());
-    } catch (OrmException e) {
+    } catch (OrmException | IOException e) {
       throw new RestApiException("Cannot list reviewed files", e);
     }
   }
+
+  @Override
+  public MergeableInfo mergeable() throws RestApiException {
+    try {
+      return mergeable.get().apply(revision);
+    } catch (OrmException | IOException e) {
+      throw new RestApiException("Cannot check mergeability", e);
+    }
+  }
+
+  @Override
+  public MergeableInfo mergeableOtherBranches() throws RestApiException {
+    try {
+      Mergeable m = mergeable.get();
+      m.setOtherBranches(true);
+      return m.apply(revision);
+    } catch (OrmException | IOException e) {
+      throw new RestApiException("Cannot check mergeability", e);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public Map<String, FileInfo> files() throws RestApiException {
+    try {
+      return (Map<String, FileInfo>)listFiles.get().apply(revision).value();
+    } catch (OrmException | IOException e) {
+      throw new RestApiException("Cannot retrieve files", e);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public Map<String, FileInfo> files(String base) throws RestApiException {
+    try {
+      return (Map<String, FileInfo>) listFiles.get().setBase(base)
+          .apply(revision).value();
+    } catch (OrmException | IOException e) {
+      throw new RestApiException("Cannot retrieve files", e);
+    }
+  }
+
+  @Override
+  public FileApi file(String path) {
+    return fileApi.create(files.get().parse(revision,
+        IdString.fromDecoded(path)));
+  }
+
+  @Override
+  public Map<String, List<CommentInfo>> comments() throws RestApiException {
+    try {
+      return listComments.apply(revision);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot retrieve comments", e);
+    }
+  }
+
+  @Override
+  public Map<String, List<CommentInfo>> drafts() throws RestApiException {
+    try {
+      return listDrafts.apply(revision);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot retrieve drafts", e);
+    }
+  }
+
+  @Override
+  public DraftApi draft(String id) throws RestApiException {
+    try {
+      return draftFactory.create(drafts.parse(revision,
+          IdString.fromDecoded(id)));
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot retrieve draft", e);
+    }
+  }
+
+  @Override
+  public DraftApi createDraft(DraftInput in) throws RestApiException {
+    try {
+      return draft(createDraft.apply(revision, in).value().id);
+    } catch (IOException | OrmException e) {
+      throw new RestApiException("Cannot create draft", e);
+    }
+  }
+
+  @Override
+  public CommentApi comment(String id) throws RestApiException {
+    try {
+      return commentFactory.create(comments.parse(revision,
+          IdString.fromDecoded(id)));
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot retrieve comment", e);
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
index 9c3d052..1cbab8a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
@@ -17,9 +17,11 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
 import org.kohsuke.args4j.CmdLineException;
@@ -30,17 +32,16 @@
 import org.kohsuke.args4j.spi.Setter;
 
 public class ChangeIdHandler extends OptionHandler<Change.Id> {
-
-  @Inject
-  private ReviewDb db;
+  private final Provider<InternalChangeQuery> queryProvider;
 
   @Inject
   public ChangeIdHandler(
-      final ReviewDb db,
+      // TODO(dborowitz): Not sure whether this is injectable here.
+      Provider<InternalChangeQuery> queryProvider,
       @Assisted final CmdLineParser parser, @Assisted final OptionDef option,
       @Assisted final Setter<Change.Id> setter) {
     super(parser, option, setter);
-    this.db = db;
+    this.queryProvider = queryProvider;
   }
 
   @Override
@@ -58,8 +59,8 @@
       final Project.NameKey project = new Project.NameKey(tokens[0]);
       final Branch.NameKey branch =
           new Branch.NameKey(project, "refs/heads/" + tokens[1]);
-      for (final Change change : db.changes().byBranchKey(branch, key)) {
-        setter.addValue(change.getId());
+      for (final ChangeData cd : queryProvider.get().byBranchKey(branch, key)) {
+        setter.addValue(cd.getId());
         return 1;
       }
     } catch (IllegalArgumentException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/TimestampHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/TimestampHandler.java
index 8dd4270..21ef31a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/TimestampHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/TimestampHandler.java
@@ -31,7 +31,7 @@
 import java.util.TimeZone;
 
 public class TimestampHandler extends OptionHandler<Timestamp> {
-  public final static String TIMESTAMP_FORMAT = "yyyyMMdd_HHmm";
+  public static final String TIMESTAMP_FORMAT = "yyyyMMdd_HHmm";
 
   @Inject
   public TimestampHandler(@Assisted CmdLineParser parser,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthRequest.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthRequest.java
index 09ab56b..e194eb7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthRequest.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthRequest.java
@@ -14,9 +14,10 @@
 
 package com.google.gerrit.server.auth;
 
-import com.google.common.base.Objects;
 import com.google.gerrit.common.Nullable;
 
+import java.util.Objects;
+
 /**
  * Defines an abstract request for user authentication to Gerrit.
  */
@@ -50,7 +51,7 @@
   }
 
   public void checkPassword(String pwd) throws AuthException {
-    if (!Objects.equal(getPassword(), pwd)) {
+    if (!Objects.equals(getPassword(), pwd)) {
       throw new InvalidCredentialsException();
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthUser.java
index 65f1f58..f2c8222 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthUser.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
+import com.google.auto.value.AutoValue;
 import com.google.gerrit.common.Nullable;
 
 /**
@@ -26,40 +27,19 @@
   /**
    * Globally unique identifier for the user.
    */
-  public static final class UUID {
-    private final String uuid;
-
+  @AutoValue
+  public abstract static class UUID {
     /**
      * A new unique identifier.
      *
      * @param uuid the unique identifier.
+     * @return identifier instance.
      */
-    public UUID(String uuid) {
-      this.uuid = checkNotNull(uuid);
+    public static UUID create(String uuid) {
+      return new AutoValue_AuthUser_UUID(uuid);
     }
 
-    /** @return the globally unique identifier. */
-    public String get() {
-      return uuid;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-      if (obj instanceof UUID) {
-        return get().equals(((UUID) obj).get());
-      }
-      return false;
-    }
-
-    @Override
-    public int hashCode() {
-      return get().hashCode();
-    }
-
-    @Override
-    public String toString() {
-      return String.format("AuthUser.UUID[%s]", get());
-    }
+    public abstract String uuid();
   }
 
   private final UUID uuid;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/InternalAuthBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/InternalAuthBackend.java
index 0b4baf2..6ecea5e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/InternalAuthBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/InternalAuthBackend.java
@@ -64,6 +64,6 @@
     }
 
     req.checkPassword(who.getPassword(username));
-    return new AuthUser(new AuthUser.UUID(username), username);
+    return new AuthUser(AuthUser.UUID.create(username), username);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/UniversalAuthBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/UniversalAuthBackend.java
index 689c94c..87c8af2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/UniversalAuthBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/UniversalAuthBackend.java
@@ -18,12 +18,11 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import java.util.List;
 
-import javax.inject.Inject;
-import javax.inject.Singleton;
-
 /**
  * Universal implementation of the AuthBackend that works with the injected
  * set of AuthBackends.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
index 618aa7d..4c29c9b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
@@ -70,6 +70,7 @@
   private final String readTimeoutMillis;
   private final String connectTimeoutMillis;
   private final boolean useConnectionPooling;
+  private final boolean groupsVisibleToAll;
 
   @Inject
   Helper(@GerritServerConfig final Config config,
@@ -81,6 +82,7 @@
     this.password = LdapRealm.optional(config, "password", "");
     this.referral = LdapRealm.optional(config, "referral", "ignore");
     this.sslVerify = config.getBoolean("ldap", "sslverify", true);
+    this.groupsVisibleToAll = config.getBoolean("ldap", "groupsVisibleToAll", false);
     this.authentication =
         LdapRealm.optional(config, "authentication", "simple");
     String readTimeout = LdapRealm.optional(config, "readTimeout");
@@ -211,7 +213,7 @@
 
   Set<AccountGroup.UUID> queryForGroups(final DirContext ctx,
       final String username, LdapQuery.Result account)
-      throws NamingException, AccountException {
+      throws NamingException {
     final LdapSchema schema = getSchema(ctx);
     final Set<String> groupDNs = new HashSet<>();
 
@@ -309,6 +311,10 @@
     }
   }
 
+  public boolean groupsVisibleToAll() {
+    return this.groupsVisibleToAll;
+  }
+
   class LdapSchema {
     final LdapType type;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
index 1d90f0c..8dc7177 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
@@ -91,7 +91,7 @@
           //
           helper.authenticate(m.getDN(), req.getPassword()).close();
         }
-        return new AuthUser(new AuthUser.UUID(username), username);
+        return new AuthUser(AuthUser.UUID.create(username), username);
       } finally {
         try {
           ctx.close();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
index 6cdce8d..158aae4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
@@ -235,4 +235,9 @@
     }
     return out;
   }
+
+  @Override
+  public boolean isVisibleToAll(AccountGroup.UUID uuid) {
+    return handles(uuid) && helper.groupsVisibleToAll();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index 7b79add..9060108 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -26,13 +26,12 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AbstractRealm;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.EmailExpander;
-import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.auth.AuthenticationUnavailableException;
 import com.google.gerrit.server.config.AuthConfig;
-import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -59,7 +58,7 @@
 import javax.security.auth.login.LoginException;
 
 @Singleton
-public class LdapRealm implements Realm {
+public class LdapRealm extends AbstractRealm {
   static final Logger log = LoggerFactory.getLogger(LdapRealm.class);
   static final String LDAP = "com.sun.jndi.ldap.LdapCtxFactory";
   static final String USERNAME = "username";
@@ -102,7 +101,7 @@
   }
 
   static SearchScope scope(final Config c, final String setting) {
-    return ConfigUtil.getEnum(c, "ldap", null, setting, SearchScope.SUBTREE);
+    return c.getEnum("ldap", null, setting, SearchScope.SUBTREE);
   }
 
   static String optional(final Config config, final String name) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java
index b99648b..69d523b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java
@@ -52,6 +52,7 @@
     }
   }
 
+  @Override
   @SuppressWarnings("unchecked")
   public void onRemoval(RemovalNotification<K, V> notification) {
     for (CacheRemovalListener<K, V> l : listeners) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
index bb9a40b..d0424d9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -18,6 +18,7 @@
 import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -28,7 +29,6 @@
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.AbandonedSender;
 import com.google.gerrit.server.mail.ReplyToChangeSender;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
new file mode 100644
index 0000000..df6b76e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.webui.PrivateInternals_UiActionDescription;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.changedetail.RebaseChange;
+import com.google.gerrit.server.extensions.webui.UiActions;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.util.Providers;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@Singleton
+public class ActionJson {
+  private final Revisions revisions;
+  private final DynamicMap<RestView<ChangeResource>> changeViews;
+  private final RebaseChange rebaseChange;
+
+  @Inject
+  ActionJson(
+      Revisions revisions,
+      DynamicMap<RestView<ChangeResource>> changeViews,
+      RebaseChange rebaseChange) {
+    this.revisions = revisions;
+    this.changeViews = changeViews;
+    this.rebaseChange = rebaseChange;
+  }
+
+  public Map<String, ActionInfo> format(RevisionResource rsrc) {
+    return toActionMap(rsrc);
+  }
+
+  public ChangeInfo addChangeActions(ChangeInfo to, ChangeControl ctl) {
+    to.actions = toActionMap(ctl);
+    return to;
+  }
+
+  public RevisionInfo addRevisionActions(RevisionInfo to,
+      RevisionResource rsrc) {
+    to.actions = toActionMap(rsrc);
+    return to;
+  }
+
+  private Map<String, ActionInfo> toActionMap(ChangeControl ctl) {
+    Map<String, ActionInfo> out = new LinkedHashMap<>();
+    if (!ctl.getCurrentUser().isIdentifiedUser()) {
+      return out;
+    }
+
+    Provider<CurrentUser> userProvider = Providers.of(ctl.getCurrentUser());
+    for (UiAction.Description d : UiActions.from(
+        changeViews,
+        new ChangeResource(ctl, rebaseChange),
+        userProvider)) {
+      out.put(d.getId(), new ActionInfo(d));
+    }
+    // TODO(sbeller): why do we need to treat followup specially here?
+    if (ctl.getChange().getStatus().isOpen()) {
+      UiAction.Description descr = new UiAction.Description();
+      PrivateInternals_UiActionDescription.setId(descr, "followup");
+      PrivateInternals_UiActionDescription.setMethod(descr, "POST");
+      descr.setTitle("Create follow-up change");
+      out.put(descr.getId(), new ActionInfo(descr));
+    }
+    return out;
+  }
+
+  private Map<String, ActionInfo> toActionMap(RevisionResource rsrc) {
+    Map<String, ActionInfo> out = new LinkedHashMap<>();
+    if (rsrc.getControl().getCurrentUser().isIdentifiedUser()) {
+      Provider<CurrentUser> userProvider = Providers.of(
+          rsrc.getControl().getCurrentUser());
+      for (UiAction.Description d : UiActions.from(
+          revisions, rsrc, userProvider)) {
+        out.put(d.getId(), new ActionInfo(d));
+      }
+    }
+    return out;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java
new file mode 100644
index 0000000..c57f5a0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.edit.ChangeEdit;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.inject.TypeLiteral;
+
+/**
+ * Represents change edit resource, that is actualy two kinds of resources:
+ * <ul>
+ * <li>the change edit itself</li>
+ * <li>a path within the edit</li>
+ * </ul>
+ * distinguished by whether path is null or not.
+ */
+public class ChangeEditResource implements RestResource {
+  public static final TypeLiteral<RestView<ChangeEditResource>> CHANGE_EDIT_KIND =
+      new TypeLiteral<RestView<ChangeEditResource>>() {};
+
+  private final ChangeResource change;
+  private final ChangeEdit edit;
+  private final String path;
+
+  public ChangeEditResource(ChangeResource change, ChangeEdit edit,
+      String path) {
+    this.change = change;
+    this.edit = edit;
+    this.path = path;
+  }
+
+  // TODO(davido): Make this cacheable.
+  // Should just depend on the SHA-1 of the edit itself.
+  public boolean isCacheable() {
+    return false;
+  }
+
+  public ChangeResource getChangeResource() {
+    return change;
+  }
+
+  public ChangeControl getControl() {
+    return getChangeResource().getControl();
+  }
+
+  public Change getChange() {
+    return edit.getChange();
+  }
+
+  public ChangeEdit getChangeEdit() {
+    return edit;
+  }
+
+  public String getPath() {
+    return path;
+  }
+
+  Account.Id getAccountId() {
+    return getUser().getAccountId();
+  }
+
+  IdentifiedUser getUser() {
+    return edit.getUser();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
new file mode 100644
index 0000000..9bd625d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
@@ -0,0 +1,557 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.common.collect.FluentIterable;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.common.DiffWebLinkInfo;
+import com.google.gerrit.extensions.common.EditInfo;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsCreate;
+import com.google.gerrit.extensions.restapi.AcceptsDelete;
+import com.google.gerrit.extensions.restapi.AcceptsPost;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RawInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.WebLinks;
+import com.google.gerrit.server.edit.ChangeEdit;
+import com.google.gerrit.server.edit.ChangeEditJson;
+import com.google.gerrit.server.edit.ChangeEditModifier;
+import com.google.gerrit.server.edit.ChangeEditUtil;
+import com.google.gerrit.server.edit.UnchangedCommitMessageException;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.util.List;
+
+@Singleton
+public class ChangeEdits implements
+    ChildCollection<ChangeResource, ChangeEditResource>,
+    AcceptsCreate<ChangeResource>,
+    AcceptsPost<ChangeResource>,
+    AcceptsDelete<ChangeResource> {
+  private final DynamicMap<RestView<ChangeEditResource>> views;
+  private final Create.Factory createFactory;
+  private final DeleteFile.Factory deleteFileFactory;
+  private final Provider<Detail> detail;
+  private final ChangeEditUtil editUtil;
+  private final Post post;
+
+  @Inject
+  ChangeEdits(DynamicMap<RestView<ChangeEditResource>> views,
+      Create.Factory createFactory,
+      Provider<Detail> detail,
+      ChangeEditUtil editUtil,
+      Post post,
+      DeleteFile.Factory deleteFileFactory) {
+    this.views = views;
+    this.createFactory = createFactory;
+    this.detail = detail;
+    this.editUtil = editUtil;
+    this.post = post;
+    this.deleteFileFactory = deleteFileFactory;
+  }
+
+  @Override
+  public DynamicMap<RestView<ChangeEditResource>> views() {
+    return views;
+  }
+
+  @Override
+  public RestView<ChangeResource> list() {
+    return detail.get();
+  }
+
+  @Override
+  public ChangeEditResource parse(ChangeResource rsrc, IdString id)
+      throws ResourceNotFoundException, AuthException, IOException,
+      InvalidChangeOperationException {
+    Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
+    if (!edit.isPresent()) {
+      throw new ResourceNotFoundException(id);
+    }
+    return new ChangeEditResource(rsrc, edit.get(), id.get());
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public Create create(ChangeResource parent, IdString id)
+      throws RestApiException {
+    return createFactory.create(parent.getChange(), id.get());
+  }
+
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public Post post(ChangeResource parent) throws RestApiException {
+    return post;
+  }
+
+  /**
+  * Create handler that is activated when collection element is accessed
+  * but doesn't exist, e. g. PUT request with a path was called but
+  * change edit wasn't created yet. Change edit is created and PUT
+  * handler is called.
+  */
+  @SuppressWarnings("unchecked")
+  @Override
+  public DeleteFile delete(ChangeResource parent, IdString id)
+      throws RestApiException {
+    // It's safe to assume that id can never be null, because
+    // otherwise we would end up in dedicated endpoint for
+    // deleting of change edits and not a file in change edit
+    return deleteFileFactory.create(id.get());
+  }
+
+  static class Create implements
+      RestModifyView<ChangeResource, Put.Input> {
+
+    interface Factory {
+      Create create(Change change, String path);
+    }
+
+    private final Provider<ReviewDb> db;
+    private final ChangeEditUtil editUtil;
+    private final ChangeEditModifier editModifier;
+    private final Put putEdit;
+    private final Change change;
+    private final String path;
+
+    @Inject
+    Create(Provider<ReviewDb> db,
+        ChangeEditUtil editUtil,
+        ChangeEditModifier editModifier,
+        Put putEdit,
+        @Assisted Change change,
+        @Assisted @Nullable String path) {
+      this.db = db;
+      this.editUtil = editUtil;
+      this.editModifier = editModifier;
+      this.putEdit = putEdit;
+      this.change = change;
+      this.path = path;
+    }
+
+    @Override
+    public Response<?> apply(ChangeResource resource, Put.Input input)
+        throws AuthException, IOException, ResourceConflictException,
+        OrmException, InvalidChangeOperationException {
+      Optional<ChangeEdit> edit = editUtil.byChange(change);
+      if (edit.isPresent()) {
+        throw new ResourceConflictException(String.format(
+            "edit already exists for the change %s",
+            resource.getChange().getChangeId()));
+      }
+      edit = createEdit();
+      if (!Strings.isNullOrEmpty(path)) {
+        putEdit.apply(new ChangeEditResource(resource, edit.get(), path),
+            input);
+      }
+      return Response.none();
+    }
+
+    private Optional<ChangeEdit> createEdit() throws AuthException,
+        IOException, ResourceConflictException, OrmException {
+      editModifier.createEdit(change,
+          db.get().patchSets().get(change.currentPatchSetId()));
+      return editUtil.byChange(change);
+    }
+  }
+
+  static class DeleteFile implements
+      RestModifyView<ChangeResource, DeleteFile.Input> {
+    public static class Input {
+    }
+
+    interface Factory {
+      DeleteFile create(String path);
+    }
+
+    private final ChangeEditUtil editUtil;
+    private final ChangeEditModifier editModifier;
+    private final Provider<ReviewDb> db;
+    private final String path;
+
+    @Inject
+    DeleteFile(ChangeEditUtil editUtil,
+        ChangeEditModifier editModifier,
+        Provider<ReviewDb> db,
+        @Assisted String path) {
+      this.editUtil = editUtil;
+      this.editModifier = editModifier;
+      this.db = db;
+      this.path = path;
+    }
+
+    @Override
+    public Response<?> apply(ChangeResource rsrc, DeleteFile.Input in)
+        throws IOException, AuthException, ResourceConflictException,
+        OrmException, InvalidChangeOperationException, BadRequestException {
+      Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
+      if (edit.isPresent()) {
+        // Edit is wiped out
+        editUtil.delete(edit.get());
+      } else {
+        // Edit is created on top of current patch set by deleting path.
+        // Even if the latest patch set changed since the user triggered
+        // the operation, deleting the whole file is probably still what
+        // they intended.
+        editModifier.createEdit(rsrc.getChange(), db.get().patchSets().get(
+            rsrc.getChange().currentPatchSetId()));
+        edit = editUtil.byChange(rsrc.getChange());
+        editModifier.deleteFile(edit.get(), path);
+      }
+      return Response.none();
+    }
+  }
+
+  // TODO(davido): Turn the boolean options to ChangeEditOption enum,
+  // like it's already the case for ListChangesOption/ListGroupsOption
+  public static class Detail implements RestReadView<ChangeResource> {
+    private final ChangeEditUtil editUtil;
+    private final ChangeEditJson editJson;
+    private final FileInfoJson fileInfoJson;
+    private final Revisions revisions;
+
+    @Option(name = "--base", metaVar = "revision-id")
+    String base;
+
+    @Option(name = "--list")
+    boolean list;
+
+    @Option(name = "--download-commands")
+    boolean downloadCommands;
+
+    @Inject
+    Detail(ChangeEditUtil editUtil,
+        ChangeEditJson editJson,
+        FileInfoJson fileInfoJson,
+        Revisions revisions) {
+      this.editJson = editJson;
+      this.editUtil = editUtil;
+      this.fileInfoJson = fileInfoJson;
+      this.revisions = revisions;
+    }
+
+    @Override
+    public Response<EditInfo> apply(ChangeResource rsrc) throws AuthException,
+        IOException, InvalidChangeOperationException,
+        ResourceNotFoundException, OrmException {
+      Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
+      if (!edit.isPresent()) {
+        return Response.none();
+      }
+
+      EditInfo editInfo = editJson.toEditInfo(edit.get(), downloadCommands);
+      if (list) {
+        PatchSet basePatchSet = null;
+        if (base != null) {
+          RevisionResource baseResource = revisions.parse(
+              rsrc, IdString.fromDecoded(base));
+          basePatchSet = baseResource.getPatchSet();
+        }
+        try {
+          editInfo.files =
+              fileInfoJson.toFileInfoMap(
+                  rsrc.getChange(),
+                  edit.get().getRevision(),
+                  basePatchSet);
+        } catch (PatchListNotAvailableException e) {
+          throw new ResourceNotFoundException(e.getMessage());
+        }
+      }
+      return Response.ok(editInfo);
+    }
+  }
+
+  /**
+   * Post to edit collection resource. Two different operations are
+   * supported:
+   * <ul>
+   * <li>Create non existing change edit</li>
+   * <li>Restore path in existing change edit</li>
+   * </ul>
+   * The combination of two operations in one request is supported.
+   */
+  @Singleton
+  public static class Post implements
+      RestModifyView<ChangeResource, Post.Input> {
+    public static class Input {
+      public String restorePath;
+      public String oldPath;
+      public String newPath;
+    }
+
+    private final Provider<ReviewDb> db;
+    private final ChangeEditUtil editUtil;
+    private final ChangeEditModifier editModifier;
+
+    @Inject
+    Post(Provider<ReviewDb> db,
+        ChangeEditUtil editUtil,
+        ChangeEditModifier editModifier) {
+      this.db = db;
+      this.editUtil = editUtil;
+      this.editModifier = editModifier;
+    }
+
+    @Override
+    public Response<?> apply(ChangeResource resource, Post.Input input)
+        throws AuthException, InvalidChangeOperationException, IOException,
+        ResourceConflictException, OrmException {
+      Optional<ChangeEdit> edit = editUtil.byChange(resource.getChange());
+      if (!edit.isPresent()) {
+        edit = createEdit(resource.getChange());
+      }
+
+      if (input != null) {
+        if (!Strings.isNullOrEmpty(input.restorePath)) {
+          editModifier.restoreFile(edit.get(), input.restorePath);
+        } else if (!Strings.isNullOrEmpty(input.oldPath)
+            && !Strings.isNullOrEmpty(input.newPath)) {
+          editModifier.renameFile(edit.get(), input.oldPath, input.newPath);
+        }
+      }
+      return Response.none();
+    }
+
+    private Optional<ChangeEdit> createEdit(Change change)
+        throws AuthException, IOException, ResourceConflictException,
+        OrmException {
+      editModifier.createEdit(change,
+          db.get().patchSets().get(change.currentPatchSetId()));
+      return editUtil.byChange(change);
+    }
+  }
+
+  /**
+  * Put handler that is activated when PUT request is called on
+  * collection element.
+  */
+  @Singleton
+  public static class Put implements
+      RestModifyView<ChangeEditResource, Put.Input> {
+    public static class Input {
+      @DefaultInput
+      public RawInput content;
+    }
+
+    private final ChangeEditModifier editModifier;
+
+    @Inject
+    Put(ChangeEditModifier editModifier) {
+      this.editModifier = editModifier;
+    }
+
+    @Override
+    public Response<?> apply(ChangeEditResource rsrc, Input input)
+        throws AuthException, ResourceConflictException, IOException {
+      String path = rsrc.getPath();
+      if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') {
+        throw new ResourceConflictException("Invalid path: " + path);
+      }
+
+      try {
+        editModifier.modifyFile(
+            rsrc.getChangeEdit(),
+            rsrc.getPath(),
+            input.content);
+      } catch(InvalidChangeOperationException | IOException e) {
+        throw new ResourceConflictException(e.getMessage());
+      }
+      return Response.none();
+    }
+  }
+
+  /**
+   * Handler to delete a file.
+   * <p>
+   * This deletes the file from the repository completely. This is not the same
+   * as reverting or restoring a file to its previous contents.
+   */
+  @Singleton
+  static class DeleteContent implements
+      RestModifyView<ChangeEditResource, DeleteContent.Input> {
+    public static class Input {
+    }
+
+    private final ChangeEditModifier editModifier;
+
+    @Inject
+    DeleteContent(ChangeEditModifier editModifier) {
+      this.editModifier = editModifier;
+    }
+
+    @Override
+    public Response<?> apply(ChangeEditResource rsrc, DeleteContent.Input input)
+        throws AuthException, ResourceConflictException {
+      try {
+        editModifier.deleteFile(rsrc.getChangeEdit(), rsrc.getPath());
+      } catch(InvalidChangeOperationException | IOException e) {
+        throw new ResourceConflictException(e.getMessage());
+      }
+      return Response.none();
+    }
+  }
+
+  @Singleton
+  static class Get implements RestReadView<ChangeEditResource> {
+    private final FileContentUtil fileContentUtil;
+
+    @Inject
+    Get(FileContentUtil fileContentUtil) {
+      this.fileContentUtil = fileContentUtil;
+    }
+
+    @Override
+    public Response<?> apply(ChangeEditResource rsrc)
+        throws ResourceNotFoundException, IOException {
+      try {
+        return Response.ok(fileContentUtil.getContent(
+              rsrc.getControl().getProjectControl().getProjectState(),
+              ObjectId.fromString(rsrc.getChangeEdit().getRevision().get()),
+              rsrc.getPath()));
+      } catch (ResourceNotFoundException rnfe) {
+        return Response.none();
+      }
+    }
+  }
+
+  @Singleton
+  static class GetMeta implements RestReadView<ChangeEditResource> {
+    private final WebLinks webLinks;
+
+    @Inject
+    GetMeta(WebLinks webLinks) {
+      this.webLinks = webLinks;
+    }
+
+    @Override
+    public FileInfo apply(ChangeEditResource rsrc) {
+      FileInfo r = new FileInfo();
+      ChangeEdit edit = rsrc.getChangeEdit();
+      Change change = edit.getChange();
+      FluentIterable<DiffWebLinkInfo> links =
+          webLinks.getDiffLinks(change.getProject().get(),
+              change.getChangeId(),
+              edit.getBasePatchSet().getPatchSetId(),
+              edit.getBasePatchSet().getRefName(),
+              rsrc.getPath(),
+              0,
+              edit.getRefName(),
+              rsrc.getPath());
+      r.webLinks = links.isEmpty() ? null : links.toList();
+      return r;
+    }
+
+    static class FileInfo {
+      List<DiffWebLinkInfo> webLinks;
+    }
+  }
+
+  @Singleton
+  public static class EditMessage implements
+      RestModifyView<ChangeResource, EditMessage.Input> {
+    public static class Input {
+      @DefaultInput
+      public String message;
+    }
+
+    private final Provider<ReviewDb> db;
+    private final ChangeEditModifier editModifier;
+    private final ChangeEditUtil editUtil;
+
+    @Inject
+    EditMessage(Provider<ReviewDb> db,
+        ChangeEditModifier editModifier,
+        ChangeEditUtil editUtil) {
+      this.db = db;
+      this.editModifier = editModifier;
+      this.editUtil = editUtil;
+    }
+
+    @Override
+    public Object apply(ChangeResource rsrc, Input input) throws AuthException,
+        IOException, InvalidChangeOperationException, BadRequestException,
+        ResourceConflictException, OrmException {
+      Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
+      if (!edit.isPresent()) {
+        editModifier.createEdit(rsrc.getChange(),
+            db.get().patchSets().get(rsrc.getChange().currentPatchSetId()));
+        edit = editUtil.byChange(rsrc.getChange());
+      }
+
+      if (input == null || Strings.isNullOrEmpty(input.message)) {
+        throw new BadRequestException("commit message must be provided");
+      }
+
+      try {
+        editModifier.modifyMessage(edit.get(), input.message);
+      } catch (UnchangedCommitMessageException ucm) {
+        throw new ResourceConflictException(ucm.getMessage());
+      }
+
+      return Response.none();
+    }
+  }
+
+  @Singleton
+  public static class GetMessage implements RestReadView<ChangeResource> {
+    private final ChangeEditUtil editUtil;
+
+    @Inject
+    GetMessage(ChangeEditUtil editUtil) {
+      this.editUtil = editUtil;
+    }
+
+    @Override
+    public BinaryResult apply(ChangeResource rsrc) throws AuthException,
+        IOException, ResourceNotFoundException {
+      Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
+      if (edit.isPresent()) {
+        String msg = edit.get().getEditCommit().getFullMessage();
+        return BinaryResult.create(msg)
+            .setContentType(FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE)
+            .base64();
+      }
+      throw new ResourceNotFoundException();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index d0c2b98..ff833f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -19,6 +19,8 @@
 import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.extensions.api.changes.HashtagsInput;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -29,12 +31,17 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.CreateChangeSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.util.RequestScopePropagator;
+import com.google.gerrit.server.validators.ValidationException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -52,7 +59,7 @@
 
 public class ChangeInserter {
   public static interface Factory {
-    ChangeInserter create(RefControl ctl, Change c, RevCommit rc);
+    ChangeInserter create(ProjectControl ctl, Change c, RevCommit rc);
   }
 
   private static final Logger log =
@@ -64,10 +71,13 @@
   private final ChangeHooks hooks;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeMessagesUtil cmUtil;
-  private final MergeabilityChecker mergeabilityChecker;
+  private final ChangeIndexer indexer;
   private final CreateChangeSender.Factory createChangeSenderFactory;
+  private final HashtagsUtil hashtagsUtil;
+  private final AccountCache accountCache;
+  private final WorkQueue workQueue;
 
-  private final RefControl refControl;
+  private final ProjectControl projectControl;
   private final Change change;
   private final PatchSet patchSet;
   private final RevCommit commit;
@@ -77,6 +87,8 @@
   private Set<Account.Id> reviewers;
   private Set<Account.Id> extraCC;
   private Map<String, Short> approvals;
+  private Set<String> hashtags;
+  private RequestScopePropagator requestScopePropagator;
   private boolean runHooks;
   private boolean sendMail;
 
@@ -88,9 +100,12 @@
       ChangeHooks hooks,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
-      MergeabilityChecker mergeabilityChecker,
+      ChangeIndexer indexer,
       CreateChangeSender.Factory createChangeSenderFactory,
-      @Assisted RefControl refControl,
+      HashtagsUtil hashtagsUtil,
+      AccountCache accountCache,
+      WorkQueue workQueue,
+      @Assisted ProjectControl projectControl,
       @Assisted Change change,
       @Assisted RevCommit commit) {
     this.dbProvider = dbProvider;
@@ -99,14 +114,18 @@
     this.hooks = hooks;
     this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
-    this.mergeabilityChecker = mergeabilityChecker;
+    this.indexer = indexer;
     this.createChangeSenderFactory = createChangeSenderFactory;
-    this.refControl = refControl;
+    this.hashtagsUtil = hashtagsUtil;
+    this.accountCache = accountCache;
+    this.workQueue = workQueue;
+    this.projectControl = projectControl;
     this.change = change;
     this.commit = commit;
     this.reviewers = Collections.emptySet();
     this.extraCC = Collections.emptySet();
     this.approvals = Collections.emptyMap();
+    this.hashtags = Collections.emptySet();
     this.runHooks = true;
     this.sendMail = true;
 
@@ -117,7 +136,6 @@
     patchSet.setRevision(new RevId(commit.name()));
     patchSetInfo = patchSetInfoFactory.get(commit, patchSet.getId());
     change.setCurrentPatchSet(patchSetInfo);
-    ChangeUtil.computeSortKey(change);
   }
 
   public Change getChange() {
@@ -145,6 +163,11 @@
     return this;
   }
 
+  public ChangeInserter setHashtags(Set<String> hashtags) {
+    this.hashtags = hashtags;
+    return this;
+  }
+
   public ChangeInserter setRunHooks(boolean runHooks) {
     this.runHooks = runHooks;
     return this;
@@ -155,6 +178,11 @@
     return this;
   }
 
+  public ChangeInserter setRequestScopePropagator(RequestScopePropagator r) {
+    this.requestScopePropagator = r;
+    return this;
+  }
+
   public PatchSet getPatchSet() {
     return patchSet;
   }
@@ -170,7 +198,7 @@
 
   public Change insert() throws OrmException, IOException {
     ReviewDb db = dbProvider.get();
-    ChangeControl ctl = refControl.getProjectControl().controlFor(change);
+    ChangeControl ctl = projectControl.controlFor(change);
     ChangeUpdate update = updateFactory.create(
         ctl,
         change.getCreatedOn());
@@ -179,11 +207,11 @@
       ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
       db.patchSets().insert(Collections.singleton(patchSet));
       db.changes().insert(Collections.singleton(change));
-      LabelTypes labelTypes = refControl.getProjectControl().getLabelTypes();
+      LabelTypes labelTypes = projectControl.getLabelTypes();
       approvalsUtil.addReviewers(db, update, labelTypes, change,
           patchSet, patchSetInfo, reviewers, Collections.<Account.Id> emptySet());
       approvalsUtil.addApprovals(db, update, labelTypes, patchSet, patchSetInfo,
-          change, ctl, approvals);
+          ctl, approvals);
       if (messageIsForChange()) {
         cmUtil.addChangeMessage(db, update, changeMessage);
       }
@@ -191,27 +219,51 @@
     } finally {
       db.rollback();
     }
-    update.commit();
-    CheckedFuture<?, IOException> f = mergeabilityChecker.newCheck()
-        .addChange(change)
-        .reindex()
-        .runAsync();
 
-    if(!messageIsForChange()) {
+    update.commit();
+
+    if (hashtags != null && hashtags.size() > 0) {
+      try {
+        HashtagsInput input = new HashtagsInput();
+        input.add = hashtags;
+        hashtagsUtil.setHashtags(ctl, input, false, false);
+      } catch (ValidationException | AuthException e) {
+        log.error("Cannot add hashtags to change " + change.getId(), e);
+      }
+    }
+
+    CheckedFuture<?, IOException> f = indexer.indexAsync(change.getId());
+
+    if (!messageIsForChange()) {
       commitMessageNotForChange();
     }
 
     if (sendMail) {
-      try {
-        CreateChangeSender cm =
-            createChangeSenderFactory.create(change);
-        cm.setFrom(change.getOwner());
-        cm.setPatchSet(patchSet, patchSetInfo);
-        cm.addReviewers(reviewers);
-        cm.addExtraCC(extraCC);
-        cm.send();
-      } catch (Exception err) {
-        log.error("Cannot send email for new change " + change.getId(), err);
+      Runnable sender = new Runnable() {
+        @Override
+        public void run() {
+          try {
+            CreateChangeSender cm =
+                createChangeSenderFactory.create(change);
+            cm.setFrom(change.getOwner());
+            cm.setPatchSet(patchSet, patchSetInfo);
+            cm.addReviewers(reviewers);
+            cm.addExtraCC(extraCC);
+            cm.send();
+          } catch (Exception e) {
+            log.error("Cannot send email for new change " + change.getId(), e);
+          }
+        }
+
+        @Override
+        public String toString() {
+          return "send-email newchange";
+        }
+      };
+      if (requestScopePropagator != null) {
+        workQueue.getDefaultQueue().submit(requestScopePropagator.wrap(sender));
+      } else {
+        sender.run();
       }
     }
     f.checkedGet();
@@ -221,6 +273,11 @@
 
     if (runHooks) {
       hooks.doPatchsetCreatedHook(change, patchSet, db);
+      if (hashtags != null && hashtags.size() > 0) {
+        hooks.doHashtagsChangedHook(change,
+            accountCache.get(change.getOwner()).getAccount(),
+            hashtags, null, hashtags, db);
+      }
     }
 
     return change;
@@ -234,8 +291,7 @@
           db.changes().get(changeMessage.getPatchSetId().getParentKey());
       ChangeUtil.bumpRowVersionNotLastUpdatedOn(
           changeMessage.getKey().getParentKey(), db);
-      ChangeControl otherControl =
-          refControl.getProjectControl().controlFor(otherChange);
+      ChangeControl otherControl = projectControl.controlFor(otherChange);
       ChangeUpdate updateForOtherChange =
           updateFactory.create(otherControl, change.getLastUpdatedOn());
       cmUtil.addChangeMessage(db, updateForOtherChange, changeMessage);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 586c294..367ec2c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -14,28 +14,34 @@
 
 package com.google.gerrit.server.change;
 
-import static com.google.gerrit.extensions.common.ListChangesOption.ALL_COMMITS;
-import static com.google.gerrit.extensions.common.ListChangesOption.ALL_FILES;
-import static com.google.gerrit.extensions.common.ListChangesOption.ALL_REVISIONS;
-import static com.google.gerrit.extensions.common.ListChangesOption.CURRENT_ACTIONS;
-import static com.google.gerrit.extensions.common.ListChangesOption.CURRENT_COMMIT;
-import static com.google.gerrit.extensions.common.ListChangesOption.CURRENT_FILES;
-import static com.google.gerrit.extensions.common.ListChangesOption.CURRENT_REVISION;
-import static com.google.gerrit.extensions.common.ListChangesOption.DETAILED_ACCOUNTS;
-import static com.google.gerrit.extensions.common.ListChangesOption.DETAILED_LABELS;
-import static com.google.gerrit.extensions.common.ListChangesOption.DOWNLOAD_COMMANDS;
-import static com.google.gerrit.extensions.common.ListChangesOption.DRAFT_COMMENTS;
-import static com.google.gerrit.extensions.common.ListChangesOption.LABELS;
-import static com.google.gerrit.extensions.common.ListChangesOption.MESSAGES;
-import static com.google.gerrit.extensions.common.ListChangesOption.REVIEWED;
-import static com.google.gerrit.extensions.common.ListChangesOption.WEB_LINKS;
+import static com.google.gerrit.extensions.client.ListChangesOption.ALL_COMMITS;
+import static com.google.gerrit.extensions.client.ListChangesOption.ALL_FILES;
+import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
+import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS;
+import static com.google.gerrit.extensions.client.ListChangesOption.CHECK;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_FILES;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
+import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_ACCOUNTS;
+import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
+import static com.google.gerrit.extensions.client.ListChangesOption.DOWNLOAD_COMMANDS;
+import static com.google.gerrit.extensions.client.ListChangesOption.DRAFT_COMMENTS;
+import static com.google.gerrit.extensions.client.ListChangesOption.LABELS;
+import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
+import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
+import static com.google.gerrit.extensions.client.ListChangesOption.WEB_LINKS;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Function;
 import com.google.common.base.Joiner;
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Optional;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.LinkedHashMultimap;
 import com.google.common.collect.Lists;
@@ -44,25 +50,30 @@
 import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Table;
+import com.google.gerrit.common.Nullable;
 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.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.SubmitRecord;
-import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.extensions.api.changes.FixInput;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ApprovalInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.FetchInfo;
 import com.google.gerrit.extensions.common.GitPerson;
-import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.common.ProblemInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.config.DownloadCommand;
 import com.google.gerrit.extensions.config.DownloadScheme;
 import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -71,24 +82,27 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.PatchSetInfo.ParentInfo;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.WebLinks;
-import com.google.gerrit.server.account.AccountInfo;
-import com.google.gerrit.server.extensions.webui.UiActions;
+import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.changedetail.RebaseChange;
 import com.google.gerrit.server.git.LabelNormalizer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
+import com.google.gerrit.server.query.change.QueryResult;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -108,6 +122,7 @@
 
 public class ChangeJson {
   private static final Logger log = LoggerFactory.getLogger(ChangeJson.class);
+
   private static final List<ChangeMessage> NO_MESSAGES =
       ImmutableList.of();
 
@@ -119,16 +134,19 @@
   private final ChangeData.Factory changeDataFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final FileInfoJson fileInfoJson;
-  private final AccountInfo.Loader.Factory accountLoaderFactory;
+  private final AccountLoader.Factory accountLoaderFactory;
   private final DynamicMap<DownloadScheme> downloadSchemes;
   private final DynamicMap<DownloadCommand> downloadCommands;
-  private final DynamicMap<RestView<ChangeResource>> changeViews;
-  private final Revisions revisions;
-  private final Provider<WebLinks> webLinks;
+  private final WebLinks webLinks;
   private final EnumSet<ListChangesOption> options;
   private final ChangeMessagesUtil cmUtil;
+  private final PatchLineCommentsUtil plcUtil;
+  private final Provider<ConsistencyChecker> checkerProvider;
+  private final ActionJson actionJson;
+  private final RebaseChange rebaseChange;
 
-  private AccountInfo.Loader accountLoader;
+  private AccountLoader accountLoader;
+  private FixInput fix;
 
   @Inject
   ChangeJson(
@@ -137,17 +155,18 @@
       Provider<CurrentUser> user,
       AnonymousUser au,
       IdentifiedUser.GenericFactory uf,
-      ProjectControl.GenericFactory pcf,
       ChangeData.Factory cdf,
       PatchSetInfoFactory psi,
       FileInfoJson fileInfoJson,
-      AccountInfo.Loader.Factory ailf,
+      AccountLoader.Factory ailf,
       DynamicMap<DownloadScheme> downloadSchemes,
       DynamicMap<DownloadCommand> downloadCommands,
-      DynamicMap<RestView<ChangeResource>> changeViews,
-      Revisions revisions,
-      Provider<WebLinks> webLinks,
-      ChangeMessagesUtil cmUtil) {
+      WebLinks webLinks,
+      ChangeMessagesUtil cmUtil,
+      PatchLineCommentsUtil plcUtil,
+      Provider<ConsistencyChecker> checkerProvider,
+      ActionJson actionJson,
+      RebaseChange rebaseChange) {
     this.db = db;
     this.labelNormalizer = ln;
     this.userProvider = user;
@@ -159,10 +178,12 @@
     this.accountLoaderFactory = ailf;
     this.downloadSchemes = downloadSchemes;
     this.downloadCommands = downloadCommands;
-    this.changeViews = changeViews;
-    this.revisions = revisions;
     this.webLinks = webLinks;
     this.cmUtil = cmUtil;
+    this.plcUtil = plcUtil;
+    this.checkerProvider = checkerProvider;
+    this.actionJson = actionJson;
+    this.rebaseChange = rebaseChange;
     options = EnumSet.noneOf(ListChangesOption.class);
   }
 
@@ -176,6 +197,11 @@
     return this;
   }
 
+  public ChangeJson fix(FixInput fix) {
+    this.fix = fix;
+    return this;
+  }
+
   public ChangeInfo format(ChangeResource rsrc) throws OrmException {
     return format(changeDataFactory.create(db.get(), rsrc.getControl()));
   }
@@ -185,7 +211,16 @@
   }
 
   public ChangeInfo format(Change.Id id) throws OrmException {
-    return format(changeDataFactory.create(db.get(), id));
+    Change c;
+    try {
+      c = db.get().changes().get(id);
+    } catch (OrmException e) {
+      if (!has(CHECK)) {
+        throw e;
+      }
+      return checkOnly(changeDataFactory.create(db.get(), id));
+    }
+    return format(changeDataFactory.create(db.get(), c));
   }
 
   public ChangeInfo format(ChangeData cd) throws OrmException {
@@ -194,14 +229,21 @@
 
   private ChangeInfo format(ChangeData cd, Optional<PatchSet.Id> limitToPsId)
       throws OrmException {
-    accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
-    Set<Change.Id> reviewed = Sets.newHashSet();
-    if (has(REVIEWED)) {
-      reviewed = loadReviewed(Collections.singleton(cd));
+    try {
+      accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
+      Set<Change.Id> reviewed = Sets.newHashSet();
+      if (has(REVIEWED)) {
+        reviewed = loadReviewed(Collections.singleton(cd));
+      }
+      ChangeInfo res = toChangeInfo(cd, reviewed, limitToPsId);
+      accountLoader.fill();
+      return res;
+    } catch (OrmException | RuntimeException e) {
+      if (!has(CHECK)) {
+        throw e;
+      }
+      return checkOnly(cd);
     }
-    ChangeInfo res = toChangeInfo(cd, reviewed, limitToPsId);
-    accountLoader.fill();
-    return res;
   }
 
   public ChangeInfo format(RevisionResource rsrc) throws OrmException {
@@ -209,10 +251,16 @@
     return format(cd, Optional.of(rsrc.getPatchSet().getId()));
   }
 
-  public List<List<ChangeInfo>> formatList2(List<List<ChangeData>> in)
+  public List<List<ChangeInfo>> formatQueryResults(List<QueryResult> in)
       throws OrmException {
     accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
-    Iterable<ChangeData> all = Iterables.concat(in);
+    Iterable<ChangeData> all = FluentIterable.from(in)
+        .transformAndConcat(new Function<QueryResult, List<ChangeData>>() {
+          @Override
+          public List<ChangeData> apply(QueryResult in) {
+            return in.changes();
+          }
+        });
     ChangeData.ensureChangeLoaded(all);
     if (has(ALL_REVISIONS)) {
       ChangeData.ensureAllPatchSetsLoaded(all);
@@ -227,8 +275,12 @@
 
     List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(in.size());
     Map<Change.Id, ChangeInfo> out = Maps.newHashMap();
-    for (List<ChangeData> changes : in) {
-      res.add(toChangeInfo(out, changes, reviewed));
+    for (QueryResult r : in) {
+      List<ChangeInfo> infos = toChangeInfo(out, r.changes(), reviewed);
+      if (r.moreChanges()) {
+        infos.get(infos.size() - 1)._moreChanges = true;
+      }
+      res.add(infos);
     }
     accountLoader.fill();
     return res;
@@ -239,17 +291,21 @@
   }
 
   private List<ChangeInfo> toChangeInfo(Map<Change.Id, ChangeInfo> out,
-      List<ChangeData> changes, Set<Change.Id> reviewed) throws OrmException {
+      List<ChangeData> changes, Set<Change.Id> reviewed) {
     List<ChangeInfo> info = Lists.newArrayListWithCapacity(changes.size());
     for (ChangeData cd : changes) {
       ChangeInfo i = out.get(cd.getId());
       if (i == null) {
         try {
           i = toChangeInfo(cd, reviewed, Optional.<PatchSet.Id> absent());
-        } catch (OrmException e) {
-          log.warn(
-              "Omitting corrupt change " + cd.getId() + " from results", e);
-          continue;
+        } catch (OrmException | RuntimeException e) {
+          if (has(CHECK)) {
+            i = checkOnly(cd);
+          } else {
+            log.warn(
+                "Omitting corrupt change " + cd.getId() + " from results", e);
+            continue;
+          }
         }
         out.put(cd.getId(), i);
       }
@@ -258,34 +314,76 @@
     return info;
   }
 
+  private ChangeInfo checkOnly(ChangeData cd) {
+    ConsistencyChecker.Result result = checkerProvider.get().check(cd, fix);
+    ChangeInfo info;
+    Change c = result.change();
+    if (c != null) {
+      info = new ChangeInfo();
+      info.project = c.getProject().get();
+      info.branch = c.getDest().getShortName();
+      info.topic = c.getTopic();
+      info.changeId = c.getKey().get();
+      info.subject = c.getSubject();
+      info.status = c.getStatus().asChangeStatus();
+      info.owner = new AccountInfo(c.getOwner().get());
+      info.created = c.getCreatedOn();
+      info.updated = c.getLastUpdatedOn();
+      info._number = c.getId().get();
+      info.problems = result.problems();
+      finish(info);
+    } else {
+      info = new ChangeInfo();
+      info._number = result.id().get();
+      info.problems = result.problems();
+    }
+    return info;
+  }
+
   private ChangeInfo toChangeInfo(ChangeData cd, Set<Change.Id> reviewed,
       Optional<PatchSet.Id> limitToPsId) throws OrmException {
-    ChangeControl ctl = cd.changeControl().forUser(userProvider.get());
     ChangeInfo out = new ChangeInfo();
+
+    if (has(CHECK)) {
+      out.problems = checkerProvider.get().check(cd.change(), fix).problems();
+      // If any problems were fixed, the ChangeData needs to be reloaded.
+      for (ProblemInfo p : out.problems) {
+        if (p.status == ProblemInfo.Status.FIXED) {
+          cd = changeDataFactory.create(cd.db(), cd.getId());
+          break;
+        }
+      }
+    }
+
     Change in = cd.change();
+    ChangeControl ctl = cd.changeControl().forUser(userProvider.get());
     out.project = in.getProject().get();
     out.branch = in.getDest().getShortName();
     out.topic = in.getTopic();
+    out.hashtags = ctl.getNotes().load().getHashtags();
     out.changeId = in.getKey().get();
-    out.mergeable = isMergeable(in);
+    // TODO(dborowitz): This gets the submit type, so we could include that in
+    // the response and avoid making a request to /submit_type from the UI.
+    out.mergeable = in.getStatus() == Change.Status.MERGED
+        ? null : cd.isMergeable();
     ChangedLines changedLines = cd.changedLines();
     if (changedLines != null) {
       out.insertions = changedLines.insertions;
       out.deletions = changedLines.deletions;
     }
     out.subject = in.getSubject();
-    out.status = in.getStatus();
+    out.status = in.getStatus().asChangeStatus();
     out.owner = accountLoader.get(in.getOwner());
     out.created = in.getCreatedOn();
     out.updated = in.getLastUpdatedOn();
     out._number = in.getId().get();
-    out._sortkey = in.getSortKey();
     out.starred = userProvider.get().getStarredChanges().contains(in.getId())
         ? true
         : null;
     out.reviewed = in.getStatus().isOpen()
         && has(REVIEWED)
         && reviewed.contains(cd.getId()) ? true : null;
+
     out.labels = labelsFor(ctl, cd, has(LABELS), has(DETAILED_LABELS));
 
     if (out.labels != null && has(DETAILED_LABELS)) {
@@ -295,19 +393,19 @@
           || limitToPsId.get().equals(in.currentPatchSetId())) {
         out.permittedLabels = permittedLabels(ctl, cd);
       }
-      out.removableReviewers = removableReviewers(ctl, cd, out.labels.values());
+      out.removableReviewers = removableReviewers(ctl, out.labels.values());
     }
 
     Map<PatchSet.Id, PatchSet> src = loadPatchSets(cd, limitToPsId);
     if (has(MESSAGES)) {
       out.messages = messages(ctl, cd, src);
     }
-    out.finish();
+    finish(out);
 
     if (has(ALL_REVISIONS)
         || has(CURRENT_REVISION)
         || limitToPsId.isPresent()) {
-      out.revisions = revisions(ctl, cd, limitToPsId, out.project, src);
+      out.revisions = revisions(ctl, cd, src);
       if (out.revisions != null) {
         for (Map.Entry<String, RevisionInfo> entry : out.revisions.entrySet()) {
           if (entry.getValue().isCurrent) {
@@ -318,44 +416,30 @@
       }
     }
 
-    if (has(CURRENT_ACTIONS) && userProvider.get().isIdentifiedUser()) {
-      out.actions = Maps.newTreeMap();
-      for (UiAction.Description d : UiActions.from(
-          changeViews,
-          new ChangeResource(ctl),
-          userProvider)) {
-        out.actions.put(d.getId(), new ActionInfo(d));
-      }
+    if (has(CURRENT_ACTIONS) || has(CHANGE_ACTIONS)) {
+      actionJson.addChangeActions(out, ctl);
     }
+
     return out;
   }
 
-  private Boolean isMergeable(Change c) {
-    if (c.getStatus() == Change.Status.MERGED
-        || c.getLastSha1MergeTested() == null) {
-      return null;
-    }
-    return c.isMergeable();
-  }
-
-  private List<SubmitRecord> submitRecords(ChangeControl ctl, ChangeData cd)
-      throws OrmException {
+  private List<SubmitRecord> submitRecords(ChangeData cd) throws OrmException {
     if (cd.getSubmitRecords() != null) {
       return cd.getSubmitRecords();
     }
-    if (ctl == null) {
-      return ImmutableList.of();
-    }
     PatchSet ps = cd.currentPatchSet();
     if (ps == null) {
       return ImmutableList.of();
     }
-    cd.setSubmitRecords(ctl.canSubmit(db.get(), ps, cd, true, false, true));
+    cd.setSubmitRecords(new SubmitRuleEvaluator(cd).setPatchSet(ps)
+        .setFastEvalLabels(true)
+        .setAllowDraft(true)
+        .canSubmit());
     return cd.getSubmitRecords();
   }
 
-  private Map<String, LabelInfo> labelsFor(ChangeControl ctl, ChangeData cd, boolean standard,
-      boolean detailed) throws OrmException {
+  private Map<String, LabelInfo> labelsFor(ChangeControl ctl,
+      ChangeData cd, boolean standard, boolean detailed) throws OrmException {
     if (!standard && !detailed) {
       return null;
     }
@@ -365,21 +449,21 @@
     }
 
     LabelTypes labelTypes = ctl.getLabelTypes();
-    if (cd.change().getStatus().isOpen()) {
-      return labelsForOpenChange(ctl, cd, labelTypes, standard, detailed);
-    } else {
-      return labelsForClosedChange(cd, labelTypes, standard, detailed);
-    }
+    Map<String, LabelWithStatus> withStatus = cd.change().getStatus().isOpen()
+      ? labelsForOpenChange(ctl, cd, labelTypes, standard, detailed)
+      : labelsForClosedChange(cd, labelTypes, standard, detailed);
+    return ImmutableMap.copyOf(
+        Maps.transformValues(withStatus, LabelWithStatus.TO_LABEL_INFO));
   }
 
-  private Map<String, LabelInfo> labelsForOpenChange(ChangeControl ctl,
+  private Map<String, LabelWithStatus> labelsForOpenChange(ChangeControl ctl,
       ChangeData cd, LabelTypes labelTypes, boolean standard, boolean detailed)
       throws OrmException {
-    Map<String, LabelInfo> labels = initLabels(ctl, cd, labelTypes, standard);
+    Map<String, LabelWithStatus> labels = initLabels(cd, labelTypes, standard);
     if (detailed) {
       setAllApprovals(ctl, cd, labels);
     }
-    for (Map.Entry<String, LabelInfo> e : labels.entrySet()) {
+    for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
       LabelType type = labelTypes.byLabel(e.getKey());
       if (type == null) {
         continue;
@@ -400,19 +484,18 @@
     return labels;
   }
 
-  private Map<String, LabelInfo> initLabels(ChangeControl ctl, ChangeData cd,
+  private Map<String, LabelWithStatus> initLabels(ChangeData cd,
       LabelTypes labelTypes, boolean standard) throws OrmException {
     // Don't use Maps.newTreeMap(Comparator) due to OpenJDK bug 100167.
-    Map<String, LabelInfo> labels = new TreeMap<>(labelTypes.nameComparator());
-    for (SubmitRecord rec : submitRecords(ctl, cd)) {
+    Map<String, LabelWithStatus> labels = new TreeMap<>(labelTypes.nameComparator());
+    for (SubmitRecord rec : submitRecords(cd)) {
       if (rec.labels == null) {
         continue;
       }
       for (SubmitRecord.Label r : rec.labels) {
-        LabelInfo p = labels.get(r.label);
-        if (p == null || p._status.compareTo(r.status) < 0) {
+        LabelWithStatus p = labels.get(r.label);
+        if (p == null || p.status().compareTo(r.status) < 0) {
           LabelInfo n = new LabelInfo();
-          n._status = r.status;
           if (standard) {
             switch (r.status) {
               case OK:
@@ -427,8 +510,8 @@
             }
           }
 
-          n.optional = n._status == SubmitRecord.Label.Status.MAY ? true : null;
-          labels.put(r.label, n);
+          n.optional = r.status == SubmitRecord.Label.Status.MAY ? true : null;
+          labels.put(r.label, LabelWithStatus.create(n, r.status));
         }
       }
     }
@@ -436,9 +519,8 @@
   }
 
   private void setLabelScores(LabelType type,
-      LabelInfo label, short score, Account.Id accountId)
-      throws OrmException {
-    if (label.approved != null || label.rejected != null) {
+      LabelWithStatus l, short score, Account.Id accountId) {
+    if (l.label().approved != null || l.label().rejected != null) {
       return;
     }
 
@@ -449,21 +531,21 @@
 
     if (score != 0) {
       if (score == type.getMin().getValue()) {
-        label.rejected = accountLoader.get(accountId);
+        l.label().rejected = accountLoader.get(accountId);
       } else if (score == type.getMax().getValue()) {
-        label.approved = accountLoader.get(accountId);
+        l.label().approved = accountLoader.get(accountId);
       } else if (score < 0) {
-        label.disliked = accountLoader.get(accountId);
-        label.value = score;
-      } else if (score > 0 && label.disliked == null) {
-        label.recommended = accountLoader.get(accountId);
-        label.value = score;
+        l.label().disliked = accountLoader.get(accountId);
+        l.label().value = score;
+      } else if (score > 0 && l.label().disliked == null) {
+        l.label().recommended = accountLoader.get(accountId);
+        l.label().value = score;
       }
     }
   }
 
   private void setAllApprovals(ChangeControl baseCtrl, ChangeData cd,
-      Map<String, LabelInfo> labels) throws OrmException {
+      Map<String, LabelWithStatus> labels) throws OrmException {
     // Include a user in the output for this label if either:
     //  - They are an explicit reviewer.
     //  - They ever voted on this change.
@@ -482,7 +564,7 @@
     for (Account.Id accountId : allUsers) {
       IdentifiedUser user = userFactory.create(accountId);
       ChangeControl ctl = baseCtrl.forUser(user);
-      for (Map.Entry<String, LabelInfo> e : labels.entrySet()) {
+      for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
         LabelType lt = ctl.getLabelTypes().byLabel(e.getKey());
         if (lt == null) {
           // Ignore submit record for undefined label; likely the submit rule
@@ -501,12 +583,12 @@
           // user can vote on this label.
           value = labelNormalizer.canVote(ctl, lt, accountId) ? 0 : null;
         }
-        e.getValue().addApproval(approvalInfo(accountId, value, date));
+        addApproval(e.getValue().label(), approvalInfo(accountId, value, date));
       }
     }
   }
 
-  private Map<String, LabelInfo> labelsForClosedChange(ChangeData cd,
+  private Map<String, LabelWithStatus> labelsForClosedChange(ChangeData cd,
       LabelTypes labelTypes, boolean standard, boolean detailed)
       throws OrmException {
     Set<Account.Id> allUsers = Sets.newHashSet();
@@ -529,14 +611,15 @@
     }
 
     // Don't use Maps.newTreeMap(Comparator) due to OpenJDK bug 100167.
-    Map<String, LabelInfo> labels = new TreeMap<>(labelTypes.nameComparator());
+    Map<String, LabelWithStatus> labels =
+        new TreeMap<>(labelTypes.nameComparator());
     for (String name : labelNames) {
       LabelType type = labelTypes.byLabel(name);
-      LabelInfo li = new LabelInfo();
+      LabelWithStatus l = LabelWithStatus.create(new LabelInfo(), null);
       if (detailed) {
-        setLabelValues(type, li);
+        setLabelValues(type, l);
       }
-      labels.put(type.getName(), li);
+      labels.put(type.getName(), l);
     }
 
     for (Account.Id accountId : allUsers) {
@@ -544,10 +627,10 @@
           Maps.newHashMapWithExpectedSize(labels.size());
 
       if (detailed) {
-        for (Map.Entry<String, LabelInfo> entry : labels.entrySet()) {
+        for (Map.Entry<String, LabelWithStatus> entry : labels.entrySet()) {
           ApprovalInfo ai = approvalInfo(accountId, 0, null);
           byLabel.put(entry.getKey(), ai);
-          entry.getValue().addApproval(ai);
+          addApproval(entry.getValue().label(), ai);
         }
       }
       for (PatchSetApproval psa : current.get(accountId)) {
@@ -562,20 +645,18 @@
           info.value = Integer.valueOf(val);
           info.date = psa.getGranted();
         }
-
-        LabelInfo li = labels.get(type.getName());
         if (!standard) {
           continue;
         }
 
-        setLabelScores(type, li, val, accountId);
+        setLabelScores(type, labels.get(type.getName()), val, accountId);
       }
     }
     return labels;
   }
 
   private ApprovalInfo approvalInfo(Account.Id id, Integer value, Timestamp date) {
-    ApprovalInfo ai = new ApprovalInfo(id);
+    ApprovalInfo ai = new ApprovalInfo(id.get());
     ai.value = value;
     ai.date = date;
     accountLoader.put(ai);
@@ -586,14 +667,14 @@
     return values.isEmpty() || (values.size() == 1 && values.contains(" 0"));
   }
 
-  private void setLabelValues(LabelType type, LabelInfo label) {
-    label.defaultValue = type.getDefaultValue();
-    label.values = Maps.newLinkedHashMap();
+  private void setLabelValues(LabelType type, LabelWithStatus l) {
+    l.label().defaultValue = type.getDefaultValue();
+    l.label().values = Maps.newLinkedHashMap();
     for (LabelValue v : type.getValues()) {
-      label.values.put(v.formatValue(), v.getText());
+      l.label().values.put(v.formatValue(), v.getText());
     }
-    if (isOnlyZero(label.values.keySet())) {
-      label.values = null;
+    if (isOnlyZero(l.label().values.keySet())) {
+      l.label().values = null;
     }
   }
 
@@ -605,7 +686,7 @@
 
     LabelTypes labelTypes = ctl.getLabelTypes();
     SetMultimap<String, String> permitted = LinkedHashMultimap.create();
-    for (SubmitRecord rec : submitRecords(ctl, cd)) {
+    for (SubmitRecord rec : submitRecords(cd)) {
       if (rec.labels == null) {
         continue;
       }
@@ -670,8 +751,8 @@
     return result;
   }
 
-  private Collection<AccountInfo> removableReviewers(ChangeControl ctl, ChangeData cd,
-      Collection<LabelInfo> labels) throws OrmException {
+  private Collection<AccountInfo> removableReviewers(ChangeControl ctl,
+      Collection<LabelInfo> labels) {
     Set<Account.Id> fixed = Sets.newHashSetWithExpectedSize(labels.size());
     Set<Account.Id> removable = Sets.newHashSetWithExpectedSize(labels.size());
     for (LabelInfo label : labels) {
@@ -679,10 +760,11 @@
         continue;
       }
       for (ApprovalInfo ai : label.all) {
-        if (ctl.canRemoveReviewer(ai._id, Objects.firstNonNull(ai.value, 0))) {
-          removable.add(ai._id);
+        Account.Id id = new Account.Id(ai._accountId);
+        if (ctl.canRemoveReviewer(id, MoreObjects.firstNonNull(ai.value, 0))) {
+          removable.add(id);
         } else {
-          fixed.add(ai._id);
+          fixed.add(id);
         }
       }
     }
@@ -739,14 +821,13 @@
   }
 
   private Map<String, RevisionInfo> revisions(ChangeControl ctl, ChangeData cd,
-      Optional<PatchSet.Id> limitToPsId, String project,
       Map<PatchSet.Id, PatchSet> map) throws OrmException {
     Map<String, RevisionInfo> res = Maps.newLinkedHashMap();
     for (PatchSet in : map.values()) {
       if ((has(ALL_REVISIONS)
           || in.getId().equals(cd.change().currentPatchSetId()))
           && ctl.isPatchVisible(in, db.get())) {
-        res.put(in.getRevision().get(), toRevisionInfo(ctl, cd, in, project));
+        res.put(in.getRevision().get(), toRevisionInfo(ctl, cd, in));
       }
     }
     return res;
@@ -781,18 +862,21 @@
   }
 
   private RevisionInfo toRevisionInfo(ChangeControl ctl, ChangeData cd,
-      PatchSet in, String project) throws OrmException {
+      PatchSet in) throws OrmException {
     RevisionInfo out = new RevisionInfo();
     out.isCurrent = in.getId().equals(cd.change().currentPatchSetId());
     out._number = in.getId().get();
+    out.ref = in.getRefName();
+    out.created = in.getCreatedOn();
+    out.uploader = accountLoader.get(in.getUploader());
     out.draft = in.isDraft() ? true : null;
-    out.fetch = makeFetchMap(ctl, cd, in);
+    out.fetch = makeFetchMap(ctl, in);
 
     if (has(ALL_COMMITS) || (out.isCurrent && has(CURRENT_COMMIT))) {
       try {
-        out.commit = toCommit(in);
+        out.commit = toCommit(in, cd.change().getProject(), has(WEB_LINKS));
       } catch (PatchSetInfoNotAvailableException e) {
-        log.warn("Cannot load PatchSetInfo " + in.getId(), e);
+        throw new OrmException(e);
       }
     }
 
@@ -801,44 +885,31 @@
         out.files = fileInfoJson.toFileInfoMap(cd.change(), in);
         out.files.remove(Patch.COMMIT_MSG);
       } catch (PatchListNotAvailableException e) {
-        log.warn("Cannot load PatchList " + in.getId(), e);
+        throw new OrmException(e);
       }
     }
 
     if ((out.isCurrent || (out.draft != null && out.draft))
         && has(CURRENT_ACTIONS)
         && userProvider.get().isIdentifiedUser()) {
-      out.actions = Maps.newTreeMap();
-      for (UiAction.Description d : UiActions.from(
-          revisions,
-          new RevisionResource(new ChangeResource(ctl), in),
-          userProvider)) {
-        out.actions.put(d.getId(), new ActionInfo(d));
-      }
+
+      actionJson.addRevisionActions(out,
+          new RevisionResource(new ChangeResource(ctl, rebaseChange), in));
     }
 
     if (has(DRAFT_COMMENTS)
         && userProvider.get().isIdentifiedUser()) {
       IdentifiedUser user = (IdentifiedUser)userProvider.get();
       out.hasDraftComments =
-          db.get().patchComments()
-              .draftByPatchSetAuthor(in.getId(), user.getAccountId())
-              .iterator().hasNext()
+          plcUtil.draftByPatchSetAuthor(db.get(), in.getId(),
+              user.getAccountId(), ctl.getNotes()).iterator().hasNext()
           ? true
           : null;
     }
-
-    if (has(WEB_LINKS)) {
-      out.webLinks = Lists.newArrayList();
-      for (WebLinkInfo link : webLinks.get().getPatchSetLinks(
-          project, in.getRevision().get())) {
-        out.webLinks.add(link);
-      }
-    }
     return out;
   }
 
-  CommitInfo toCommit(PatchSet in)
+  CommitInfo toCommit(PatchSet in, Project.NameKey project, boolean addLinks)
       throws PatchSetInfoNotAvailableException {
     PatchSetInfo info = patchSetInfoFactory.get(db.get(), in.getId());
     CommitInfo commit = new CommitInfo();
@@ -847,16 +918,28 @@
     commit.committer = toGitPerson(info.getCommitter());
     commit.subject = info.getSubject();
     commit.message = info.getMessage();
+
+    if (addLinks) {
+      FluentIterable<WebLinkInfo> links =
+          webLinks.getPatchSetLinks(project, in.getRevision().get());
+      commit.webLinks = links.isEmpty() ? null : links.toList();
+    }
+
     for (ParentInfo parent : info.getParents()) {
       CommitInfo i = new CommitInfo();
       i.commit = parent.id.get();
       i.subject = parent.shortMessage;
+      if (addLinks) {
+        FluentIterable<WebLinkInfo> parentLinks =
+            webLinks.getPatchSetLinks(project, parent.id.get());
+        i.webLinks = parentLinks.isEmpty() ? null : parentLinks.toList();
+      }
       commit.parents.add(i);
     }
     return commit;
   }
 
-  private Map<String, FetchInfo> makeFetchMap(ChangeControl ctl, ChangeData cd, PatchSet in)
+  private Map<String, FetchInfo> makeFetchMap(ChangeControl ctl, PatchSet in)
       throws OrmException {
     Map<String, FetchInfo> r = Maps.newLinkedHashMap();
 
@@ -880,21 +963,29 @@
       r.put(schemeName, fetchInfo);
 
       if (has(DOWNLOAD_COMMANDS)) {
-        for (DynamicMap.Entry<DownloadCommand> e2 : downloadCommands) {
-          String commandName = e2.getExportName();
-          DownloadCommand command = e2.getProvider().get();
-          String c = command.getCommand(scheme, projectName, refName);
-          if (c != null) {
-            addCommand(fetchInfo, commandName, c);
-          }
-        }
+        populateFetchMap(scheme, downloadCommands, projectName, refName,
+            fetchInfo);
       }
     }
 
     return r;
   }
 
-  private void addCommand(FetchInfo fetchInfo, String commandName, String c) {
+  public static void populateFetchMap(DownloadScheme scheme,
+      DynamicMap<DownloadCommand> commands, String projectName,
+      String refName, FetchInfo fetchInfo) {
+    for (DynamicMap.Entry<DownloadCommand> e2 : commands) {
+      String commandName = e2.getExportName();
+      DownloadCommand command = e2.getProvider().get();
+      String c = command.getCommand(scheme, projectName, refName);
+      if (c != null) {
+        addCommand(fetchInfo, commandName, c);
+      }
+    }
+  }
+
+  private static void addCommand(FetchInfo fetchInfo, String commandName,
+      String c) {
     if (fetchInfo.commands == null) {
       fetchInfo.commands = Maps.newTreeMap();
     }
@@ -910,83 +1001,36 @@
     return p;
   }
 
-  public static class ChangeInfo {
-    public String id;
-    public String project;
-    public String branch;
-    public String topic;
-    public String changeId;
-    public String subject;
-    public Change.Status status;
-    public Timestamp created;
-    public Timestamp updated;
-    public Boolean starred;
-    public Boolean reviewed;
-    public Boolean mergeable;
-    public Integer insertions;
-    public Integer deletions;
-
-    public String _sortkey;
-    public int _number;
-
-    public AccountInfo owner;
-
-    public Map<String, ActionInfo> actions;
-    public Map<String, LabelInfo> labels;
-    public Map<String, Collection<String>> permittedLabels;
-    public Collection<AccountInfo> removableReviewers;
-    public Collection<ChangeMessageInfo> messages;
-
-    public String currentRevision;
-    public Map<String, RevisionInfo> revisions;
-    public Boolean _moreChanges;
-
-    void finish() {
-      id = Joiner.on('~').join(
-          Url.encode(project),
-          Url.encode(branch),
-          Url.encode(changeId));
-    }
+  static void finish(ChangeInfo info) {
+    info.id = Joiner.on('~').join(
+        Url.encode(info.project),
+        Url.encode(info.branch),
+        Url.encode(info.changeId));
   }
 
-  public static class LabelInfo {
-    transient SubmitRecord.Label.Status _status;
-
-    public AccountInfo approved;
-    public AccountInfo rejected;
-    public AccountInfo recommended;
-    public AccountInfo disliked;
-    public List<ApprovalInfo> all;
-
-    public Map<String, String> values;
-
-    public Short value;
-    public Short defaultValue;
-    public Boolean optional;
-    public Boolean blocking;
-
-    void addApproval(ApprovalInfo ai) {
-      if (all == null) {
-        all = Lists.newArrayList();
-      }
-      all.add(ai);
+  private static void addApproval(LabelInfo label, ApprovalInfo approval) {
+    if (label.all == null) {
+      label.all = Lists.newArrayList();
     }
+    label.all.add(approval);
   }
 
-  public static class ApprovalInfo extends AccountInfo {
-    public Integer value;
-    public Timestamp date;
+  @AutoValue
+  abstract static class LabelWithStatus {
+    private static final Function<LabelWithStatus, LabelInfo> TO_LABEL_INFO =
+        new Function<LabelWithStatus, LabelInfo>() {
+          @Override
+          public LabelInfo apply(LabelWithStatus in) {
+            return in.label();
+          }
+        };
 
-    ApprovalInfo(Account.Id id) {
-      super(id);
+    private static LabelWithStatus create(LabelInfo label,
+        SubmitRecord.Label.Status status) {
+      return new AutoValue_ChangeJson_LabelWithStatus(label, status);
     }
-  }
 
-  public static class ChangeMessageInfo {
-    public String id;
-    public AccountInfo author;
-    public Timestamp date;
-    public String message;
-    public Integer _revisionNumber;
+    abstract LabelInfo label();
+    @Nullable abstract SubmitRecord.Label.Status status();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKind.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKind.java
index c6e088f..d22d6ff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKind.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKind.java
@@ -22,6 +22,9 @@
   /** Conflict-free merge between the new parent and the prior patch set. */
   TRIVIAL_REBASE,
 
-  /** Same tree and same parents. */
-  NO_CODE_CHANGE;
+  /** Same tree and same parent tree. */
+  NO_CODE_CHANGE,
+
+  /** Same tree, parent tree, same commit message. */
+  NO_CHANGE;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index 8d98a4c..23039aa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -19,7 +19,6 @@
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Objects;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.cache.Weigher;
@@ -53,6 +52,7 @@
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
 import java.util.Collection;
+import java.util.Objects;
 import java.util.concurrent.ExecutionException;
 
 public class ChangeKindCacheImpl implements ChangeKindCache {
@@ -156,16 +156,16 @@
     public boolean equals(Object o) {
       if (o instanceof Key) {
         Key k = (Key) o;
-        return Objects.equal(prior, k.prior)
-            && Objects.equal(next, k.next)
-            && Objects.equal(strategyName, k.strategyName);
+        return Objects.equals(prior, k.prior)
+            && Objects.equals(next, k.next)
+            && Objects.equals(strategyName, k.strategyName);
       }
       return false;
     }
 
     @Override
     public int hashCode() {
-      return Objects.hashCode(prior, next, strategyName);
+      return Objects.hash(prior, next, strategyName);
     }
 
     private void writeObject(ObjectOutputStream out) throws IOException {
@@ -185,35 +185,33 @@
   private static class Loader extends CacheLoader<Key, ChangeKind> {
     @Override
     public ChangeKind load(Key key) throws IOException {
-      if (Objects.equal(key.prior, key.next)) {
+      if (Objects.equals(key.prior, key.next)) {
         return ChangeKind.NO_CODE_CHANGE;
       }
 
-      RevWalk walk = new RevWalk(key.repo);
-      try {
+      try (RevWalk walk = new RevWalk(key.repo)) {
         RevCommit prior = walk.parseCommit(key.prior);
         walk.parseBody(prior);
         RevCommit next = walk.parseCommit(key.next);
         walk.parseBody(next);
 
         if (!next.getFullMessage().equals(prior.getFullMessage())) {
-          if (next.getTree() == prior.getTree() && isSameParents(prior, next)) {
+          if (isSameDeltaAndTree(prior, next)) {
             return ChangeKind.NO_CODE_CHANGE;
           } else {
             return ChangeKind.REWORK;
           }
         }
 
+        if (isSameDeltaAndTree(prior, next)) {
+          return ChangeKind.NO_CHANGE;
+        }
+
         if (prior.getParentCount() != 1 || next.getParentCount() != 1) {
           // Trivial rebases done by machine only work well on 1 parent.
           return ChangeKind.REWORK;
         }
 
-        if (next.getTree() == prior.getTree() &&
-           isSameParents(prior, next)) {
-          return ChangeKind.TRIVIAL_REBASE;
-        }
-
         // A trivial rebase can be detected by looking for the next commit
         // having the same tree as would exist when the prior commit is
         // cherry-picked onto the next commit's new first parent.
@@ -228,17 +226,28 @@
         }
       } finally {
         key.repo = null;
-        walk.release();
       }
     }
 
-    private static boolean isSameParents(RevCommit prior, RevCommit next) {
+    private static boolean isSameDeltaAndTree(RevCommit prior, RevCommit next) {
+      if (next.getTree() != prior.getTree()) {
+        return false;
+      }
+
       if (prior.getParentCount() != next.getParentCount()) {
         return false;
       } else if (prior.getParentCount() == 0) {
         return true;
       }
-      return prior.getParent(0).equals(next.getParent(0));
+
+      // Make sure that the prior/next delta is the same - not just the tree.
+      // This is done by making sure that the parent trees are equal.
+      for (int i = 0; i < prior.getParentCount(); i++) {
+        if (next.getParent(i).getTree() != prior.getParent(i).getTree()) {
+          return false;
+        }
+      }
+      return true;
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
index fb8397f..ff4a9f7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
 import com.google.gerrit.extensions.restapi.RestResource;
@@ -23,9 +23,11 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.changedetail.RebaseChange;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.TypeLiteral;
 
 import org.eclipse.jgit.lib.ObjectId;
@@ -35,13 +37,16 @@
       new TypeLiteral<RestView<ChangeResource>>() {};
 
   private final ChangeControl control;
+  private final RebaseChange rebaseChange;
 
-  ChangeResource(ChangeControl control) {
+  public ChangeResource(ChangeControl control, RebaseChange rebaseChange) {
     this.control = control;
+    this.rebaseChange = rebaseChange;
   }
 
   protected ChangeResource(ChangeResource copy) {
     this.control = copy.control;
+    this.rebaseChange = copy.rebaseChange;
   }
 
   public ChangeControl getControl() {
@@ -65,14 +70,28 @@
       .putBoolean(user.getStarredChanges().contains(getChange().getId()))
       .putInt(user.isIdentifiedUser()
           ? ((IdentifiedUser) user).getAccountId().get()
-          : 0);
+          : 0)
+      .putBoolean(rebaseChange != null && rebaseChange.canRebase(this));
 
     byte[] buf = new byte[20];
+    ObjectId noteId;
+    try {
+      noteId = getNotes().loadRevision();
+    } catch (OrmException e) {
+      noteId = null; // This ETag will be invalidated if it loads next time.
+    }
+    hashObjectId(h, noteId, buf);
+    // TODO(dborowitz): Include more notedb and other related refs, e.g. drafts
+    // and edits.
+
     for (ProjectState p : control.getProjectControl().getProjectState().tree()) {
-      ObjectId id = p.getConfig().getRevision();
-      Objects.firstNonNull(id, ObjectId.zeroId()).copyRawTo(buf, 0);
-      h.putBytes(buf);
+      hashObjectId(h, p.getConfig().getRevision(), buf);
     }
     return h.hash().toString();
   }
+
+  private void hashObjectId(Hasher h, ObjectId id, byte[] buf) {
+    MoreObjects.firstNonNull(id, ObjectId.zeroId()).copyRawTo(buf, 0);
+    h.putBytes(buf);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
index 79c442c..45bb1d4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -21,17 +23,30 @@
 
 import org.eclipse.jgit.lib.Constants;
 
-public class ChangeTriplet {
+@AutoValue
+public abstract class ChangeTriplet {
+  public static String format(Change change) {
+    return format(change.getDest(), change.getKey());
+  }
 
-  private final Change.Key changeKey;
-  private final Project.NameKey projectNameKey;
-  private final Branch.NameKey branchNameKey;
+  private static String format(Branch.NameKey branch, Change.Key change) {
+    return branch.getParentKey().get()
+        + "~" + branch.getShortName()
+        + "~" + change.get();
+  }
 
-  public ChangeTriplet(final String triplet) throws ParseException {
+  /**
+   * Parse a triplet out of a string.
+   *
+   * @param triplet string of the form "project~branch~id".
+   * @return the triplet if the input string has the proper format, or absent if
+   *     not.
+   */
+  public static Optional<ChangeTriplet> parse(String triplet) {
     int t2 = triplet.lastIndexOf('~');
     int t1 = triplet.lastIndexOf('~', t2 - 1);
     if (t1 < 0 || t2 < 0) {
-      throw new ParseException();
+      return Optional.absent();
     }
 
     String project = Url.decode(triplet.substring(0, t1));
@@ -42,30 +57,21 @@
       branch = Constants.R_HEADS + branch;
     }
 
-    changeKey = new Change.Key(changeId);
-    projectNameKey = new Project.NameKey(project);
-    branchNameKey = new Branch.NameKey(projectNameKey, branch);
+    ChangeTriplet result = new AutoValue_ChangeTriplet(
+        new Branch.NameKey(new Project.NameKey(project), branch),
+        new Change.Key(changeId));
+    return Optional.of(result);
   }
 
-  public Change.Key getChangeKey() {
-    return changeKey;
+  public final Project.NameKey project() {
+    return branch().getParentKey();
   }
 
-  public Branch.NameKey getBranchNameKey() {
-    return branchNameKey;
-  }
+  public abstract Branch.NameKey branch();
+  public abstract Change.Key id();
 
-  public static String format(final Change change) {
-    return change.getProject().get() + "~"
-        + change.getDest().getShortName() + "~"
-        + change.getKey().get();
-  }
-
-  public static class ParseException extends Exception {
-    private static final long serialVersionUID = 1L;
-
-    ParseException() {
-      super();
-    }
+  @Override
+  public String toString() {
+    return format(branch(), id());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
index d3e97ca..42f16a3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AcceptsPost;
@@ -25,8 +24,9 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.changedetail.RebaseChange;
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -37,37 +37,39 @@
 import com.google.inject.Singleton;
 
 import java.io.IOException;
-import java.util.Collections;
 import java.util.List;
 
 @Singleton
 public class ChangesCollection implements
     RestCollection<TopLevelResource, ChangeResource>,
     AcceptsPost<TopLevelResource> {
-  private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> user;
   private final ChangeControl.GenericFactory changeControlFactory;
   private final Provider<QueryChanges> queryFactory;
   private final DynamicMap<RestView<ChangeResource>> views;
+  private final ChangeUtil changeUtil;
   private final CreateChange createChange;
   private final ChangeIndexer changeIndexer;
+  private final RebaseChange rebaseChange;
 
   @Inject
   ChangesCollection(
-      Provider<ReviewDb> dbProvider,
       Provider<CurrentUser> user,
       ChangeControl.GenericFactory changeControlFactory,
       Provider<QueryChanges> queryFactory,
       DynamicMap<RestView<ChangeResource>> views,
+      ChangeUtil changeUtil,
       CreateChange createChange,
-      ChangeIndexer changeIndexer) {
-    this.db = dbProvider;
+      ChangeIndexer changeIndexer,
+      RebaseChange rebaseChange) {
     this.user = user;
     this.changeControlFactory = changeControlFactory;
     this.queryFactory = queryFactory;
     this.views = views;
+    this.changeUtil = changeUtil;
     this.createChange = createChange;
     this.changeIndexer = changeIndexer;
+    this.rebaseChange = rebaseChange;
   }
 
   @Override
@@ -83,12 +85,12 @@
   @Override
   public ChangeResource parse(TopLevelResource root, IdString id)
       throws ResourceNotFoundException, OrmException {
-    List<Change> changes = findChanges(id.encoded());
+    List<Change> changes = changeUtil.findChanges(id.encoded());
     if (changes.isEmpty()) {
       Integer changeId = Ints.tryParse(id.get());
       if (changeId != null) {
         try {
-          changeIndexer.delete(changeId);
+          changeIndexer.delete(new Change.Id(changeId));
         } catch (IOException e) {
           throw new ResourceNotFoundException(id.get(), e);
         }
@@ -104,7 +106,7 @@
     } catch (NoSuchChangeException e) {
       throw new ResourceNotFoundException(id);
     }
-    return new ChangeResource(control);
+    return new ChangeResource(control, rebaseChange);
   }
 
   public ChangeResource parse(Change.Id id)
@@ -113,41 +115,8 @@
         IdString.fromUrl(Integer.toString(id.get())));
   }
 
-  public ChangeResource parse(ChangeControl control) throws OrmException {
-    return new ChangeResource(control);
-  }
-
-  private List<Change> findChanges(String id)
-      throws OrmException, ResourceNotFoundException {
-    // Try legacy id
-    if (id.matches("^[1-9][0-9]*$")) {
-      Change c = db.get().changes().get(Change.Id.parse(id));
-      if (c != null) {
-        return ImmutableList.of(c);
-      }
-      return Collections.emptyList();
-    }
-
-    // Try isolated changeId
-    if (!id.contains("~")) {
-      Change.Key key = new Change.Key(id);
-      if (key.get().length() == 41) {
-        return db.get().changes().byKey(key).toList();
-      } else {
-        return db.get().changes().byKeyRange(key, key.max()).toList();
-      }
-    }
-
-    // Try change triplet
-    ChangeTriplet triplet;
-    try {
-        triplet = new ChangeTriplet(id);
-    } catch (ChangeTriplet.ParseException e) {
-        throw new ResourceNotFoundException(id);
-    }
-    return db.get().changes().byBranchKey(
-        triplet.getBranchNameKey(),
-        triplet.getChangeKey()).toList();
+  public ChangeResource parse(ChangeControl control) {
+    return new ChangeResource(control, rebaseChange);
   }
 
   @SuppressWarnings("unchecked")
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
new file mode 100644
index 0000000..5c9fe91
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.api.changes.FixInput;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+public class Check implements RestReadView<ChangeResource>,
+    RestModifyView<ChangeResource, FixInput> {
+  private final ChangeJson json;
+
+  @Inject
+  Check(ChangeJson json) {
+    this.json = json;
+    json.addOption(ListChangesOption.CHECK);
+  }
+
+  @Override
+  public Response<ChangeInfo> apply(ChangeResource rsrc) throws OrmException {
+    return Response.withMustRevalidate(json.format(rsrc));
+  }
+
+  @Override
+  public Response<ChangeInfo> apply(ChangeResource rsrc, FixInput input)
+      throws AuthException, OrmException {
+    ChangeControl ctl = rsrc.getControl();
+    if (!ctl.isOwner()
+        && !ctl.getProjectControl().isOwner()
+        && !ctl.getCurrentUser().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("Not owner");
+    }
+    return Response.withMustRevalidate(json.fix(input).format(rsrc));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
index d034a10..efcd6d9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -23,9 +24,7 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
@@ -83,15 +82,15 @@
           + input.destination);
     }
 
-    final PatchSet.Id patchSetId = revision.getPatchSet().getId();
     try {
-      Change.Id cherryPickedChangeId = cherryPickChange.cherryPick(
-          patchSetId, input.message,
-          input.destination, refControl);
+      Change.Id cherryPickedChangeId =
+          cherryPickChange.cherryPick(revision.getChange(),
+              revision.getPatchSet(), input.message, input.destination,
+              refControl);
       return json.format(cherryPickedChangeId);
     } catch (InvalidChangeOperationException e) {
       throw new BadRequestException(e.getMessage());
-    } catch (MergeException  e) {
+    } catch (MergeException e) {
       throw new ResourceConflictException(e.getMessage());
     } catch (NoSuchChangeException e) {
       throw new ResourceNotFoundException(e.getMessage());
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 b1f6d8c..b386894 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -14,30 +14,36 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.common.FooterConstants;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeConflictException;
 import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.MergeIdenticalTreeException;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.ssh.NoSshInfo;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -52,7 +58,6 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.FooterKey;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
@@ -65,27 +70,32 @@
 @Singleton
 public class CherryPickChange {
 
-  private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
-
   private final Provider<ReviewDb> db;
+  private final Provider<InternalChangeQuery> queryProvider;
   private final GitRepositoryManager gitManager;
   private final TimeZone serverTimeZone;
   private final Provider<CurrentUser> currentUser;
   private final CommitValidators.Factory commitValidatorsFactory;
   private final ChangeInserter.Factory changeInserterFactory;
   private final PatchSetInserter.Factory patchSetInserterFactory;
-  final MergeUtil.Factory mergeUtilFactory;
+  private final MergeUtil.Factory mergeUtilFactory;
+  private final ChangeMessagesUtil changeMessagesUtil;
+  private final ChangeUpdate.Factory updateFactory;
 
   @Inject
-  CherryPickChange(final Provider<ReviewDb> db,
-      @GerritPersonIdent final PersonIdent myIdent,
-      final GitRepositoryManager gitManager,
-      final Provider<CurrentUser> currentUser,
-      final CommitValidators.Factory commitValidatorsFactory,
-      final ChangeInserter.Factory changeInserterFactory,
-      final PatchSetInserter.Factory patchSetInserterFactory,
-      final MergeUtil.Factory mergeUtilFactory) {
+  CherryPickChange(Provider<ReviewDb> db,
+      Provider<InternalChangeQuery> queryProvider,
+      @GerritPersonIdent PersonIdent myIdent,
+      GitRepositoryManager gitManager,
+      Provider<CurrentUser> currentUser,
+      CommitValidators.Factory commitValidatorsFactory,
+      ChangeInserter.Factory changeInserterFactory,
+      PatchSetInserter.Factory patchSetInserterFactory,
+      MergeUtil.Factory mergeUtilFactory,
+      ChangeMessagesUtil changeMessagesUtil,
+      ChangeUpdate.Factory updateFactory) {
     this.db = db;
+    this.queryProvider = queryProvider;
     this.gitManager = gitManager;
     this.serverTimeZone = myIdent.getTimeZone();
     this.currentUser = currentUser;
@@ -93,115 +103,105 @@
     this.changeInserterFactory = changeInserterFactory;
     this.patchSetInserterFactory = patchSetInserterFactory;
     this.mergeUtilFactory = mergeUtilFactory;
+    this.changeMessagesUtil = changeMessagesUtil;
+    this.updateFactory = updateFactory;
   }
 
-  public Change.Id cherryPick(final PatchSet.Id patchSetId,
+  public Change.Id cherryPick(Change change, PatchSet patch,
       final String message, final String destinationBranch,
       final RefControl refControl) throws NoSuchChangeException,
-      EmailException, OrmException, MissingObjectException,
+      OrmException, MissingObjectException,
       IncorrectObjectTypeException, IOException,
       InvalidChangeOperationException, MergeException {
 
-    final Change.Id changeId = patchSetId.getParentKey();
-    final PatchSet patch = db.get().patchSets().get(patchSetId);
-    if (patch == null) {
-      throw new NoSuchChangeException(changeId);
-    }
     if (destinationBranch == null || destinationBranch.length() == 0) {
       throw new InvalidChangeOperationException(
           "Cherry Pick: Destination branch cannot be null or empty");
     }
 
-    Project.NameKey project = db.get().changes().get(changeId).getProject();
+    Project.NameKey project = change.getProject();
     IdentifiedUser identifiedUser = (IdentifiedUser) currentUser.get();
-    final Repository git;
-    try {
-      git = gitManager.openRepository(project);
-    } catch (RepositoryNotFoundException e) {
-      throw new NoSuchChangeException(changeId, e);
-    }
-
-    try {
-      RevWalk revWalk = new RevWalk(git);
-      try {
-        Ref destRef = git.getRef(destinationBranch);
-        if (destRef == null) {
-          throw new InvalidChangeOperationException("Branch "
-              + destinationBranch + " does not exist.");
-        }
-
-        final RevCommit mergeTip = revWalk.parseCommit(destRef.getObjectId());
-
-        RevCommit commitToCherryPick =
-            revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
-
-        PersonIdent committerIdent =
-            identifiedUser.newCommitterIdent(TimeUtil.nowTs(),
-                serverTimeZone);
-
-        final ObjectId computedChangeId =
-            ChangeIdUtil
-                .computeChangeId(commitToCherryPick.getTree(), mergeTip,
-                    commitToCherryPick.getAuthorIdent(), committerIdent, message);
-        String commitMessage =
-            ChangeIdUtil.insertId(message, computedChangeId).trim() + '\n';
-
-        RevCommit cherryPickCommit;
-        ObjectInserter oi = git.newObjectInserter();
-        try {
-          ProjectState projectState = refControl.getProjectControl().getProjectState();
-          cherryPickCommit =
-              mergeUtilFactory.create(projectState).createCherryPickFromCommit(git, oi, mergeTip,
-                  commitToCherryPick, committerIdent, commitMessage, revWalk);
-        } finally {
-          oi.release();
-        }
-
-        if (cherryPickCommit == null) {
-          throw new MergeException("Cherry pick failed");
-        }
-
-        Change.Key changeKey;
-        final List<String> idList = cherryPickCommit.getFooterLines(CHANGE_ID);
-        if (!idList.isEmpty()) {
-          final String idStr = idList.get(idList.size() - 1).trim();
-          changeKey = new Change.Key(idStr);
-        } else {
-          changeKey = new Change.Key("I" + computedChangeId.name());
-        }
-
-        List<Change> destChanges =
-            db.get().changes()
-                .byBranchKey(
-                    new Branch.NameKey(db.get().changes().get(changeId).getProject(),
-                        destRef.getName()), changeKey).toList();
-
-        if (destChanges.size() > 1) {
-          throw new InvalidChangeOperationException("Several changes with key "
-              + changeKey + " reside on the same branch. "
-              + "Cannot create a new patch set.");
-        } else if (destChanges.size() == 1) {
-          // The change key exists on the destination branch. The cherry pick
-          // will be added as a new patch set.
-          return insertPatchSet(git, revWalk, destChanges.get(0), patchSetId,
-              cherryPickCommit, refControl, identifiedUser);
-        } else {
-          // Change key not found on destination branch. We can create a new
-          // change.
-          return createNewChange(git, revWalk, changeKey, project, patchSetId, destRef,
-              cherryPickCommit, refControl, identifiedUser);
-        }
-      } finally {
-        revWalk.release();
+    try (Repository git = gitManager.openRepository(project);
+        RevWalk revWalk = new RevWalk(git)) {
+      Ref destRef = git.getRef(destinationBranch);
+      if (destRef == null) {
+        throw new InvalidChangeOperationException("Branch "
+            + destinationBranch + " does not exist.");
       }
-    } finally {
-      git.close();
+
+      final RevCommit mergeTip = revWalk.parseCommit(destRef.getObjectId());
+
+      RevCommit commitToCherryPick =
+          revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
+
+      PersonIdent committerIdent =
+          identifiedUser.newCommitterIdent(TimeUtil.nowTs(),
+              serverTimeZone);
+
+      final ObjectId computedChangeId =
+          ChangeIdUtil
+              .computeChangeId(commitToCherryPick.getTree(), mergeTip,
+                  commitToCherryPick.getAuthorIdent(), committerIdent, message);
+      String commitMessage =
+          ChangeIdUtil.insertId(message, computedChangeId).trim() + '\n';
+
+      RevCommit cherryPickCommit;
+      try (ObjectInserter oi = git.newObjectInserter()) {
+        ProjectState projectState = refControl.getProjectControl().getProjectState();
+        cherryPickCommit =
+            mergeUtilFactory.create(projectState).createCherryPickFromCommit(git, oi, mergeTip,
+                commitToCherryPick, committerIdent, commitMessage, revWalk);
+      } catch (MergeIdenticalTreeException | MergeConflictException e) {
+        throw new MergeException("Cherry pick failed: " + e.getMessage());
+      }
+
+      Change.Key changeKey;
+      final List<String> idList = cherryPickCommit.getFooterLines(
+          FooterConstants.CHANGE_ID);
+      if (!idList.isEmpty()) {
+        final String idStr = idList.get(idList.size() - 1).trim();
+        changeKey = new Change.Key(idStr);
+      } else {
+        changeKey = new Change.Key("I" + computedChangeId.name());
+      }
+
+      Branch.NameKey newDest =
+          new Branch.NameKey(change.getProject(), destRef.getName());
+      List<ChangeData> destChanges = queryProvider.get()
+          .setLimit(2)
+          .byBranchKey(newDest, changeKey);
+      if (destChanges.size() > 1) {
+        throw new InvalidChangeOperationException("Several changes with key "
+            + changeKey + " reside on the same branch. "
+            + "Cannot create a new patch set.");
+      } else if (destChanges.size() == 1) {
+        // The change key exists on the destination branch. The cherry pick
+        // will be added as a new patch set.
+        return insertPatchSet(git, revWalk, destChanges.get(0).change(),
+            cherryPickCommit, refControl, identifiedUser);
+      } else {
+        // Change key not found on destination branch. We can create a new
+        // change.
+        Change newChange = createNewChange(git, revWalk, changeKey, project,
+            destRef, cherryPickCommit, refControl,
+            identifiedUser, change.getTopic());
+
+        addMessageToSourceChange(change, patch.getId(), destinationBranch,
+            cherryPickCommit, identifiedUser, refControl);
+
+        addMessageToDestinationChange(newChange, change.getDest().getShortName(),
+            identifiedUser, refControl);
+
+        return newChange.getId();
+      }
+    } catch (RepositoryNotFoundException e) {
+      throw new NoSuchChangeException(change.getId(), e);
     }
   }
 
   private Change.Id insertPatchSet(Repository git, RevWalk revWalk, Change change,
-      PatchSet.Id patchSetId, RevCommit cherryPickCommit,
-      RefControl refControl, IdentifiedUser identifiedUser)
+      RevCommit cherryPickCommit, RefControl refControl,
+      IdentifiedUser identifiedUser)
       throws InvalidChangeOperationException, IOException, OrmException,
       NoSuchChangeException {
     final ChangeControl changeControl =
@@ -214,22 +214,24 @@
       .setMessage("Uploaded patch set " + newPatchSetId.get() + ".")
       .setDraft(current.isDraft())
       .setUploader(identifiedUser.getAccountId())
-      .setCopyLabels(true)
+      .setSendMail(false)
       .insert();
     return change.getId();
   }
 
-  private Change.Id createNewChange(Repository git, RevWalk revWalk,
-      Change.Key changeKey, Project.NameKey project, PatchSet.Id patchSetId,
+  private Change createNewChange(Repository git, RevWalk revWalk,
+      Change.Key changeKey, Project.NameKey project,
       Ref destRef, RevCommit cherryPickCommit, RefControl refControl,
-      IdentifiedUser identifiedUser)
+      IdentifiedUser identifiedUser, String topic)
       throws OrmException, InvalidChangeOperationException, IOException {
     Change change =
         new Change(changeKey, new Change.Id(db.get().nextChangeId()),
             identifiedUser.getAccountId(), new Branch.NameKey(project,
                 destRef.getName()), TimeUtil.nowTs());
+    change.setTopic(topic);
     ChangeInserter ins =
-        changeInserterFactory.create(refControl, change, cherryPickCommit);
+        changeInserterFactory.create(refControl.getProjectControl(), change,
+            cherryPickCommit);
     PatchSet newPatchSet = ins.getPatchSet();
 
     CommitValidators commitValidators =
@@ -256,31 +258,51 @@
           change.getDest().getParentKey().get(), ru.getResult()));
     }
 
-    ins.setMessage(buildChangeMessage(patchSetId, change, cherryPickCommit,
-        identifiedUser))
-        .insert();
+    ins.insert();
 
-    return change.getId();
+    return change;
   }
 
-  private ChangeMessage buildChangeMessage(PatchSet.Id patchSetId, Change dest,
-      RevCommit cherryPickCommit, IdentifiedUser identifiedUser)
-      throws OrmException {
-    ChangeMessage cmsg = new ChangeMessage(
+  private void addMessageToSourceChange(Change change, PatchSet.Id patchSetId,
+      String destinationBranch, RevCommit cherryPickCommit,
+      IdentifiedUser identifiedUser, RefControl refControl) throws OrmException {
+    ChangeMessage changeMessage = new ChangeMessage(
         new ChangeMessage.Key(
             patchSetId.getParentKey(), ChangeUtil.messageUUID(db.get())),
             identifiedUser.getAccountId(), TimeUtil.nowTs(), patchSetId);
-    String destBranchName = dest.getDest().get();
-    StringBuilder msgBuf = new StringBuilder("Patch Set ")
+    StringBuilder sb = new StringBuilder("Patch Set ")
         .append(patchSetId.get())
         .append(": Cherry Picked")
         .append("\n\n")
         .append("This patchset was cherry picked to branch ")
-        .append(destBranchName.substring(
-            destBranchName.indexOf("refs/heads/") + "refs/heads/".length()))
+        .append(destinationBranch)
         .append(" as commit ")
         .append(cherryPickCommit.getId().getName());
-    cmsg.setMessage(msgBuf.toString());
-    return cmsg;
+    changeMessage.setMessage(sb.toString());
+
+    ChangeControl ctl = refControl.getProjectControl().controlFor(change);
+    ChangeUpdate update = updateFactory.create(ctl, change.getCreatedOn());
+    changeMessagesUtil.addChangeMessage(db.get(), update, changeMessage);
+  }
+
+  private void addMessageToDestinationChange(Change change, String sourceBranch,
+      IdentifiedUser identifiedUser, RefControl refControl) throws OrmException {
+    PatchSet.Id patchSetId =
+        db.get().patchSets().get(change.currentPatchSetId()).getId();
+    ChangeMessage changeMessage = new ChangeMessage(
+        new ChangeMessage.Key(
+            patchSetId.getParentKey(), ChangeUtil.messageUUID(db.get())),
+            identifiedUser.getAccountId(), TimeUtil.nowTs(), patchSetId);
+
+    StringBuilder sb = new StringBuilder("Patch Set ")
+      .append(patchSetId.get())
+      .append(": Cherry Picked from branch ")
+      .append(sourceBranch)
+      .append(".");
+    changeMessage.setMessage(sb.toString());
+
+    ChangeControl ctl = refControl.getProjectControl().controlFor(change);
+    ChangeUpdate update = updateFactory.create(ctl, change.getCreatedOn());
+    changeMessagesUtil.addChangeMessage(db.get(), update, changeMessage);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java
deleted file mode 100644
index adc1644..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import com.google.common.base.Strings;
-import com.google.gerrit.common.changes.Side;
-import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.CommentRange;
-import com.google.gerrit.server.account.AccountInfo;
-
-import java.sql.Timestamp;
-
-public class CommentInfo {
-  String id;
-  String path;
-  Side side;
-  Integer line;
-  String inReplyTo;
-  String message;
-  Timestamp updated;
-  AccountInfo author;
-  CommentRange range;
-
-  CommentInfo(PatchLineComment c, AccountInfo.Loader accountLoader) {
-    id = Url.encode(c.getKey().get());
-    path = c.getKey().getParentKey().getFileName();
-    if (c.getSide() == 0) {
-      side = Side.PARENT;
-    }
-    if (c.getLine() > 0) {
-      line = c.getLine();
-    }
-    inReplyTo = Url.encode(c.getParentUuid());
-    message = Strings.emptyToNull(c.getMessage());
-    updated = c.getWrittenOn();
-    range = c.getRange();
-    if (accountLoader != null) {
-      author = accountLoader.get(c.getAuthor());
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
new file mode 100644
index 0000000..4a52fc5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
@@ -0,0 +1,137 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.client.Comment.Range;
+import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.CommentRange;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.server.account.AccountLoader;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+@Singleton
+class CommentJson {
+
+  private final AccountLoader.Factory accountLoaderFactory;
+
+  @Inject
+  CommentJson(AccountLoader.Factory accountLoaderFactory) {
+    this.accountLoaderFactory = accountLoaderFactory;
+  }
+
+  CommentInfo format(PatchLineComment c) throws OrmException {
+    return format(c, true);
+  }
+
+  CommentInfo format(PatchLineComment c, boolean fill) throws OrmException {
+    AccountLoader loader = null;
+    if (fill) {
+      loader = accountLoaderFactory.create(true);
+    }
+    CommentInfo commentInfo = toCommentInfo(c, loader);
+    if (fill) {
+      loader.fill();
+    }
+    return commentInfo;
+  }
+
+  Map<String, List<CommentInfo>> format(Iterable<PatchLineComment> l,
+      boolean fill) throws OrmException {
+    Map<String, List<CommentInfo>> out = new TreeMap<>();
+    AccountLoader accountLoader = fill
+        ? accountLoaderFactory.create(true)
+        : null;
+
+    for (PatchLineComment c : l) {
+      CommentInfo o = toCommentInfo(c, accountLoader);
+      List<CommentInfo> list = out.get(o.path);
+      if (list == null) {
+        list = new ArrayList<>();
+        out.put(o.path, list);
+      }
+      o.path = null;
+      list.add(o);
+    }
+
+    for (List<CommentInfo> list : out.values()) {
+      Collections.sort(list, new Comparator<CommentInfo>() {
+        @Override
+        public int compare(CommentInfo a, CommentInfo b) {
+          int c = firstNonNull(a.side, Side.REVISION).ordinal()
+                - firstNonNull(b.side, Side.REVISION).ordinal();
+          if (c == 0) {
+            c = firstNonNull(a.line, 0) - firstNonNull(b.line, 0);
+          }
+          if (c == 0) {
+            c = a.id.compareTo(b.id);
+          }
+          return c;
+        }
+      });
+    }
+
+    if (accountLoader != null) {
+      accountLoader.fill();
+    }
+
+    return out;
+  }
+
+  private CommentInfo toCommentInfo(PatchLineComment c, AccountLoader loader) {
+    CommentInfo r = new CommentInfo();
+    r.id = Url.encode(c.getKey().get());
+    r.path = c.getKey().getParentKey().getFileName();
+    if (c.getSide() == 0) {
+      r.side = Side.PARENT;
+    }
+    if (c.getLine() > 0) {
+      r.line = c.getLine();
+    }
+    r.inReplyTo = Url.encode(c.getParentUuid());
+    r.message = Strings.emptyToNull(c.getMessage());
+    r.updated = c.getWrittenOn();
+    r.range = toRange(c.getRange());
+    if (loader != null) {
+      r.author = loader.get(c.getAuthor());
+    }
+    return r;
+  }
+
+  private Range toRange(CommentRange commentRange) {
+    Range range = null;
+    if (commentRange != null) {
+      range = new Range();
+      range.startLine = commentRange.getStartLine();
+      range.startCharacter = commentRange.getStartCharacter();
+      range.endLine = commentRange.getEndLine();
+      range.endCharacter = commentRange.getEndCharacter();
+    }
+    return range;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java
index ec47d01..c535e9e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java
@@ -28,7 +28,7 @@
   private final RevisionResource rev;
   private final PatchLineComment comment;
 
-  CommentResource(RevisionResource rev, PatchLineComment c) {
+  public CommentResource(RevisionResource rev, PatchLineComment c) {
     this.rev = rev;
     this.comment = c;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java
index c987ce8..f25da7c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java
@@ -29,7 +29,7 @@
 import com.google.inject.Singleton;
 
 @Singleton
-class Comments implements ChildCollection<RevisionResource, CommentResource> {
+public class Comments implements ChildCollection<RevisionResource, CommentResource> {
   private final DynamicMap<RestView<CommentResource>> views;
   private final ListComments list;
   private final Provider<ReviewDb> dbProvider;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
new file mode 100644
index 0000000..ee28ed2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -0,0 +1,490 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.Ordering;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.changes.FixInput;
+import com.google.gerrit.extensions.common.ProblemInfo;
+import com.google.gerrit.extensions.common.ProblemInfo.Status;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Checks changes for various kinds of inconsistency and corruption.
+ * <p>
+ * A single instance may be reused for checking multiple changes, but not
+ * concurrently.
+ */
+public class ConsistencyChecker {
+  private static final Logger log =
+      LoggerFactory.getLogger(ConsistencyChecker.class);
+
+  @AutoValue
+  public abstract static class Result {
+    private static Result create(Change.Id id, List<ProblemInfo> problems) {
+      return new AutoValue_ConsistencyChecker_Result(id, null, problems);
+    }
+
+    private static Result create(Change c, List<ProblemInfo> problems) {
+      return new AutoValue_ConsistencyChecker_Result(c.getId(), c, problems);
+    }
+
+    public abstract Change.Id id();
+
+    @Nullable
+    public abstract Change change();
+
+    public abstract List<ProblemInfo> problems();
+  }
+
+  private final Provider<ReviewDb> db;
+  private final GitRepositoryManager repoManager;
+  private final Provider<CurrentUser> user;
+  private final Provider<PersonIdent> serverIdent;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+
+  private FixInput fix;
+  private Change change;
+  private Repository repo;
+  private RevWalk rw;
+
+  private PatchSet currPs;
+  private RevCommit currPsCommit;
+
+  private List<ProblemInfo> problems;
+
+  @Inject
+  ConsistencyChecker(Provider<ReviewDb> db,
+      GitRepositoryManager repoManager,
+      Provider<CurrentUser> user,
+      @GerritPersonIdent Provider<PersonIdent> serverIdent,
+      PatchSetInfoFactory patchSetInfoFactory) {
+    this.db = db;
+    this.repoManager = repoManager;
+    this.user = user;
+    this.serverIdent = serverIdent;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    reset();
+  }
+
+  private void reset() {
+    change = null;
+    repo = null;
+    rw = null;
+    problems = new ArrayList<>();
+  }
+
+  public Result check(ChangeData cd) {
+    return check(cd, null);
+  }
+
+  public Result check(ChangeData cd, @Nullable FixInput f) {
+    reset();
+    try {
+      return check(cd.change(), f);
+    } catch (OrmException e) {
+      error("Error looking up change", e);
+      return Result.create(cd.getId(), problems);
+    }
+  }
+
+  public Result check(Change c) {
+    return check(c, null);
+  }
+
+  public Result check(Change c, @Nullable FixInput f) {
+    reset();
+    fix = f;
+    change = c;
+    try {
+      checkImpl();
+      return Result.create(c, problems);
+    } finally {
+      if (rw != null) {
+        rw.close();
+      }
+      if (repo != null) {
+        repo.close();
+      }
+    }
+  }
+
+  private void checkImpl() {
+    checkOwner();
+    checkCurrentPatchSetEntity();
+
+    // All checks that require the repo.
+    if (!openRepo()) {
+      return;
+    }
+    if (!checkPatchSets()) {
+      return;
+    }
+    checkMerged();
+  }
+
+  private void checkOwner() {
+    try {
+      if (db.get().accounts().get(change.getOwner()) == null) {
+        problem("Missing change owner: " + change.getOwner());
+      }
+    } catch (OrmException e) {
+      error("Failed to look up owner", e);
+    }
+  }
+
+  private void checkCurrentPatchSetEntity() {
+    try {
+      PatchSet.Id psId = change.currentPatchSetId();
+      currPs = db.get().patchSets().get(psId);
+      if (currPs == null) {
+        problem(String.format("Current patch set %d not found", psId.get()));
+      }
+    } catch (OrmException e) {
+      error("Failed to look up current patch set", e);
+    }
+  }
+
+  private boolean openRepo() {
+    Project.NameKey project = change.getDest().getParentKey();
+    try {
+      repo = repoManager.openRepository(project);
+      rw = new RevWalk(repo);
+      return true;
+    } catch (RepositoryNotFoundException e) {
+      return error("Destination repository not found: " + project, e);
+    } catch (IOException e) {
+      return error("Failed to open repository: " + project, e);
+    }
+  }
+
+  private static final Function<PatchSet, Integer> TO_PS_ID =
+      new Function<PatchSet, Integer>() {
+        @Override
+        public Integer apply(PatchSet in) {
+          return in.getId().get();
+        }
+      };
+
+  private static final Ordering<PatchSet> PS_ID_ORDER = Ordering.natural()
+    .onResultOf(TO_PS_ID);
+
+  private boolean checkPatchSets() {
+    List<PatchSet> all;
+    try {
+      all = Lists.newArrayList(db.get().patchSets().byChange(change.getId()));
+    } catch (OrmException e) {
+      return error("Failed to look up patch sets", e);
+    }
+    // Iterate in descending order so deletePatchSet can assume the latest patch
+    // set exists.
+    Collections.sort(all, PS_ID_ORDER.reverse());
+    Multimap<ObjectId, PatchSet> bySha = MultimapBuilder.hashKeys(all.size())
+        .treeSetValues(PS_ID_ORDER)
+        .build();
+    for (PatchSet ps : all) {
+      // Check revision format.
+      ObjectId objId;
+      String rev = ps.getRevision().get();
+      int psNum = ps.getId().get();
+      String refName = ps.getId().toRefName();
+      try {
+        objId = ObjectId.fromString(rev);
+      } catch (IllegalArgumentException e) {
+        error(String.format("Invalid revision on patch set %d: %s", psNum, rev),
+            e);
+        continue;
+      }
+      bySha.put(objId, ps);
+
+      // Check ref existence.
+      ProblemInfo refProblem = null;
+      try {
+        Ref ref = repo.getRef(refName);
+        if (ref == null) {
+          refProblem = problem("Ref missing: " + refName);
+        } else if (!objId.equals(ref.getObjectId())) {
+          String actual = ref.getObjectId() != null
+              ? ref.getObjectId().name()
+              : "null";
+          refProblem = problem(String.format(
+              "Expected %s to point to %s, found %s",
+              ref.getName(), objId.name(), actual));
+        }
+      } catch (IOException e) {
+        error("Error reading ref: " + refName, e);
+        refProblem = lastProblem();
+      }
+
+      // Check object existence.
+      RevCommit psCommit = parseCommit(
+          objId, String.format("patch set %d", psNum));
+      if (psCommit == null) {
+        if (fix != null && fix.deletePatchSetIfCommitMissing) {
+          deletePatchSet(lastProblem(), ps.getId());
+        }
+        continue;
+      } else if (refProblem != null && fix != null) {
+        fixPatchSetRef(refProblem, ps);
+      }
+      if (ps.getId().equals(change.currentPatchSetId())) {
+        currPsCommit = psCommit;
+      }
+    }
+
+    // Check for duplicates.
+    for (Map.Entry<ObjectId, Collection<PatchSet>> e
+        : bySha.asMap().entrySet()) {
+      if (e.getValue().size() > 1) {
+        problem(String.format("Multiple patch sets pointing to %s: %s",
+            e.getKey().name(),
+            Collections2.transform(e.getValue(), TO_PS_ID)));
+      }
+    }
+
+    return currPs != null && currPsCommit != null;
+  }
+
+  private void checkMerged() {
+    String refName = change.getDest().get();
+    Ref dest;
+    try {
+      dest = repo.getRef(refName);
+    } catch (IOException e) {
+      problem("Failed to look up destination ref: " + refName);
+      return;
+    }
+    if (dest == null) {
+      problem("Destination ref not found (may be new branch): "
+          + change.getDest().get());
+      return;
+    }
+    RevCommit tip = parseCommit(dest.getObjectId(),
+        "destination ref " + refName);
+    if (tip == null) {
+      return;
+    }
+    boolean merged;
+    try {
+      merged = rw.isMergedInto(currPsCommit, tip);
+    } catch (IOException e) {
+      problem("Error checking whether patch set " + currPs.getId().get()
+          + " is merged");
+      return;
+    }
+    if (merged && change.getStatus() != Change.Status.MERGED) {
+      ProblemInfo p = problem(String.format(
+          "Patch set %d (%s) is merged into destination ref %s (%s), but change"
+          + " status is %s", currPs.getId().get(), currPsCommit.name(),
+          refName, tip.name(), change.getStatus()));
+      if (fix != null) {
+        fixMerged(p);
+      }
+    } else if (!merged && change.getStatus() == Change.Status.MERGED) {
+      problem(String.format("Patch set %d (%s) is not merged into"
+            + " destination ref %s (%s), but change status is %s",
+            currPs.getId().get(), currPsCommit.name(), refName, tip.name(),
+            change.getStatus()));
+    }
+  }
+
+  private void fixMerged(ProblemInfo p) {
+    try {
+      change = db.get().changes().atomicUpdate(change.getId(),
+          new AtomicUpdate<Change>() {
+            @Override
+            public Change update(Change c) {
+              c.setStatus(Change.Status.MERGED);
+              return c;
+            }
+          });
+      p.status = Status.FIXED;
+      p.outcome = "Marked change as merged";
+    } catch (OrmException e) {
+      log.warn("Error marking " + change.getId() + "as merged", e);
+      p.status = Status.FIX_FAILED;
+      p.outcome = "Error updating status to merged";
+    }
+  }
+
+  private void fixPatchSetRef(ProblemInfo p, PatchSet ps) {
+    try {
+      RefUpdate ru = repo.updateRef(ps.getId().toRefName());
+      ru.setForceUpdate(true);
+      ru.setNewObjectId(ObjectId.fromString(ps.getRevision().get()));
+      ru.setRefLogIdent(newRefLogIdent());
+      ru.setRefLogMessage("Repair patch set ref", true);
+      RefUpdate.Result result = ru.update();
+      switch (result) {
+        case NEW:
+        case FORCED:
+        case FAST_FORWARD:
+        case NO_CHANGE:
+          p.status = Status.FIXED;
+          p.outcome = "Repaired patch set ref";
+          return;
+        default:
+          p.status = Status.FIX_FAILED;
+          p.outcome = "Failed to update patch set ref: " + result;
+          return;
+      }
+    } catch (IOException e) {
+      String msg = "Error fixing patch set ref";
+      log.warn(msg + ' ' + ps.getId().toRefName(), e);
+      p.status = Status.FIX_FAILED;
+      p.outcome = msg;
+    }
+  }
+
+  private void deletePatchSet(ProblemInfo p, PatchSet.Id psId) {
+    ReviewDb db = this.db.get();
+    Change.Id cid = psId.getParentKey();
+    try {
+      db.changes().beginTransaction(cid);
+      try {
+        Change c = db.changes().get(cid);
+        if (c == null) {
+          throw new OrmException("Change missing: " + cid);
+        }
+
+        if (psId.equals(c.currentPatchSetId())) {
+          List<PatchSet> all = Lists.newArrayList(db.patchSets().byChange(cid));
+          if (all.size() == 1 && all.get(0).getId().equals(psId)) {
+            p.status = Status.FIX_FAILED;
+            p.outcome = "Cannot delete patch set; no patch sets would remain";
+            return;
+          }
+          // If there were multiple missing patch sets, assumes deletePatchSet
+          // has been called in decreasing order, so the max remaining PatchSet
+          // is the effective current patch set.
+          Collections.sort(all, PS_ID_ORDER.reverse());
+          PatchSet.Id latest = null;
+          for (PatchSet ps : all) {
+            latest = ps.getId();
+            if (!ps.getId().equals(psId)) {
+              break;
+            }
+          }
+          c.setCurrentPatchSet(patchSetInfoFactory.get(db, latest));
+          db.changes().update(Collections.singleton(c));
+        }
+
+        // Delete dangling primary key references. Don't delete ChangeMessages,
+        // which don't use patch sets as a primary key, and may provide useful
+        // historical information.
+        db.accountPatchReviews().delete(
+            db.accountPatchReviews().byPatchSet(psId));
+        db.patchSetAncestors().delete(
+            db.patchSetAncestors().byPatchSet(psId));
+        db.patchSetApprovals().delete(
+            db.patchSetApprovals().byPatchSet(psId));
+        db.patchComments().delete(
+            db.patchComments().byPatchSet(psId));
+        db.patchSets().deleteKeys(Collections.singleton(psId));
+        db.commit();
+
+        p.status = Status.FIXED;
+        p.outcome = "Deleted patch set";
+      } finally {
+        db.rollback();
+      }
+    } catch (PatchSetInfoNotAvailableException | OrmException e) {
+      String msg = "Error deleting patch set";
+      log.warn(msg + ' ' + psId, e);
+      p.status = Status.FIX_FAILED;
+      p.outcome = msg;
+    }
+  }
+
+  private PersonIdent newRefLogIdent() {
+    CurrentUser u = user.get();
+    if (u.isIdentifiedUser()) {
+      return ((IdentifiedUser) u).newRefLogIdent();
+    } else {
+      return serverIdent.get();
+    }
+  }
+
+  private RevCommit parseCommit(ObjectId objId, String desc) {
+    try {
+      return rw.parseCommit(objId);
+    } catch (MissingObjectException e) {
+      problem(String.format("Object missing: %s: %s", desc, objId.name()));
+    } catch (IncorrectObjectTypeException e) {
+      problem(String.format("Not a commit: %s: %s", desc, objId.name()));
+    } catch (IOException e) {
+      problem(String.format("Failed to look up: %s: %s", desc, objId.name()));
+    }
+    return null;
+  }
+
+  private ProblemInfo problem(String msg) {
+    ProblemInfo p = new ProblemInfo();
+    p.message = msg;
+    problems.add(p);
+    return p;
+  }
+
+  private ProblemInfo lastProblem() {
+    return problems.get(problems.size() - 1);
+  }
+
+  private boolean error(String msg, Throwable t) {
+    problem(msg);
+    // TODO(dborowitz): Expose stack trace to administrators.
+    log.warn("Error in consistency check of change " + change.getId(), t);
+    return false;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
index 263f7f1..4dffd67 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
@@ -15,11 +15,16 @@
 package com.google.gerrit.server.change;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.FooterConstants;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.ChangeStatus;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
@@ -29,13 +34,13 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.api.changes.ChangeInfoMapper;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
@@ -43,13 +48,13 @@
 import com.google.gerrit.server.project.ProjectsCollection;
 import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.ssh.NoSshInfo;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -80,6 +85,8 @@
   private final CommitValidators.Factory commitValidatorsFactory;
   private final ChangeInserter.Factory changeInserterFactory;
   private final ChangeJson json;
+  private final ChangeUtil changeUtil;
+  private final boolean allowDrafts;
 
   @Inject
   CreateChange(Provider<ReviewDb> db,
@@ -89,7 +96,9 @@
       ProjectsCollection projectsCollection,
       CommitValidators.Factory commitValidatorsFactory,
       ChangeInserter.Factory changeInserterFactory,
-      ChangeJson json) {
+      ChangeJson json,
+      ChangeUtil changeUtil,
+      @GerritServerConfig Config config) {
     this.db = db;
     this.gitManager = gitManager;
     this.serverTimeZone = myIdent.getTimeZone();
@@ -98,14 +107,16 @@
     this.commitValidatorsFactory = commitValidatorsFactory;
     this.changeInserterFactory = changeInserterFactory;
     this.json = json;
+    this.changeUtil = changeUtil;
+    this.allowDrafts = config.getBoolean("change", "allowDrafts", true);
   }
 
   @Override
-  public Response<ChangeJson.ChangeInfo> apply(TopLevelResource parent,
+  public Response<ChangeInfo> apply(TopLevelResource parent,
       ChangeInfo input) throws AuthException, OrmException,
       BadRequestException, UnprocessableEntityException, IOException,
-      InvalidChangeOperationException {
-
+      InvalidChangeOperationException, ResourceNotFoundException,
+      MethodNotAllowedException {
     if (Strings.isNullOrEmpty(input.project)) {
       throw new BadRequestException("project must be non-empty");
     }
@@ -123,6 +134,10 @@
           && input.status != ChangeStatus.DRAFT) {
         throw new BadRequestException("unsupported change status");
       }
+
+      if (!allowDrafts && input.status == ChangeStatus.DRAFT) {
+        throw new MethodNotAllowedException("draft workflow is disabled");
+      }
     }
 
     String refName = input.branch;
@@ -143,51 +158,63 @@
     }
 
     Project.NameKey project = rsrc.getNameKey();
-    Repository git = gitManager.openRepository(project);
-
-    try {
-      RevWalk rw = new RevWalk(git);
-      try {
+    try (Repository git = gitManager.openRepository(project);
+        RevWalk rw = new RevWalk(git)) {
+      ObjectId parentCommit;
+      if (input.baseChange != null) {
+        List<Change> changes = changeUtil.findChanges(input.baseChange);
+        if (changes.size() != 1) {
+          throw new InvalidChangeOperationException(
+              "Base change not found: " + input.baseChange);
+        }
+        Change change = Iterables.getOnlyElement(changes);
+        if (!rsrc.getControl().controlFor(change).isVisible(db.get())) {
+          throw new InvalidChangeOperationException(
+              "Base change not found: " + input.baseChange);
+        }
+        PatchSet ps = db.get().patchSets().get(
+            new PatchSet.Id(change.getId(),
+            change.currentPatchSetId().get()));
+        parentCommit = ObjectId.fromString(ps.getRevision().get());
+      } else {
         Ref destRef = git.getRef(refName);
         if (destRef == null) {
           throw new UnprocessableEntityException(String.format(
               "Branch %s does not exist.", refName));
         }
-
-        Timestamp now = TimeUtil.nowTs();
-        IdentifiedUser me = (IdentifiedUser) userProvider.get();
-        PersonIdent author = me.newCommitterIdent(now, serverTimeZone);
-
-        RevCommit mergeTip = rw.parseCommit(destRef.getObjectId());
-        ObjectId id = ChangeIdUtil.computeChangeId(mergeTip.getTree(),
-            mergeTip, author, author, input.subject);
-        String commitMessage = ChangeIdUtil.insertId(input.subject, id);
-
-        RevCommit c = newCommit(git, rw, author, mergeTip, commitMessage);
-
-        Change change = new Change(
-            getChangeId(id, c),
-            new Change.Id(db.get().nextChangeId()),
-            me.getAccountId(),
-            new Branch.NameKey(project, destRef.getName()),
-            now);
-
-        ChangeInserter ins =
-            changeInserterFactory.create(refControl, change, c);
-
-        validateCommit(git, refControl, c, me, ins);
-        updateRef(git, rw, c, change, ins.getPatchSet());
-
-        change.setTopic(input.topic);
-        change.setStatus(ChangeInfoMapper.changeStatus2Status(input.status));
-        ins.insert();
-
-        return Response.created(json.format(change.getId()));
-      } finally {
-        rw.release();
+        parentCommit = destRef.getObjectId();
       }
-    } finally {
-      git.close();
+      RevCommit mergeTip = rw.parseCommit(parentCommit);
+
+      Timestamp now = TimeUtil.nowTs();
+      IdentifiedUser me = (IdentifiedUser) userProvider.get();
+      PersonIdent author = me.newCommitterIdent(now, serverTimeZone);
+
+      ObjectId id = ChangeIdUtil.computeChangeId(mergeTip.getTree(),
+          mergeTip, author, author, input.subject);
+      String commitMessage = ChangeIdUtil.insertId(input.subject, id);
+
+      RevCommit c = newCommit(git, rw, author, mergeTip, commitMessage);
+
+      Change change = new Change(
+          getChangeId(id, c),
+          new Change.Id(db.get().nextChangeId()),
+          me.getAccountId(),
+          new Branch.NameKey(project, refName),
+          now);
+
+      ChangeInserter ins =
+          changeInserterFactory.create(refControl.getProjectControl(),
+              change, c);
+
+      validateCommit(git, refControl, c, me, ins);
+      updateRef(git, rw, c, change, ins.getPatchSet());
+
+      change.setTopic(input.topic);
+      ins.setDraft(input.status != null && input.status == ChangeStatus.DRAFT);
+      ins.insert();
+
+      return Response.created(json.format(change.getId()));
     }
   }
 
@@ -229,7 +256,7 @@
 
   private static Change.Key getChangeId(ObjectId id, RevCommit emptyCommit) {
     List<String> idList = emptyCommit.getFooterLines(
-        MergeUtil.CHANGE_ID);
+        FooterConstants.CHANGE_ID);
     Change.Key changeKey = !idList.isEmpty()
         ? new Change.Key(idList.get(idList.size() - 1).trim())
         : new Change.Key("I" + id.name());
@@ -240,8 +267,7 @@
       PersonIdent authorIdent, RevCommit mergeTip, String commitMessage)
       throws IOException {
     RevCommit emptyCommit;
-    ObjectInserter oi = git.newObjectInserter();
-    try {
+    try (ObjectInserter oi = git.newObjectInserter()) {
       CommitBuilder commit = new CommitBuilder();
       commit.setTreeId(mergeTip.getTree().getId());
       commit.setParentId(mergeTip);
@@ -249,8 +275,6 @@
       commit.setCommitter(authorIdent);
       commit.setMessage(commitMessage);
       emptyCommit = rw.parseCommit(insert(oi, commit));
-    } finally {
-      oi.release();
     }
     return emptyCommit;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
deleted file mode 100644
index 1d2fa406..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import com.google.common.base.Strings;
-import com.google.gerrit.common.changes.Side;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.change.PutDraft.Input;
-import com.google.gerrit.server.util.TimeUtil;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-import java.util.Collections;
-
-@Singleton
-class CreateDraft implements RestModifyView<RevisionResource, Input> {
-  private final Provider<ReviewDb> db;
-
-  @Inject
-  CreateDraft(Provider<ReviewDb> db) {
-    this.db = db;
-  }
-
-  @Override
-  public Response<CommentInfo> apply(RevisionResource rsrc, Input in)
-      throws BadRequestException, OrmException {
-    if (Strings.isNullOrEmpty(in.path)) {
-      throw new BadRequestException("path must be non-empty");
-    } else if (in.message == null || in.message.trim().isEmpty()) {
-      throw new BadRequestException("message must be non-empty");
-    } else if (in.line != null && in.line <= 0) {
-      throw new BadRequestException("line must be > 0");
-    } else if (in.line != null && in.range != null && in.line != in.range.getEndLine()) {
-      throw new BadRequestException("range endLine must be on the same line as the comment");
-    }
-
-    int line = in.line != null
-        ? in.line
-        : in.range != null ? in.range.getEndLine() : 0;
-
-    PatchLineComment c = new PatchLineComment(
-        new PatchLineComment.Key(
-            new Patch.Key(rsrc.getPatchSet().getId(), in.path),
-            ChangeUtil.messageUUID(db.get())),
-        line, rsrc.getAccountId(), Url.decode(in.inReplyTo), TimeUtil.nowTs());
-    c.setSide(in.side == Side.PARENT ? (short) 0 : (short) 1);
-    c.setMessage(in.message.trim());
-    c.setRange(in.range);
-    db.get().patchComments().insert(Collections.singleton(c));
-    return Response.created(new CommentInfo(c, null));
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
new file mode 100644
index 0000000..36b9692
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collections;
+
+@Singleton
+public class CreateDraftComment implements RestModifyView<RevisionResource, DraftInput> {
+  private final Provider<ReviewDb> db;
+  private final ChangeUpdate.Factory updateFactory;
+  private final CommentJson commentJson;
+  private final PatchLineCommentsUtil plcUtil;
+  private final PatchListCache patchListCache;
+
+  @Inject
+  CreateDraftComment(Provider<ReviewDb> db,
+      ChangeUpdate.Factory updateFactory,
+      CommentJson commentJson,
+      PatchLineCommentsUtil plcUtil,
+      PatchListCache patchListCache) {
+    this.db = db;
+    this.updateFactory = updateFactory;
+    this.commentJson = commentJson;
+    this.plcUtil = plcUtil;
+    this.patchListCache = patchListCache;
+  }
+
+  @Override
+  public Response<CommentInfo> apply(RevisionResource rsrc, DraftInput in)
+      throws BadRequestException, OrmException, IOException {
+    if (Strings.isNullOrEmpty(in.path)) {
+      throw new BadRequestException("path must be non-empty");
+    } else if (in.message == null || in.message.trim().isEmpty()) {
+      throw new BadRequestException("message must be non-empty");
+    } else if (in.line != null && in.line <= 0) {
+      throw new BadRequestException("line must be > 0");
+    } else if (in.line != null && in.range != null && in.line != in.range.endLine) {
+      throw new BadRequestException("range endLine must be on the same line as the comment");
+    }
+
+    int line = in.line != null
+        ? in.line
+        : in.range != null ? in.range.endLine : 0;
+
+    Timestamp now = TimeUtil.nowTs();
+    ChangeUpdate update = updateFactory.create(rsrc.getControl(), now);
+
+    PatchLineComment c = new PatchLineComment(
+        new PatchLineComment.Key(
+            new Patch.Key(rsrc.getPatchSet().getId(), in.path),
+            ChangeUtil.messageUUID(db.get())),
+        line, rsrc.getAccountId(), Url.decode(in.inReplyTo), now);
+    c.setSide(in.side == Side.PARENT ? (short) 0 : (short) 1);
+    c.setMessage(in.message.trim());
+    c.setRange(in.range);
+    setCommentRevId(c, patchListCache, rsrc.getChange(), rsrc.getPatchSet());
+    plcUtil.insertComments(db.get(), update, Collections.singleton(c));
+    update.commit();
+    return Response.created(commentJson.format(c, false));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java
new file mode 100644
index 0000000..9bb1f02
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeEdit.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Optional;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.change.DeleteChangeEdit.Input;
+import com.google.gerrit.server.edit.ChangeEdit;
+import com.google.gerrit.server.edit.ChangeEditUtil;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+
+@Singleton
+public class DeleteChangeEdit implements RestModifyView<ChangeResource, Input> {
+  public static class Input {
+  }
+
+  private final ChangeEditUtil editUtil;
+
+  @Inject
+  DeleteChangeEdit(ChangeEditUtil editUtil) {
+    this.editUtil = editUtil;
+  }
+
+  @Override
+  public Response<?> apply(ChangeResource rsrc, Input input)
+      throws AuthException, ResourceNotFoundException, IOException,
+      InvalidChangeOperationException {
+    Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
+    if (edit.isPresent()) {
+      editUtil.delete(edit.get());
+    } else {
+      throw new ResourceNotFoundException();
+    }
+
+    return Response.none();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraft.java
deleted file mode 100644
index 46ae834..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraft.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.DeleteDraft.Input;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-import java.util.Collections;
-
-@Singleton
-class DeleteDraft implements RestModifyView<DraftResource, Input> {
-  static class Input {
-  }
-
-  private final Provider<ReviewDb> db;
-
-  @Inject
-  DeleteDraft(Provider<ReviewDb> db) {
-    this.db = db;
-  }
-
-  @Override
-  public Response<CommentInfo> apply(DraftResource rsrc, Input input)
-      throws OrmException {
-    db.get().patchComments().delete(Collections.singleton(rsrc.getComment()));
-    return Response.none();
-  }
-}
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 28885db..b276aae 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.change;
 
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -25,7 +26,6 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.change.DeleteDraftChange.Input;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -48,7 +48,6 @@
 
   @Inject
   public DeleteDraftChange(Provider<ReviewDb> dbProvider,
-      PatchSetInfoFactory patchSetInfoFactory,
       ChangeUtil changeUtil,
       @GerritServerConfig Config cfg) {
     this.dbProvider = dbProvider;
@@ -59,7 +58,8 @@
   @Override
   public Response<?> apply(ChangeResource rsrc, Input input)
       throws ResourceConflictException, AuthException,
-      ResourceNotFoundException, OrmException, IOException {
+      ResourceNotFoundException, MethodNotAllowedException,
+      OrmException, IOException {
     if (rsrc.getChange().getStatus() != Status.DRAFT) {
       throw new ResourceConflictException("Change is not a draft");
     }
@@ -69,11 +69,11 @@
     }
 
     if (!allowDrafts) {
-      throw new ResourceConflictException("Draft workflow is disabled.");
+      throw new MethodNotAllowedException("draft workflow is disabled");
     }
 
     try {
-      changeUtil.deleteDraftChange(rsrc.getChange().getId());
+      changeUtil.deleteDraftChange(rsrc.getChange());
     } catch (NoSuchChangeException e) {
       throw new ResourceNotFoundException(e.getMessage());
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java
new file mode 100644
index 0000000..c4270a9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
+
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.change.DeleteDraftComment.Input;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+import java.util.Collections;
+
+@Singleton
+public class DeleteDraftComment implements RestModifyView<DraftCommentResource, Input> {
+  static class Input {
+  }
+
+  private final Provider<ReviewDb> db;
+  private final PatchLineCommentsUtil plcUtil;
+  private final ChangeUpdate.Factory updateFactory;
+  private final PatchListCache patchListCache;
+
+  @Inject
+  DeleteDraftComment(Provider<ReviewDb> db,
+      PatchLineCommentsUtil plcUtil,
+      ChangeUpdate.Factory updateFactory,
+      PatchListCache patchListCache) {
+    this.db = db;
+    this.plcUtil = plcUtil;
+    this.updateFactory = updateFactory;
+    this.patchListCache = patchListCache;
+  }
+
+  @Override
+  public Response<CommentInfo> apply(DraftCommentResource rsrc, Input input)
+      throws OrmException, IOException {
+    ChangeUpdate update = updateFactory.create(rsrc.getControl());
+
+    PatchLineComment c = rsrc.getComment();
+    setCommentRevId(c, patchListCache, rsrc.getChange(), rsrc.getPatchSet());
+    plcUtil.deleteComments(db.get(), update, Collections.singleton(c));
+    update.commit();
+    return Response.none();
+  }
+}
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 46d0ed9..a266337 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.change;
 
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -27,6 +28,7 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.change.DeleteDraftPatchSet.Input;
 import com.google.gerrit.server.config.GerritServerConfig;
+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;
@@ -49,33 +51,37 @@
   protected final Provider<ReviewDb> dbProvider;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final ChangeUtil changeUtil;
+  private final ChangeIndexer indexer;
   private final boolean allowDrafts;
 
   @Inject
   public DeleteDraftPatchSet(Provider<ReviewDb> dbProvider,
       PatchSetInfoFactory patchSetInfoFactory,
       ChangeUtil changeUtil,
+      ChangeIndexer indexer,
       @GerritServerConfig Config cfg) {
     this.dbProvider = dbProvider;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.changeUtil = changeUtil;
+    this.indexer = indexer;
     this.allowDrafts = cfg.getBoolean("change", "allowDrafts", true);
   }
 
   @Override
   public Response<?> apply(RevisionResource rsrc, Input input)
       throws AuthException, ResourceNotFoundException,
-      ResourceConflictException, OrmException, IOException {
+      ResourceConflictException, MethodNotAllowedException,
+      OrmException, IOException {
     PatchSet patchSet = rsrc.getPatchSet();
     PatchSet.Id patchSetId = patchSet.getId();
     Change change = rsrc.getChange();
 
     if (!patchSet.isDraft()) {
-      throw new ResourceConflictException("Patch set is not a draft.");
+      throw new ResourceConflictException("Patch set is not a draft");
     }
 
     if (!allowDrafts) {
-      throw new ResourceConflictException("Draft workflow is disabled.");
+      throw new MethodNotAllowedException("draft workflow is disabled");
     }
 
     if (!rsrc.getControl().canDeleteDraft(dbProvider.get())) {
@@ -121,7 +127,7 @@
             .patchSets()
             .byChange(change.getId())
             .toList().size() == 0) {
-      deleteDraftChange(patchSetId);
+      deleteDraftChange(change);
     } else {
       if (change.currentPatchSetId().equals(patchSetId)) {
         updateChange(dbProvider.get(), change,
@@ -133,38 +139,40 @@
     }
   }
 
-  private void deleteDraftChange(PatchSet.Id patchSetId)
+  private void deleteDraftChange(Change change)
       throws OrmException, IOException, ResourceNotFoundException {
     try {
-      changeUtil.deleteDraftChange(patchSetId);
+      changeUtil.deleteDraftChange(change);
     } catch (NoSuchChangeException e) {
       throw new ResourceNotFoundException(e.getMessage());
     }
   }
 
   private PatchSetInfo previousPatchSetInfo(PatchSet.Id patchSetId)
-      throws ResourceNotFoundException {
+      throws OrmException {
     try {
       return patchSetInfoFactory.get(dbProvider.get(),
           new PatchSet.Id(patchSetId.getParentKey(),
               patchSetId.get() - 1));
     } catch (PatchSetInfoNotAvailableException e) {
-        throw new ResourceNotFoundException(e.getMessage());
+        throw new OrmException(e);
     }
   }
 
-  private static void updateChange(final ReviewDb db,
-      final Change change, final PatchSetInfo psInfo)
-      throws OrmException {
-    db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
-      @Override
-      public Change update(Change c) {
-        if (psInfo != null) {
-          c.setCurrentPatchSet(psInfo);
-        }
-        ChangeUtil.updated(c);
-        return c;
-      }
-    });
+  private void updateChange(final ReviewDb db,
+      Change change, final PatchSetInfo psInfo)
+      throws OrmException, IOException  {
+    change = db.changes().atomicUpdate(change.getId(),
+        new AtomicUpdate<Change>() {
+          @Override
+          public Change update(Change c) {
+            if (psInfo != null) {
+              c.setCurrentPatchSet(psInfo);
+            }
+            ChangeUtil.updated(c);
+            return c;
+          }
+        });
+    indexer.index(db, change);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
index e64d3f4..d9512eb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -34,7 +35,6 @@
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftCommentResource.java
similarity index 85%
rename from gerrit-server/src/main/java/com/google/gerrit/server/change/DraftResource.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/change/DraftCommentResource.java
index bcd8902..006b1ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftCommentResource.java
@@ -24,14 +24,14 @@
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.inject.TypeLiteral;
 
-public class DraftResource implements RestResource {
-  public static final TypeLiteral<RestView<DraftResource>> DRAFT_KIND =
-      new TypeLiteral<RestView<DraftResource>>() {};
+public class DraftCommentResource implements RestResource {
+  public static final TypeLiteral<RestView<DraftCommentResource>> DRAFT_COMMENT_KIND =
+      new TypeLiteral<RestView<DraftCommentResource>>() {};
 
   private final RevisionResource rev;
   private final PatchLineComment comment;
 
-  DraftResource(RevisionResource rev, PatchLineComment c) {
+  public DraftCommentResource(RevisionResource rev, PatchLineComment c) {
     this.rev = rev;
     this.comment = c;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftComments.java
similarity index 72%
rename from gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/change/DraftComments.java
index 322faea..fdb181b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftComments.java
@@ -23,31 +23,35 @@
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
-class Drafts implements ChildCollection<RevisionResource, DraftResource> {
-  private final DynamicMap<RestView<DraftResource>> views;
+public class DraftComments implements ChildCollection<RevisionResource, DraftCommentResource> {
+  private final DynamicMap<RestView<DraftCommentResource>> views;
   private final Provider<CurrentUser> user;
-  private final ListDrafts list;
+  private final ListDraftComments list;
   private final Provider<ReviewDb> dbProvider;
+  private final PatchLineCommentsUtil plcUtil;
 
   @Inject
-  Drafts(DynamicMap<RestView<DraftResource>> views,
+  DraftComments(DynamicMap<RestView<DraftCommentResource>> views,
       Provider<CurrentUser> user,
-      ListDrafts list,
-      Provider<ReviewDb> dbProvider) {
+      ListDraftComments list,
+      Provider<ReviewDb> dbProvider,
+      PatchLineCommentsUtil plcUtil) {
     this.views = views;
     this.user = user;
     this.list = list;
     this.dbProvider = dbProvider;
+    this.plcUtil = plcUtil;
   }
 
   @Override
-  public DynamicMap<RestView<DraftResource>> views() {
+  public DynamicMap<RestView<DraftCommentResource>> views() {
     return views;
   }
 
@@ -58,16 +62,14 @@
   }
 
   @Override
-  public DraftResource parse(RevisionResource rev, IdString id)
+  public DraftCommentResource parse(RevisionResource rev, IdString id)
       throws ResourceNotFoundException, OrmException, AuthException {
     checkIdentifiedUser();
     String uuid = id.get();
-    for (PatchLineComment c : dbProvider.get().patchComments()
-        .draftByPatchSetAuthor(
-            rev.getPatchSet().getId(),
-            rev.getAccountId())) {
+    for (PatchLineComment c : plcUtil.draftByPatchSetAuthor(dbProvider.get(),
+        rev.getPatchSet().getId(), rev.getAccountId(), rev.getNotes())) {
       if (uuid.equals(c.getKey().get())) {
-        return new DraftResource(rev, c);
+        return new DraftCommentResource(rev, c);
       }
     }
     throw new ResourceNotFoundException(id);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/EditMessage.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/EditMessage.java
deleted file mode 100644
index af81627..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/EditMessage.java
+++ /dev/null
@@ -1,102 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import com.google.common.base.Strings;
-import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.DefaultInput;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
-import com.google.gerrit.server.change.EditMessage.Input;
-import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.util.TimeUtil;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.PersonIdent;
-
-import java.io.IOException;
-
-@Singleton
-class EditMessage implements RestModifyView<RevisionResource, Input>,
-    UiAction<RevisionResource> {
-  private final ChangeUtil changeUtil;
-  private final PersonIdent myIdent;
-  private final ChangeJson json;
-
-  static class Input {
-    @DefaultInput
-    String message;
-  }
-
-  @Inject
-  EditMessage(ChangeUtil changeUtil,
-      @GerritPersonIdent PersonIdent myIdent,
-      ChangeJson json) {
-    this.changeUtil = changeUtil;
-    this.myIdent = myIdent;
-    this.json = json;
-  }
-
-  @Override
-  public ChangeInfo apply(RevisionResource rsrc, Input input)
-      throws BadRequestException, ResourceConflictException,
-      ResourceNotFoundException, EmailException, OrmException, IOException {
-    if (Strings.isNullOrEmpty(input.message)) {
-      throw new BadRequestException("message must be non-empty");
-    } else if (!rsrc.getPatchSet().getId()
-        .equals(rsrc.getChange().currentPatchSetId())) {
-      throw new ResourceConflictException(String.format(
-          "revision %s is not current revision",
-          rsrc.getPatchSet().getRevision().get()));
-    }
-    try {
-      return json.format(changeUtil.editCommitMessage(
-          rsrc.getControl(),
-          rsrc.getPatchSet().getId(),
-          input.message,
-          new PersonIdent(myIdent, TimeUtil.nowTs())));
-    } catch (InvalidChangeOperationException e) {
-      throw new BadRequestException(e.getMessage());
-    } catch (NoSuchChangeException e) {
-      throw new ResourceNotFoundException();
-    } catch (MissingObjectException | IncorrectObjectTypeException
-        | PatchSetInfoNotAvailableException e) {
-      throw new ResourceConflictException(e.getMessage());
-    }
-  }
-
-  @Override
-  public UiAction.Description getDescription(RevisionResource resource) {
-    PatchSet.Id current = resource.getChange().currentPatchSetId();
-    return new UiAction.Description()
-        .setLabel("Edit commit message")
-        .setVisible(resource.getChange().getStatus().isOpen()
-            && resource.getPatchSet().getId().equals(current)
-            && resource.getControl().canAddPatchSet());
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
index bc38039..6330e34 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -32,6 +32,7 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
+import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 import com.google.inject.assistedinject.Assisted;
@@ -152,7 +153,7 @@
 
   @Override
   public CurrentUser getCurrentUser() {
-    return null;
+    throw new OutOfScopeException("No user on email thread");
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
new file mode 100644
index 0000000..a8e1793
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -0,0 +1,144 @@
+// 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 static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.gerrit.common.data.PatchScript.FileMode;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.mime.FileTypeRegistry;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.LargeObjectException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+@Singleton
+public class FileContentUtil {
+  public static final String TEXT_X_GERRIT_COMMIT_MESSAGE = "text/x-gerrit-commit-message";
+  private static final String X_GIT_SYMLINK = "x-git/symlink";
+  private static final String X_GIT_GITLINK = "x-git/gitlink";
+  private static final int MAX_SIZE = 5 << 20;
+
+  private final GitRepositoryManager repoManager;
+  private final FileTypeRegistry registry;
+
+  @Inject
+  FileContentUtil(GitRepositoryManager repoManager,
+      FileTypeRegistry ftr) {
+    this.repoManager = repoManager;
+    this.registry = ftr;
+  }
+
+  public BinaryResult getContent(ProjectState project, ObjectId revstr,
+      String path) throws ResourceNotFoundException, IOException {
+    try (Repository repo = openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+      RevCommit commit = rw.parseCommit(revstr);
+      ObjectReader reader = rw.getObjectReader();
+      TreeWalk tw = TreeWalk.forPath(reader, path, commit.getTree());
+      if (tw == null) {
+        throw new ResourceNotFoundException();
+      }
+
+      org.eclipse.jgit.lib.FileMode mode = tw.getFileMode(0);
+      ObjectId id = tw.getObjectId(0);
+      if (mode == org.eclipse.jgit.lib.FileMode.GITLINK) {
+        return BinaryResult.create(id.name())
+            .setContentType(X_GIT_GITLINK)
+            .base64();
+      }
+
+      final ObjectLoader obj = repo.open(id, OBJ_BLOB);
+      byte[] raw;
+      try {
+        raw = obj.getCachedBytes(MAX_SIZE);
+      } catch (LargeObjectException e) {
+        raw = null;
+      }
+
+      BinaryResult result;
+      if (raw != null) {
+        result = BinaryResult.create(raw);
+      } else {
+        result = asBinaryResult(obj);
+      }
+
+      String type;
+      if (mode == org.eclipse.jgit.lib.FileMode.SYMLINK) {
+        type = X_GIT_SYMLINK;
+      } else {
+        type = registry.getMimeType(path, raw).toString();
+        type = resolveContentType(project, path, FileMode.FILE, type);
+      }
+      return result.setContentType(type).base64();
+    }
+  }
+
+  private static BinaryResult asBinaryResult(final ObjectLoader obj) {
+    BinaryResult result = new BinaryResult() {
+      @Override
+      public void writeTo(OutputStream os) throws IOException {
+        obj.copyTo(os);
+      }
+    };
+    result.setContentLength(obj.getSize());
+    return result;
+  }
+
+  public static String resolveContentType(ProjectState project, String path,
+      FileMode fileMode, String mimeType) {
+    switch (fileMode) {
+      case FILE:
+        if (Patch.COMMIT_MSG.equals(path)) {
+          return TEXT_X_GERRIT_COMMIT_MESSAGE;
+        }
+        if (project != null) {
+          for (ProjectState p : project.tree()) {
+            String t = p.getConfig().getMimeTypes().getMimeType(path);
+            if (t != null) {
+              return t;
+            }
+          }
+        }
+        return mimeType;
+      case GITLINK:
+        return X_GIT_GITLINK;
+      case SYMLINK:
+        return X_GIT_SYMLINK;
+      default:
+        throw new IllegalStateException("file mode: " + fileMode);
+    }
+  }
+
+  private Repository openRepository(ProjectState project)
+      throws RepositoryNotFoundException, IOException {
+    return repoManager.openRepository(project.getProject().getNameKey());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
index 6bb7236..6ae87a3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
@@ -44,15 +45,15 @@
 
   Map<String, FileInfo> toFileInfoMap(Change change, PatchSet patchSet)
       throws PatchListNotAvailableException {
-    return toFileInfoMap(change, patchSet, null);
+    return toFileInfoMap(change, patchSet.getRevision(), null);
   }
 
-  Map<String, FileInfo> toFileInfoMap(Change change, PatchSet patchSet, @Nullable PatchSet base)
+  Map<String, FileInfo> toFileInfoMap(Change change, RevId revision, @Nullable PatchSet base)
       throws PatchListNotAvailableException {
     ObjectId a = (base == null)
         ? null
         : ObjectId.fromString(base.getRevision().get());
-    ObjectId b = ObjectId.fromString(patchSet.getRevision().get());
+    ObjectId b = ObjectId.fromString(revision.get());
     PatchList list = patchListCache.get(
         new PatchListKey(change.getProject(), a, b, Whitespace.IGNORE_NONE));
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java
index 521e8c8..1662237 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java
@@ -27,7 +27,7 @@
   private final RevisionResource rev;
   private final Patch.Key key;
 
-  FileResource(RevisionResource rev, String name) {
+  public FileResource(RevisionResource rev, String name) {
     this.rev = rev;
     this.key = new Patch.Key(rev.getPatchSet().getId(), name);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
index 74659b8..e19b6a9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.reviewdb.client.AccountPatchReview;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -43,8 +44,11 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
@@ -53,6 +57,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -82,8 +87,7 @@
   }
 
   @Override
-  public FileResource parse(RevisionResource rev, IdString id)
-      throws ResourceNotFoundException, OrmException, AuthException {
+  public FileResource parse(RevisionResource rev, IdString id) {
     return new FileResource(rev, id.get());
   }
 
@@ -96,6 +100,9 @@
     @Option(name = "--reviewed")
     boolean reviewed;
 
+    @Option(name = "-q")
+    String query;
+
     private final Provider<ReviewDb> db;
     private final Provider<CurrentUser> self;
     private final FileInfoJson fileInfoJson;
@@ -125,11 +132,13 @@
 
     @Override
     public Response<?> apply(RevisionResource resource) throws AuthException,
-        BadRequestException, ResourceNotFoundException, OrmException {
-      if (base != null && reviewed) {
-        throw new BadRequestException("cannot combine base and reviewed");
-      } else if (reviewed) {
+        BadRequestException, ResourceNotFoundException, OrmException,
+        RepositoryNotFoundException, IOException {
+      checkOptions();
+      if (reviewed) {
         return Response.ok(reviewed(resource));
+      } else if (query != null) {
+        return Response.ok(query(resource));
       }
 
       PatchSet basePatchSet = null;
@@ -141,7 +150,7 @@
       try {
         Response<Map<String, FileInfo>> r = Response.ok(fileInfoJson.toFileInfoMap(
             resource.getChange(),
-            resource.getPatchSet(),
+            resource.getPatchSet().getRevision(),
             basePatchSet));
         if (resource.isCacheable()) {
           r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
@@ -152,6 +161,45 @@
       }
     }
 
+    private void checkOptions() throws BadRequestException {
+      int supplied = 0;
+      if (base != null) {
+        supplied++;
+      }
+      if (reviewed) {
+        supplied++;
+      }
+      if (query != null) {
+        supplied++;
+      }
+      if (supplied > 1) {
+        throw new BadRequestException("cannot combine base, reviewed, query");
+      }
+    }
+
+    private List<String> query(RevisionResource resource)
+        throws RepositoryNotFoundException, IOException {
+      Project.NameKey project = resource.getChange().getProject();
+      try (Repository git = gitManager.openRepository(project);
+          ObjectReader or = git.newObjectReader();
+          RevWalk rw = new RevWalk(or);
+          TreeWalk tw = new TreeWalk(or)) {
+        RevCommit c = rw.parseCommit(
+            ObjectId.fromString(resource.getPatchSet().getRevision().get()));
+
+        tw.addTree(c.getTree());
+        tw.setRecursive(true);
+        List<String> paths = new ArrayList<>();
+        while (tw.next() && paths.size() < 20) {
+          String s = tw.getPathString();
+          if (s.contains(query)) {
+            paths.add(s);
+          }
+        }
+        return paths;
+      }
+    }
+
     private List<String> reviewed(RevisionResource resource)
         throws AuthException, OrmException {
       CurrentUser user = self.get();
@@ -208,77 +256,75 @@
     private List<String> copy(Set<String> paths, PatchSet.Id old,
         RevisionResource resource, Account.Id userId) throws IOException,
         PatchListNotAvailableException, OrmException {
-      Repository git =
-          gitManager.openRepository(resource.getChange().getProject());
-      try {
-        ObjectReader reader = git.newObjectReader();
-        try {
-          PatchList oldList = patchListCache.get(
-              resource.getChange(),
-              db.get().patchSets().get(old));
-
-          PatchList curList = patchListCache.get(
-              resource.getChange(),
-              resource.getPatchSet());
-
-          int sz = paths.size();
-          List<AccountPatchReview> inserts = Lists.newArrayListWithCapacity(sz);
-          List<String> pathList = Lists.newArrayListWithCapacity(sz);
-
+      Project.NameKey project = resource.getChange().getProject();
+      try (Repository git = gitManager.openRepository(project);
+          ObjectReader reader = git.newObjectReader();
           RevWalk rw = new RevWalk(reader);
-          TreeWalk tw = new TreeWalk(reader);
-          tw.setFilter(PathFilterGroup.createFromStrings(paths));
-          tw.setRecursive(true);
-          int o = tw.addTree(rw.parseCommit(oldList.getNewId()).getTree());
-          int c = tw.addTree(rw.parseCommit(curList.getNewId()).getTree());
+          TreeWalk tw = new TreeWalk(reader)) {
+        PatchList oldList = patchListCache.get(
+            resource.getChange(),
+            db.get().patchSets().get(old));
 
-          int op = -1;
-          if (oldList.getOldId() != null) {
-            op = tw.addTree(rw.parseTree(oldList.getOldId()));
-          }
+        PatchList curList = patchListCache.get(
+            resource.getChange(),
+            resource.getPatchSet());
 
-          int cp = -1;
-          if (curList.getOldId() != null) {
-            cp = tw.addTree(rw.parseTree(curList.getOldId()));
-          }
+        int sz = paths.size();
+        List<AccountPatchReview> inserts = Lists.newArrayListWithCapacity(sz);
+        List<String> pathList = Lists.newArrayListWithCapacity(sz);
 
-          while (tw.next()) {
-            String path = tw.getPathString();
-            if (tw.getRawMode(o) != 0 && tw.getRawMode(c) != 0
-                && tw.idEqual(o, c)
-                && paths.contains(path)) {
-              // File exists in previously reviewed oldList and in curList.
-              // File content is identical.
-              inserts.add(new AccountPatchReview(
-                  new Patch.Key(
-                      resource.getPatchSet().getId(),
-                      path),
-                    userId));
-              pathList.add(path);
-            } else if (op >= 0 && cp >= 0
-                && tw.getRawMode(o) == 0 && tw.getRawMode(c) == 0
-                && tw.getRawMode(op) != 0 && tw.getRawMode(cp) != 0
-                && tw.idEqual(op, cp)
-                && paths.contains(path)) {
-              // File was deleted in previously reviewed oldList and curList.
-              // File exists in ancestor of oldList and curList.
-              // File content is identical in ancestors.
-              inserts.add(new AccountPatchReview(
-                  new Patch.Key(
-                      resource.getPatchSet().getId(),
-                      path),
-                    userId));
-              pathList.add(path);
-            }
-          }
-          db.get().accountPatchReviews().insert(inserts);
-          return pathList;
-        } finally {
-          reader.release();
+        tw.setFilter(PathFilterGroup.createFromStrings(paths));
+        tw.setRecursive(true);
+        int o = tw.addTree(rw.parseCommit(oldList.getNewId()).getTree());
+        int c = tw.addTree(rw.parseCommit(curList.getNewId()).getTree());
+
+        int op = -1;
+        if (oldList.getOldId() != null) {
+          op = tw.addTree(rw.parseTree(oldList.getOldId()));
         }
-      } finally {
-        git.close();
+
+        int cp = -1;
+        if (curList.getOldId() != null) {
+          cp = tw.addTree(rw.parseTree(curList.getOldId()));
+        }
+
+        while (tw.next()) {
+          String path = tw.getPathString();
+          if (tw.getRawMode(o) != 0 && tw.getRawMode(c) != 0
+              && tw.idEqual(o, c)
+              && paths.contains(path)) {
+            // File exists in previously reviewed oldList and in curList.
+            // File content is identical.
+            inserts.add(new AccountPatchReview(
+                new Patch.Key(
+                    resource.getPatchSet().getId(),
+                    path),
+                  userId));
+            pathList.add(path);
+          } else if (op >= 0 && cp >= 0
+              && tw.getRawMode(o) == 0 && tw.getRawMode(c) == 0
+              && tw.getRawMode(op) != 0 && tw.getRawMode(cp) != 0
+              && tw.idEqual(op, cp)
+              && paths.contains(path)) {
+            // File was deleted in previously reviewed oldList and curList.
+            // File exists in ancestor of oldList and curList.
+            // File content is identical in ancestors.
+            inserts.add(new AccountPatchReview(
+                new Patch.Key(
+                    resource.getPatchSet().getId(),
+                    path),
+                  userId));
+            pathList.add(path);
+          }
+        }
+        db.get().accountPatchReviews().insert(inserts);
+        return pathList;
       }
     }
+
+    public ListFiles setBase(String base) {
+      this.base = base;
+      return this;
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
index d602c47..913f69e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
@@ -126,7 +126,7 @@
 
           @Override
           public void close() throws IOException {
-            rw.release();
+            rw.close();
             repo.close();
           }
         };
@@ -139,7 +139,7 @@
         return bin;
       } finally {
         if (close) {
-          rw.release();
+          rw.close();
         }
       }
     } finally {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetChange.java
index c50a6ef..8b64c1b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetChange.java
@@ -14,18 +14,15 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.gerrit.extensions.common.ListChangesOption;
-import com.google.gerrit.extensions.restapi.CacheControl;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
 import org.kohsuke.args4j.Option;
 
-import java.util.concurrent.TimeUnit;
-
 public class GetChange implements RestReadView<ChangeResource> {
   private final ChangeJson json;
 
@@ -46,15 +43,10 @@
 
   @Override
   public Response<ChangeInfo> apply(ChangeResource rsrc) throws OrmException {
-    return cache(json.format(rsrc));
+    return Response.withMustRevalidate(json.format(rsrc));
   }
 
   Response<ChangeInfo> apply(RevisionResource rsrc) throws OrmException {
-    return cache(json.format(rsrc));
-  }
-
-  private Response<ChangeInfo> cache(ChangeInfo res) {
-    return Response.ok(res)
-        .caching(CacheControl.PRIVATE(0, TimeUnit.SECONDS).setMustRevalidate());
+    return Response.withMustRevalidate(json.format(rsrc));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java
index 27de91c..ea84f50 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java
@@ -14,27 +14,24 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.account.AccountInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 @Singleton
-class GetComment implements RestReadView<CommentResource> {
+public class GetComment implements RestReadView<CommentResource> {
 
-  private final AccountInfo.Loader.Factory accountLoaderFactory;
+  private final CommentJson commentJson;
 
   @Inject
-  GetComment(AccountInfo.Loader.Factory accountLoaderFactory) {
-    this.accountLoaderFactory = accountLoaderFactory;
+  GetComment(CommentJson commentJson) {
+    this.commentJson = commentJson;
   }
 
   @Override
   public CommentInfo apply(CommentResource rsrc) throws OrmException {
-    AccountInfo.Loader accountLoader = accountLoaderFactory.create(true);
-    CommentInfo ci = new CommentInfo(rsrc.getComment(), accountLoader);
-    accountLoader.fill();
-    return ci;
+    return commentJson.format(rsrc.getComment());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
index 2cd948e..296a262 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
@@ -16,20 +16,22 @@
 
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.restapi.CacheControl;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Singleton;
+
+import org.kohsuke.args4j.Option;
 
 import java.util.concurrent.TimeUnit;
 
-@Singleton
 public class GetCommit implements RestReadView<RevisionResource> {
   private final ChangeJson json;
 
+  @Option(name = "--links", usage = "Add weblinks")
+  private boolean addLinks;
+
   @Inject
   GetCommit(ChangeJson json) {
     this.json = json;
@@ -37,16 +39,17 @@
 
   @Override
   public Response<CommitInfo> apply(RevisionResource resource)
-      throws ResourceNotFoundException, OrmException {
+      throws OrmException {
     try {
       Response<CommitInfo> r =
-          Response.ok(json.toCommit(resource.getPatchSet()));
+          Response.ok(json.toCommit(resource.getPatchSet(), resource
+              .getChange().getProject(), addLinks));
       if (resource.isCacheable()) {
         r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
       }
       return r;
     } catch (PatchSetInfoNotAvailableException e) {
-      throw new ResourceNotFoundException(e.getMessage());
+      throw new OrmException(e);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
index bfc1df9..810a3a6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
@@ -17,69 +17,43 @@
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.lib.ObjectLoader;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.lib.ObjectId;
 
 import java.io.IOException;
-import java.io.OutputStream;
 
 @Singleton
 public class GetContent implements RestReadView<FileResource> {
-  private final GitRepositoryManager repoManager;
+  private final FileContentUtil fileContentUtil;
+  private final ChangeUtil changeUtil;
 
   @Inject
-  GetContent(GitRepositoryManager repoManager) {
-    this.repoManager = repoManager;
+  GetContent(FileContentUtil fileContentUtil,
+      ChangeUtil changeUtil) {
+    this.fileContentUtil = fileContentUtil;
+    this.changeUtil = changeUtil;
   }
 
   @Override
   public BinaryResult apply(FileResource rsrc)
-      throws ResourceNotFoundException, IOException {
-    return apply(rsrc.getRevision().getControl().getProject().getNameKey(),
-        rsrc.getRevision().getPatchSet().getRevision().get(),
-        rsrc.getPatchKey().get());
-  }
-
-  public BinaryResult apply(Project.NameKey project, String revstr, String path)
-      throws ResourceNotFoundException, IOException {
-    Repository repo = repoManager.openRepository(project);
-    try {
-      RevWalk rw = new RevWalk(repo);
-      try {
-        RevCommit commit =
-            rw.parseCommit(repo.resolve(revstr));
-        TreeWalk tw =
-            TreeWalk.forPath(rw.getObjectReader(), path,
-                commit.getTree().getId());
-        if (tw == null) {
-          throw new ResourceNotFoundException();
-        }
-        try {
-          final ObjectLoader object = repo.open(tw.getObjectId(0));
-          @SuppressWarnings("resource")
-          BinaryResult result = new BinaryResult() {
-            @Override
-            public void writeTo(OutputStream os) throws IOException {
-              object.copyTo(os);
-            }
-          };
-          return result.setContentLength(object.getSize()).base64();
-        } finally {
-          tw.release();
-        }
-      } finally {
-        rw.release();
-      }
-    } finally {
-      repo.close();
+      throws ResourceNotFoundException, IOException, NoSuchChangeException,
+      OrmException {
+    String path = rsrc.getPatchKey().get();
+    if (Patch.COMMIT_MSG.equals(path)) {
+      String msg = changeUtil.getMessage(rsrc.getRevision().getChange());
+      return BinaryResult.create(msg)
+          .setContentType(FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE)
+          .base64();
     }
+    return fileContentUtil.getContent(
+        rsrc.getRevision().getControl().getProjectControl().getProjectState(),
+        ObjectId.fromString(rsrc.getRevision().getPatchSet().getRevision().get()),
+        path);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
index 514c12a..509bbd4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
index 0f68182..8e3a5d1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
@@ -16,12 +16,22 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchScript.DisplayMethod;
-import com.google.gerrit.common.data.PatchScript.FileMode;
+import com.google.gerrit.extensions.common.ChangeType;
+import com.google.gerrit.extensions.common.DiffInfo;
+import com.google.gerrit.extensions.common.DiffInfo.ContentEntry;
+import com.google.gerrit.extensions.common.DiffInfo.FileMeta;
+import com.google.gerrit.extensions.common.DiffInfo.IntraLineStatus;
+import com.google.gerrit.extensions.common.DiffWebLinkInfo;
+import com.google.gerrit.extensions.common.WebLinkInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.CacheControl;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -32,10 +42,12 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.git.LargeObjectException;
 import com.google.gerrit.server.patch.PatchScriptFactory;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
@@ -53,13 +65,26 @@
 import org.kohsuke.args4j.spi.Parameters;
 import org.kohsuke.args4j.spi.Setter;
 
+import java.io.IOException;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
 public class GetDiff implements RestReadView<FileResource> {
+  private static final ImmutableMap<Patch.ChangeType, ChangeType> CHANGE_TYPE =
+      Maps.immutableEnumMap(
+          new ImmutableMap.Builder<Patch.ChangeType, ChangeType>()
+              .put(Patch.ChangeType.ADDED, ChangeType.ADDED)
+              .put(Patch.ChangeType.MODIFIED, ChangeType.MODIFIED)
+              .put(Patch.ChangeType.DELETED, ChangeType.DELETED)
+              .put(Patch.ChangeType.RENAMED, ChangeType.RENAMED)
+              .put(Patch.ChangeType.COPIED, ChangeType.COPIED)
+              .put(Patch.ChangeType.REWRITE, ChangeType.REWRITE)
+              .build());
+
   private final ProjectCache projectCache;
   private final PatchScriptFactory.Factory patchScriptFactoryFactory;
   private final Revisions revisions;
+  private final WebLinks webLinks;
 
   @Option(name = "--base", metaVar = "REVISION")
   String base;
@@ -73,23 +98,29 @@
   @Option(name = "--intraline")
   boolean intraline;
 
+  @Option(name = "--weblinks-only")
+  boolean webLinksOnly;
+
   @Inject
   GetDiff(ProjectCache projectCache,
       PatchScriptFactory.Factory patchScriptFactoryFactory,
-      Revisions revisions) {
+      Revisions revisions,
+      WebLinks webLinks) {
     this.projectCache = projectCache;
     this.patchScriptFactoryFactory = patchScriptFactoryFactory;
     this.revisions = revisions;
+    this.webLinks = webLinks;
   }
 
   @Override
-  public Response<Result> apply(FileResource resource)
-      throws ResourceConflictException, ResourceNotFoundException, OrmException {
-    PatchSet.Id basePatchSet = null;
+  public Response<DiffInfo> apply(FileResource resource)
+      throws ResourceConflictException, ResourceNotFoundException,
+      OrmException, AuthException, InvalidChangeOperationException, IOException {
+    PatchSet basePatchSet = null;
     if (base != null) {
       RevisionResource baseResource = revisions.parse(
           resource.getRevision().getChangeResource(), IdString.fromDecoded(base));
-      basePatchSet = baseResource.getPatchSet().getId();
+      basePatchSet = baseResource.getPatchSet();
     }
     AccountDiffPreference prefs = new AccountDiffPreference(new Account.Id(0));
     prefs.setIgnoreWhitespace(ignoreWhitespace.whitespace);
@@ -100,7 +131,7 @@
       PatchScriptFactory psf = patchScriptFactoryFactory.create(
           resource.getRevision().getControl(),
           resource.getPatchKey().getFileName(),
-          basePatchSet,
+          basePatchSet != null ? basePatchSet.getId() : null,
           resource.getPatchKey().getParentKey(),
           prefs);
       psf.setLoadHistory(false);
@@ -114,9 +145,9 @@
         content.addCommon(edit.getBeginA());
 
         checkState(content.nextA == edit.getBeginA(),
-            "nextA = %d; want %d", content.nextA, edit.getBeginA());
+            "nextA = %s; want %s", content.nextA, edit.getBeginA());
         checkState(content.nextB == edit.getBeginB(),
-            "nextB = %d; want %d", content.nextB, edit.getBeginB());
+            "nextB = %s; want %s", content.nextB, edit.getBeginB());
         switch (edit.getType()) {
           case DELETE:
           case INSERT:
@@ -136,37 +167,75 @@
       ProjectState state =
           projectCache.get(resource.getRevision().getChange().getProject());
 
-      Result result = new Result();
-      if (ps.getDisplayMethodA() != DisplayMethod.NONE) {
-        result.metaA = new FileMeta();
-        result.metaA.name = Objects.firstNonNull(ps.getOldName(), ps.getNewName());
-        setContentType(result.metaA, state, ps.getFileModeA(), ps.getMimeTypeA());
-        result.metaA.lines = ps.getA().size();
-      }
+      DiffInfo result = new DiffInfo();
+      // TODO referring to the parent commit by refs/changes/12/60012/1^1
+      // will likely not work for inline edits
+      String revA = basePatchSet != null
+          ? basePatchSet.getRefName()
+          : resource.getRevision().getPatchSet().getRefName() + "^1";
+      String revB = resource.getRevision().getEdit().isPresent()
+           ? resource.getRevision().getEdit().get().getRefName()
+           : resource.getRevision().getPatchSet().getRefName();
 
-      if (ps.getDisplayMethodB() != DisplayMethod.NONE) {
-        result.metaB = new FileMeta();
-        result.metaB.name = ps.getNewName();
-        setContentType(result.metaB, state, ps.getFileModeB(), ps.getMimeTypeB());
-        result.metaB.lines = ps.getB().size();
-      }
+      FluentIterable<DiffWebLinkInfo> links =
+          webLinks.getDiffLinks(state.getProject().getName(),
+              resource.getPatchKey().getParentKey().getParentKey().get(),
+              basePatchSet != null ? basePatchSet.getId().get() : null,
+              revA,
+              MoreObjects.firstNonNull(ps.getOldName(), ps.getNewName()),
+              resource.getPatchKey().getParentKey().get(),
+              revB,
+              ps.getNewName());
+      result.webLinks = links.isEmpty() ? null : links.toList();
 
-      if (intraline) {
-        if (ps.hasIntralineTimeout()) {
-          result.intralineStatus = IntraLineStatus.TIMEOUT;
-        } else if (ps.hasIntralineFailure()) {
-          result.intralineStatus = IntraLineStatus.FAILURE;
-        } else {
-          result.intralineStatus = IntraLineStatus.OK;
+      if (!webLinksOnly) {
+        if (ps.isBinary()) {
+          result.binary = true;
         }
+        if (ps.getDisplayMethodA() != DisplayMethod.NONE) {
+          result.metaA = new FileMeta();
+          result.metaA.name = MoreObjects.firstNonNull(ps.getOldName(),
+              ps.getNewName());
+          result.metaA.contentType = FileContentUtil.resolveContentType(
+              state, result.metaA.name, ps.getFileModeA(), ps.getMimeTypeA());
+          result.metaA.lines = ps.getA().size();
+          result.metaA.webLinks =
+              getFileWebLinks(state.getProject(), revA, result.metaA.name);
+        }
+
+        if (ps.getDisplayMethodB() != DisplayMethod.NONE) {
+          result.metaB = new FileMeta();
+          result.metaB.name = ps.getNewName();
+          result.metaB.contentType = FileContentUtil.resolveContentType(
+              state, result.metaB.name, ps.getFileModeB(), ps.getMimeTypeB());
+          result.metaB.lines = ps.getB().size();
+          result.metaB.webLinks =
+              getFileWebLinks(state.getProject(), revB, result.metaB.name);
+        }
+
+        if (intraline) {
+          if (ps.hasIntralineTimeout()) {
+            result.intralineStatus = IntraLineStatus.TIMEOUT;
+          } else if (ps.hasIntralineFailure()) {
+            result.intralineStatus = IntraLineStatus.FAILURE;
+          } else {
+            result.intralineStatus = IntraLineStatus.OK;
+          }
+        }
+
+        result.changeType = CHANGE_TYPE.get(ps.getChangeType());
+        if (result.changeType == null) {
+          throw new IllegalStateException(
+              "unknown change type: " + ps.getChangeType());
+        }
+
+        if (ps.getPatchHeader().size() > 0) {
+          result.diffHeader = ps.getPatchHeader();
+        }
+        result.content = content.lines;
       }
 
-      result.changeType = ps.getChangeType();
-      if (ps.getPatchHeader().size() > 0) {
-        result.diffHeader = ps.getPatchHeader();
-      }
-      result.content = content.lines;
-      Response<Result> r = Response.ok(result);
+      Response<DiffInfo> r = Response.ok(result);
       if (resource.isCacheable()) {
         r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
       }
@@ -178,53 +247,16 @@
     }
   }
 
-  static class Result {
-    FileMeta metaA;
-    FileMeta metaB;
-    IntraLineStatus intralineStatus;
-    ChangeType changeType;
-    List<String> diffHeader;
-    List<ContentEntry> content;
+  private List<WebLinkInfo> getFileWebLinks(Project project, String rev,
+      String file) {
+    FluentIterable<WebLinkInfo> links =
+        webLinks.getFileLinks(project.getName(), rev, file);
+    return links.isEmpty() ? null : links.toList();
   }
 
-  static class FileMeta {
-    String name;
-    String contentType;
-    Integer lines;
-  }
-
-  private void setContentType(FileMeta meta, ProjectState project,
-      FileMode fileMode, String mimeType) {
-    switch (fileMode) {
-      case FILE:
-        if (Patch.COMMIT_MSG.equals(meta.name)) {
-          mimeType = "text/x-gerrit-commit-message";
-        } else if (project != null) {
-          for (ProjectState p : project.tree()) {
-            String t = p.getConfig().getMimeTypes().getMimeType(meta.name);
-            if (t != null) {
-              mimeType = t;
-              break;
-            }
-          }
-        }
-        meta.contentType = mimeType;
-        break;
-      case GITLINK:
-        meta.contentType = "x-git/gitlink";
-        break;
-      case SYMLINK:
-        meta.contentType = "x-git/symlink";
-        break;
-      default:
-        throw new IllegalStateException("file mode: " + fileMode);
-    }
-  }
-
-  enum IntraLineStatus {
-    OK,
-    TIMEOUT,
-    FAILURE
+  public GetDiff setBase(String base) {
+    this.base = base;
+    return this;
   }
 
   private static class Content {
@@ -310,11 +342,13 @@
         int lastB = 0;
         for (Edit edit : internalEdit) {
           if (edit.getBeginA() != edit.getEndA()) {
-            e.editA.add(ImmutableList.of(edit.getBeginA() - lastA, edit.getEndA() - edit.getBeginA()));
+            e.editA.add(ImmutableList.of(
+                edit.getBeginA() - lastA, edit.getEndA() - edit.getBeginA()));
             lastA = edit.getEndA();
           }
           if (edit.getBeginB() != edit.getEndB()) {
-            e.editB.add(ImmutableList.of(edit.getBeginB() - lastB, edit.getEndB() - edit.getBeginB()));
+            e.editB.add(ImmutableList.of(
+                edit.getBeginB() - lastB, edit.getEndB() - edit.getBeginB()));
             lastB = edit.getEndB();
           }
         }
@@ -341,28 +375,6 @@
     }
   }
 
-  static final class ContentEntry {
-    // Common lines to both sides.
-    List<String> ab;
-    // Lines of a.
-    List<String> a;
-    // Lines of b.
-    List<String> b;
-
-    // A list of changed sections of the corresponding line list.
-    // Each entry is a character <offset, length> pair. The offset is from the
-    // beginning of the first line in the list. Also, the offset includes an
-    // implied trailing newline character for each line.
-    List<List<Integer>> editA;
-    List<List<Integer>> editB;
-
-    // a and b are actually common with this whitespace ignore setting.
-    Boolean common;
-
-    // Number of lines to skip on both sides.
-    Integer skip;
-  }
-
   public static class ContextOptionHandler extends OptionHandler<Short> {
     public ContextOptionHandler(
         CmdLineParser parser, OptionDef option, Setter<Short> setter) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraftComment.java
similarity index 61%
rename from gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraftComment.java
index 12c50ae..a13ecdf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraftComment.java
@@ -14,27 +14,24 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.account.AccountInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 @Singleton
-class GetDraft implements RestReadView<DraftResource> {
+public class GetDraftComment implements RestReadView<DraftCommentResource> {
 
-  private final AccountInfo.Loader.Factory accountLoaderFactory;
+  private final CommentJson commentJson;
 
   @Inject
-  GetDraft(AccountInfo.Loader.Factory accountLoaderFactory) {
-    this.accountLoaderFactory = accountLoaderFactory;
+  GetDraftComment(CommentJson commentJson) {
+    this.commentJson = commentJson;
   }
 
   @Override
-  public CommentInfo apply(DraftResource rsrc) throws OrmException {
-    AccountInfo.Loader accountLoader = accountLoaderFactory.create(true);
-    CommentInfo ci = new CommentInfo(rsrc.getComment(), accountLoader);
-    accountLoader.fill();
-    return ci;
+  public CommentInfo apply(DraftCommentResource rsrc) throws OrmException {
+    return commentJson.format(rsrc.getComment());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetHashtags.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetHashtags.java
new file mode 100644
index 0000000..4846c0b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetHashtags.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Set;
+
+@Singleton
+public class GetHashtags implements RestReadView<ChangeResource> {
+  @Override
+  public Response<? extends Set<String>> apply(ChangeResource req)
+      throws AuthException, OrmException, IOException, BadRequestException {
+
+    ChangeControl control = req.getControl();
+    ChangeNotes notes = control.getNotes().load();
+    Set<String> hashtags = notes.getHashtags();
+    if (hashtags == null) {
+      hashtags = Collections.emptySet();
+    }
+    return Response.ok(hashtags);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java
index 4a8be84..afd11569 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java
@@ -94,15 +94,16 @@
 
           private void format(OutputStream out) throws IOException {
             out.write(formatEmailHeader(commit).getBytes(UTF_8));
-            DiffFormatter fmt = new DiffFormatter(out);
-            fmt.setRepository(repo);
-            fmt.format(base.getTree(), commit.getTree());
-            fmt.flush();
+            try (DiffFormatter fmt = new DiffFormatter(out)) {
+              fmt.setRepository(repo);
+              fmt.format(base.getTree(), commit.getTree());
+              fmt.flush();
+            }
           }
 
           @Override
           public void close() throws IOException {
-            rw.release();
+            rw.close();
             repo.close();
           }
         };
@@ -123,7 +124,7 @@
         return bin;
       } finally {
         if (close) {
-          rw.release();
+          rw.close();
         }
       }
     } finally {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
index 0ccb15c..6cdae44 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
@@ -21,15 +21,17 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.common.CommitInfo;
-import com.google.gerrit.extensions.common.GitPerson;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetAncestor;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CommonConverters;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
@@ -39,7 +41,6 @@
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -50,7 +51,6 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.sql.Timestamp;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedList;
@@ -64,36 +64,33 @@
 
   private final GitRepositoryManager gitMgr;
   private final Provider<ReviewDb> dbProvider;
+  private final Provider<InternalChangeQuery> queryProvider;
 
   @Inject
-  GetRelated(GitRepositoryManager gitMgr, Provider<ReviewDb> db) {
+  GetRelated(GitRepositoryManager gitMgr,
+      Provider<ReviewDb> db,
+      Provider<InternalChangeQuery> queryProvider) {
     this.gitMgr = gitMgr;
     this.dbProvider = db;
+    this.queryProvider = queryProvider;
   }
 
   @Override
   public RelatedInfo apply(RevisionResource rsrc)
       throws RepositoryNotFoundException, IOException, OrmException {
-    Repository git = gitMgr.openRepository(rsrc.getChange().getProject());
-    try {
+    try (Repository git = gitMgr.openRepository(rsrc.getChange().getProject());
+        RevWalk rw = new RevWalk(git)) {
       Ref ref = git.getRef(rsrc.getChange().getDest().get());
-      RevWalk rw = new RevWalk(git);
-      try {
-        RelatedInfo info = new RelatedInfo();
-        info.changes = walk(rsrc, rw, ref);
-        return info;
-      } finally {
-        rw.release();
-      }
-    } finally {
-      git.close();
+      RelatedInfo info = new RelatedInfo();
+      info.changes = walk(rsrc, rw, ref);
+      return info;
     }
   }
 
   private List<ChangeAndCommit> walk(RevisionResource rsrc, RevWalk rw, Ref ref)
       throws OrmException, IOException {
-    Map<Change.Id, Change> changes = allOpenChanges(rsrc);
-    Map<PatchSet.Id, PatchSet> patchSets = allPatchSets(changes.keySet());
+    Map<Change.Id, ChangeData> changes = allOpenChanges(rsrc);
+    Map<PatchSet.Id, PatchSet> patchSets = allPatchSets(rsrc, changes.values());
 
     Map<String, PatchSet> commits = Maps.newHashMap();
     for (PatchSet p : patchSets.values()) {
@@ -119,7 +116,7 @@
       PatchSet p = commits.get(c.name());
       Change g = null;
       if (p != null) {
-        g = changes.get(p.getId().getParentKey());
+        g = changes.get(p.getId().getParentKey()).change();
         added.add(p.getId().getParentKey());
       }
       parents.add(new ChangeAndCommit(g, p, c));
@@ -129,42 +126,37 @@
 
     if (list.size() == 1) {
       ChangeAndCommit r = list.get(0);
-      if (r._changeNumber != null && r._revisionNumber != null
-          && r._changeNumber == rsrc.getChange().getChangeId()
-          && r._revisionNumber == rsrc.getPatchSet().getPatchSetId()) {
+      if (r.commit != null && r.commit.commit.equals(rsrc.getPatchSet().getRevision().get())) {
         return Collections.emptyList();
       }
     }
     return list;
   }
 
-  private Map<Change.Id, Change> allOpenChanges(RevisionResource rsrc)
+  private Map<Change.Id, ChangeData> allOpenChanges(RevisionResource rsrc)
       throws OrmException {
-    ReviewDb db = dbProvider.get();
-    return db.changes().toMap(
-        db.changes().byBranchOpenAll(rsrc.getChange().getDest()));
+    return ChangeData.asMap(
+        queryProvider.get().byBranchOpen(rsrc.getChange().getDest()));
   }
 
-  private Map<PatchSet.Id, PatchSet> allPatchSets(Collection<Change.Id> ids)
-      throws OrmException {
-    int n = ids.size();
-    ReviewDb db = dbProvider.get();
-    List<ResultSet<PatchSet>> t = Lists.newArrayListWithCapacity(n);
-    for (Change.Id id : ids) {
-      t.add(db.patchSets().byChange(id));
-    }
-
-    Map<PatchSet.Id, PatchSet> r = Maps.newHashMapWithExpectedSize(n * 2);
-    for (ResultSet<PatchSet> rs : t) {
-      for (PatchSet p : rs) {
+  private Map<PatchSet.Id, PatchSet> allPatchSets(RevisionResource rsrc,
+      Collection<ChangeData> cds) throws OrmException {
+    Map<PatchSet.Id, PatchSet> r =
+        Maps.newHashMapWithExpectedSize(cds.size() * 2);
+    for (ChangeData cd : cds) {
+      for (PatchSet p : cd.patches()) {
         r.put(p.getId(), p);
       }
     }
+
+    if (rsrc.getEdit().isPresent()) {
+      r.put(rsrc.getPatchSet().getId(), rsrc.getPatchSet());
+    }
     return r;
   }
 
   private List<ChangeAndCommit> children(RevisionResource rsrc, RevWalk rw,
-      Map<Change.Id, Change> changes, Map<PatchSet.Id, PatchSet> patchSets,
+      Map<Change.Id, ChangeData> changes, Map<PatchSet.Id, PatchSet> patchSets,
       Set<Change.Id> added)
       throws OrmException, IOException {
     // children is a map of parent commit name to PatchSet built on it.
@@ -191,9 +183,9 @@
       }
 
       for (Map.Entry<Change.Id, PatchSet.Id> e : matches.entrySet()) {
-        Change change = changes.get(e.getKey());
+        ChangeData cd = changes.get(e.getKey());
         PatchSet ps = patchSets.get(e.getValue());
-        if (change == null || ps == null || !seenChange.add(e.getKey())) {
+        if (cd == null || ps == null || !seenChange.add(e.getKey())) {
           continue;
         }
 
@@ -204,7 +196,7 @@
           q.addFirst(ps.getRevision().get());
           if (added.add(ps.getId().getParentKey())) {
             rw.parseBody(c);
-            graph.add(new ChangeAndCommit(change, ps, c));
+            graph.add(new ChangeAndCommit(cd.change(), ps, c));
           }
         }
       }
@@ -214,13 +206,15 @@
   }
 
   private boolean isVisible(ProjectControl projectCtl,
-      Map<Change.Id, Change> changes,
+      Map<Change.Id, ChangeData> changes,
       Map<PatchSet.Id, PatchSet> patchSets,
       PatchSet.Id psId) throws OrmException {
-    Change c = changes.get(psId.getParentKey());
+    ChangeData cd = changes.get(psId.getParentKey());
     PatchSet ps = patchSets.get(psId);
-    if (c != null && ps != null) {
-      ChangeControl ctl = projectCtl.controlFor(c);
+    if (cd != null && ps != null) {
+      // Related changes are in the same project, so reuse the existing
+      // ProjectControl.
+      ChangeControl ctl = projectCtl.controlFor(cd.change());
       return ctl.isVisible(dbProvider.get())
           && ctl.isPatchVisible(ps, dbProvider.get());
     }
@@ -272,15 +266,6 @@
     return r;
   }
 
-  private static GitPerson toGitPerson(PersonIdent id) {
-    GitPerson p = new GitPerson();
-    p.name = id.getName();
-    p.email = id.getEmailAddress();
-    p.date = new Timestamp(id.getWhen().getTime());
-    p.tz = id.getTimeZoneOffset();
-    return p;
-  }
-
   public static class RelatedInfo {
     public List<ChangeAndCommit> changes;
   }
@@ -309,7 +294,7 @@
         p.commit = c.getParent(i).name();
         commit.parents.add(p);
       }
-      commit.author = toGitPerson(c.getAuthorIdent());
+      commit.author = CommonConverters.toGitPerson(c.getAuthorIdent());
       commit.subject = c.getShortMessage();
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java
index 9f98590..f379d83 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
new file mode 100644
index 0000000..d58c8d2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class GetRevisionActions implements RestReadView<RevisionResource> {
+  private final ActionJson delegate;
+
+  @Inject
+  GetRevisionActions(ActionJson delegate) {
+    this.delegate = delegate;
+  }
+
+  @Override
+  public Object apply(RevisionResource rsrc) {
+    return Response.withMustRevalidate(delegate.format(rsrc));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetTopic.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetTopic.java
index 3a2f7e7..0746588 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetTopic.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetTopic.java
@@ -19,7 +19,7 @@
 import com.google.inject.Singleton;
 
 @Singleton
-class GetTopic implements RestReadView<ChangeResource> {
+public class GetTopic implements RestReadView<ChangeResource> {
   @Override
   public String apply(ChangeResource rsrc) {
     return Strings.nullToEmpty(rsrc.getChange().getTopic());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/HashtagsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/HashtagsUtil.java
new file mode 100644
index 0000000..cd3b3b1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/HashtagsUtil.java
@@ -0,0 +1,156 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.base.CharMatcher.WHITESPACE;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Strings;
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.extensions.api.changes.HashtagsInput;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.validators.HashtagValidationListener;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@Singleton
+public class HashtagsUtil {
+  private static final CharMatcher LEADER = WHITESPACE.or(CharMatcher.is('#'));
+  private static final String PATTERN = "(?:\\s|\\A)#[\\p{L}[0-9]-_]+";
+
+  private final ChangeUpdate.Factory updateFactory;
+  private final Provider<ReviewDb> dbProvider;
+  private final ChangeIndexer indexer;
+  private final ChangeHooks hooks;
+  private final DynamicSet<HashtagValidationListener> hashtagValidationListeners;
+
+  @Inject
+  HashtagsUtil(ChangeUpdate.Factory updateFactory,
+      Provider<ReviewDb> dbProvider,
+      ChangeIndexer indexer,
+      ChangeHooks hooks,
+      DynamicSet<HashtagValidationListener> hashtagValidationListeners) {
+    this.updateFactory = updateFactory;
+    this.dbProvider = dbProvider;
+    this.indexer = indexer;
+    this.hooks = hooks;
+    this.hashtagValidationListeners = hashtagValidationListeners;
+  }
+
+  public static String cleanupHashtag(String hashtag) {
+    hashtag = LEADER.trimLeadingFrom(hashtag);
+    hashtag = WHITESPACE.trimTrailingFrom(hashtag);
+    return hashtag;
+  }
+
+  public static Set<String> extractTags(String input) {
+    Set<String> result = new HashSet<>();
+    if (!Strings.isNullOrEmpty(input)) {
+      Matcher matcher = Pattern.compile(PATTERN).matcher(input);
+      while (matcher.find()) {
+        result.add(cleanupHashtag(matcher.group()));
+      }
+    }
+    return result;
+  }
+
+  private Set<String> extractTags(Set<String> input)
+      throws IllegalArgumentException {
+    if (input == null) {
+      return Collections.emptySet();
+    } else {
+      HashSet<String> result = new HashSet<>();
+      for (String hashtag : input) {
+        if (hashtag.contains(",")) {
+          throw new IllegalArgumentException("Hashtags may not contain commas");
+        }
+        hashtag = cleanupHashtag(hashtag);
+        if (!hashtag.isEmpty()) {
+          result.add(hashtag);
+        }
+      }
+      return result;
+    }
+  }
+
+  public TreeSet<String> setHashtags(ChangeControl control,
+      HashtagsInput input, boolean runHooks, boolean index)
+          throws IllegalArgumentException, IOException,
+          ValidationException, AuthException, OrmException {
+    if (input == null
+        || (input.add == null && input.remove == null)) {
+      throw new IllegalArgumentException("Hashtags are required");
+    }
+
+    if (!control.canEditHashtags()) {
+      throw new AuthException("Editing hashtags not permitted");
+    }
+    ChangeUpdate update = updateFactory.create(control);
+    ChangeNotes notes = control.getNotes().load();
+
+    Set<String> existingHashtags = notes.getHashtags();
+    Set<String> updatedHashtags = new HashSet<>();
+    Set<String> toAdd = new HashSet<>(extractTags(input.add));
+    Set<String> toRemove = new HashSet<>(extractTags(input.remove));
+
+    for (HashtagValidationListener validator : hashtagValidationListeners) {
+      validator.validateHashtags(update.getChange(), toAdd, toRemove);
+    }
+
+    if (existingHashtags != null && !existingHashtags.isEmpty()) {
+      updatedHashtags.addAll(existingHashtags);
+      toAdd.removeAll(existingHashtags);
+      toRemove.retainAll(existingHashtags);
+    }
+
+    if (toAdd.size() > 0 || toRemove.size() > 0) {
+      updatedHashtags.addAll(toAdd);
+      updatedHashtags.removeAll(toRemove);
+      update.setHashtags(updatedHashtags);
+      update.commit();
+
+      if (index) {
+        indexer.index(dbProvider.get(), update.getChange());
+      }
+
+      if (runHooks) {
+        IdentifiedUser currentUser = ((IdentifiedUser) control.getCurrentUser());
+        hooks.doHashtagsChangedHook(
+            update.getChange(), currentUser.getAccount(),
+            toAdd, toRemove, updatedHashtags,
+            dbProvider.get());
+      }
+    }
+    return new TreeSet<>(updatedHashtags);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
index e0cdebb..7e9bb14 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedIn.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ChangeControl;
@@ -55,26 +56,19 @@
     ChangeControl ctl = rsrc.getControl();
     PatchSet ps =
         db.get().patchSets().get(ctl.getChange().currentPatchSetId());
-    Repository r =
-        repoManager.openRepository(ctl.getProject().getNameKey());
-    try {
-      RevWalk rw = new RevWalk(r);
+    Project.NameKey project = ctl.getProject().getNameKey();
+    try (Repository r = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(r)) {
+      rw.setRetainBody(false);
+      RevCommit rev;
       try {
-        rw.setRetainBody(false);
-        RevCommit rev;
-        try {
-          rev = rw.parseCommit(ObjectId.fromString(ps.getRevision().get()));
-        } catch (IncorrectObjectTypeException err) {
-          throw new BadRequestException(err.getMessage());
-        } catch (MissingObjectException err) {
-          throw new ResourceConflictException(err.getMessage());
-        }
-        return new IncludedInInfo(IncludedInResolver.resolve(r, rw, rev));
-      } finally {
-        rw.release();
+        rev = rw.parseCommit(ObjectId.fromString(ps.getRevision().get()));
+      } catch (IncorrectObjectTypeException err) {
+        throw new BadRequestException(err.getMessage());
+      } catch (MissingObjectException err) {
+        throw new ResourceConflictException(err.getMessage());
       }
-    } finally {
-      r.close();
+      return new IncludedInInfo(IncludedInResolver.resolve(r, rw, rev));
     }
   }
 
@@ -87,4 +81,4 @@
       tags = in.getTags();
     }
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
index 201ee14..fcac76d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
@@ -49,12 +49,26 @@
 
   public static IncludedInDetail resolve(final Repository repo,
       final RevWalk rw, final RevCommit commit) throws IOException {
-    return new IncludedInResolver(repo, rw, commit).resolve();
+    RevFlag flag = newFlag(rw);
+    try {
+      return new IncludedInResolver(repo, rw, commit, flag).resolve();
+    } finally {
+      rw.disposeFlag(flag);
+    }
   }
 
   public static boolean includedInOne(final Repository repo, final RevWalk rw,
       final RevCommit commit, final Collection<Ref> refs) throws IOException {
-    return new IncludedInResolver(repo, rw, commit).includedInOne(refs);
+    RevFlag flag = newFlag(rw);
+    try {
+      return new IncludedInResolver(repo, rw, commit, flag).includedInOne(refs);
+    } finally {
+      rw.disposeFlag(flag);
+    }
+  }
+
+  private static RevFlag newFlag(RevWalk rw) {
+    return rw.newFlag("CONTAINS_TARGET");
   }
 
   private final Repository repo;
@@ -65,12 +79,12 @@
   private Multimap<RevCommit, String> commitToRef;
   private List<RevCommit> tipsByCommitTime;
 
-  private IncludedInResolver(final Repository repo, final RevWalk rw,
-      final RevCommit target) {
+  private IncludedInResolver(Repository repo, RevWalk rw, RevCommit target,
+      RevFlag containsTarget) {
     this.repo = repo;
     this.rw = rw;
     this.target = target;
-    this.containsTarget = rw.newFlag("CONTAINS_TARGET");
+    this.containsTarget = containsTarget;
   }
 
   private IncludedInDetail resolve() throws IOException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
index 6cf3e00..280d078 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -27,7 +25,6 @@
 
 import java.io.IOException;
 
-@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @Singleton
 public class Index implements RestModifyView<ChangeResource, Input> {
   public static class Input {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java
index f4d7b49..b50e243 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.PatchLineCommentsUtil;
-import com.google.gerrit.server.account.AccountInfo;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -25,14 +24,12 @@
 import com.google.inject.Singleton;
 
 @Singleton
-class ListComments extends ListDrafts {
-  private final PatchLineCommentsUtil plcUtil;
-
+public class ListComments extends ListDraftComments {
   @Inject
-  ListComments(Provider<ReviewDb> db, AccountInfo.Loader.Factory alf,
+  ListComments(Provider<ReviewDb> db,
+      CommentJson commentJson,
       PatchLineCommentsUtil plcUtil) {
-    super(db, alf);
-    this.plcUtil = plcUtil;
+    super(db, commentJson, plcUtil);
   }
 
   @Override
@@ -40,6 +37,7 @@
     return true;
   }
 
+  @Override
   protected Iterable<PatchLineComment> listComments(RevisionResource rsrc)
       throws OrmException {
     ChangeNotes notes = rsrc.getNotes();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDraftComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDraftComments.java
new file mode 100644
index 0000000..3375cba
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDraftComments.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.util.List;
+import java.util.Map;
+
+@Singleton
+public class ListDraftComments implements RestReadView<RevisionResource> {
+  protected final Provider<ReviewDb> db;
+  protected CommentJson commentJson;
+  protected final PatchLineCommentsUtil plcUtil;
+
+  @Inject
+  ListDraftComments(Provider<ReviewDb> db,
+      CommentJson commentJson,
+      PatchLineCommentsUtil plcUtil) {
+    this.db = db;
+    this.commentJson = commentJson;
+    this.plcUtil = plcUtil;
+  }
+
+  protected Iterable<PatchLineComment> listComments(RevisionResource rsrc)
+      throws OrmException {
+    return plcUtil.draftByPatchSetAuthor(db.get(), rsrc.getPatchSet().getId(),
+        rsrc.getAccountId(), rsrc.getNotes());
+  }
+
+  protected boolean includeAuthorInfo() {
+    return false;
+  }
+
+  @Override
+  public Map<String, List<CommentInfo>> apply(RevisionResource rsrc)
+      throws OrmException {
+    return commentJson.format(listComments(rsrc), includeAuthorInfo());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
deleted file mode 100644
index bd3aa04..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
+++ /dev/null
@@ -1,96 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import static com.google.common.base.Objects.firstNonNull;
-
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.gerrit.common.changes.Side;
-import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.account.AccountInfo;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
-
-@Singleton
-class ListDrafts implements RestReadView<RevisionResource> {
-  protected final Provider<ReviewDb> db;
-  private final AccountInfo.Loader.Factory accountLoaderFactory;
-
-  @Inject
-  ListDrafts(Provider<ReviewDb> db, AccountInfo.Loader.Factory alf) {
-    this.db = db;
-    this.accountLoaderFactory = alf;
-  }
-
-  protected Iterable<PatchLineComment> listComments(RevisionResource rsrc)
-      throws OrmException {
-    return db.get().patchComments()
-        .draftByPatchSetAuthor(
-            rsrc.getPatchSet().getId(),
-            rsrc.getAccountId());
-  }
-
-  protected boolean includeAuthorInfo() {
-    return false;
-  }
-
-  @Override
-  public Map<String, List<CommentInfo>> apply(RevisionResource rsrc)
-      throws OrmException {
-    Map<String, List<CommentInfo>> out = Maps.newTreeMap();
-    AccountInfo.Loader accountLoader =
-        includeAuthorInfo() ? accountLoaderFactory.create(true) : null;
-    for (PatchLineComment c : listComments(rsrc)) {
-      CommentInfo o = new CommentInfo(c, accountLoader);
-      List<CommentInfo> list = out.get(o.path);
-      if (list == null) {
-        list = Lists.newArrayList();
-        out.put(o.path, list);
-      }
-      o.path = null;
-      list.add(o);
-    }
-    for (List<CommentInfo> list : out.values()) {
-      Collections.sort(list, new Comparator<CommentInfo>() {
-        @Override
-        public int compare(CommentInfo a, CommentInfo b) {
-          int c = firstNonNull(a.side, Side.REVISION).ordinal()
-                - firstNonNull(b.side, Side.REVISION).ordinal();
-          if (c == 0) {
-            c = firstNonNull(a.line, 0) - firstNonNull(b.line, 0);
-          }
-          if (c == 0) {
-            c = a.id.compareTo(b.id);
-          }
-          return c;
-        }
-      });
-    }
-    if (accountLoader != null) {
-      accountLoader.fill();
-    }
-    return out;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCache.java
new file mode 100644
index 0000000..547a500
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCache.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+/** Cache for mergeability of commits into destination branches. */
+public interface MergeabilityCache {
+  public static class NotImplemented implements MergeabilityCache {
+    @Override
+    public boolean get(ObjectId commit, Ref intoRef, SubmitType submitType,
+        String mergeStrategy, Branch.NameKey dest, Repository repo,
+        ReviewDb db) {
+      throw new UnsupportedOperationException("Mergeability checking disabled");
+    }
+
+    @Override
+    public Boolean getIfPresent(ObjectId commit, Ref intoRef,
+        SubmitType submitType, String mergeStrategy) {
+      throw new UnsupportedOperationException("Mergeability checking disabled");
+    }
+  }
+
+  public boolean get(ObjectId commit, Ref intoRef, SubmitType submitType,
+      String mergeStrategy, Branch.NameKey dest, Repository repo, ReviewDb db);
+
+  public Boolean getIfPresent(ObjectId commit, Ref intoRef,
+      SubmitType submitType, String mergeStrategy);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
new file mode 100644
index 0000000..a840651
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -0,0 +1,312 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.ioutil.BasicSerialization.readString;
+import static com.google.gerrit.server.ioutil.BasicSerialization.writeString;
+import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
+import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.cache.Weigher;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableBiMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevFlag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+@Singleton
+public class MergeabilityCacheImpl implements MergeabilityCache {
+  private static final Logger log =
+      LoggerFactory.getLogger(MergeabilityCacheImpl.class);
+
+  private static final String CACHE_NAME = "mergeability";
+
+  public static final BiMap<SubmitType, Character> SUBMIT_TYPES = ImmutableBiMap.of(
+        SubmitType.FAST_FORWARD_ONLY, 'F',
+        SubmitType.MERGE_IF_NECESSARY, 'M',
+        SubmitType.REBASE_IF_NECESSARY, 'R',
+        SubmitType.MERGE_ALWAYS, 'A',
+        SubmitType.CHERRY_PICK, 'C');
+
+  static {
+    checkState(SUBMIT_TYPES.size() == SubmitType.values().length,
+        "SubmitType <-> char BiMap needs updating");
+  }
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        persist(CACHE_NAME, EntryKey.class, Boolean.class)
+            .maximumWeight(1 << 20)
+            .weigher(MergeabilityWeigher.class)
+            .loader(Loader.class);
+        bind(MergeabilityCache.class).to(MergeabilityCacheImpl.class);
+      }
+    };
+  }
+
+  public static ObjectId toId(Ref ref) {
+    return ref != null && ref.getObjectId() != null
+        ? ref.getObjectId()
+        : ObjectId.zeroId();
+  }
+
+  public static class EntryKey implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private ObjectId commit;
+    private ObjectId into;
+    private SubmitType submitType;
+    private String mergeStrategy;
+
+    // Only used for loading, not stored.
+    private transient LoadHelper load;
+
+    public EntryKey(ObjectId commit, ObjectId into, SubmitType submitType,
+        String mergeStrategy) {
+      this.commit = checkNotNull(commit, "commit");
+      this.into = checkNotNull(into, "into");
+      this.submitType = checkNotNull(submitType, "submitType");
+      this.mergeStrategy = checkNotNull(mergeStrategy, "mergeStrategy");
+    }
+
+    private EntryKey(ObjectId commit, ObjectId into, SubmitType submitType,
+        String mergeStrategy, Branch.NameKey dest, Repository repo,
+        ReviewDb db) {
+      this(commit, into, submitType, mergeStrategy);
+      load = new LoadHelper(dest, repo, db);
+    }
+
+    public ObjectId getCommit() {
+      return commit;
+    }
+
+    public ObjectId getInto() {
+      return into;
+    }
+
+    public SubmitType getSubmitType() {
+      return submitType;
+    }
+
+    public String getMergeStrategy() {
+      return mergeStrategy;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (o instanceof EntryKey) {
+        EntryKey k = (EntryKey) o;
+        return commit.equals(k.commit)
+            && into.equals(k.into)
+            && submitType == k.submitType
+            && mergeStrategy.equals(k.mergeStrategy);
+      }
+      return false;
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(commit, into, submitType, mergeStrategy);
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("commit", commit.name())
+          .add("into", into.name())
+          .addValue(submitType)
+          .addValue(mergeStrategy)
+          .toString();
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+      writeNotNull(out, commit);
+      writeNotNull(out, into);
+      Character c = SUBMIT_TYPES.get(submitType);
+      if (c == null) {
+        throw new IOException("Invalid submit type: " + submitType);
+      }
+      out.writeChar(c);
+      writeString(out, mergeStrategy);
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException {
+      commit = readNotNull(in);
+      into = readNotNull(in);
+      char t = in.readChar();
+      submitType = SUBMIT_TYPES.inverse().get(t);
+      if (submitType == null) {
+        throw new IOException("Invalid submit type code: " + t);
+      }
+      mergeStrategy = readString(in);
+    }
+  }
+
+  private static class LoadHelper {
+    private final Branch.NameKey dest;
+    private final Repository repo;
+    private final ReviewDb db;
+
+    private LoadHelper(Branch.NameKey dest, Repository repo, ReviewDb db) {
+      this.dest = checkNotNull(dest, "dest");
+      this.repo = checkNotNull(repo, "repo");
+      this.db = checkNotNull(db, "db");
+    }
+  }
+
+  @Singleton
+  public static class Loader extends CacheLoader<EntryKey, Boolean> {
+    private final SubmitStrategyFactory submitStrategyFactory;
+
+    @Inject
+    Loader(SubmitStrategyFactory submitStrategyFactory) {
+      this.submitStrategyFactory = submitStrategyFactory;
+    }
+
+    @Override
+    public Boolean load(EntryKey key)
+        throws NoSuchProjectException, MergeException, IOException {
+      checkArgument(key.load != null, "Key cannot be loaded: %s", key);
+      if (key.into.equals(ObjectId.zeroId())) {
+        return true; // Assume yes on new branch.
+      }
+      try {
+        RefDatabase refDatabase = key.load.repo.getRefDatabase();
+        Iterable<Ref> refs = Iterables.concat(
+            refDatabase.getRefs(Constants.R_HEADS).values(),
+            refDatabase.getRefs(Constants.R_TAGS).values());
+        try (RevWalk rw = CodeReviewCommit.newRevWalk(key.load.repo)) {
+          RevFlag canMerge = rw.newFlag("CAN_MERGE");
+          CodeReviewCommit rev = parse(rw, key.commit);
+          rev.add(canMerge);
+          CodeReviewCommit tip = parse(rw, key.into);
+          Set<RevCommit> accepted = alreadyAccepted(rw, refs);
+          accepted.add(tip);
+          accepted.addAll(Arrays.asList(rev.getParents()));
+          return submitStrategyFactory.create(
+              key.submitType,
+              key.load.db,
+              key.load.repo,
+              rw,
+              null /*inserter*/,
+              canMerge,
+              accepted,
+              key.load.dest).dryRun(tip, rev);
+        }
+      } finally {
+        key.load = null;
+      }
+    }
+
+    private static Set<RevCommit> alreadyAccepted(RevWalk rw, Iterable<Ref> refs)
+        throws MissingObjectException, IOException {
+      Set<RevCommit> accepted = Sets.newHashSet();
+      for (Ref r : refs) {
+        try {
+          accepted.add(rw.parseCommit(r.getObjectId()));
+        } catch (IncorrectObjectTypeException nonCommit) {
+          // Not a commit? Skip over it.
+        }
+      }
+      return accepted;
+    }
+
+    private static CodeReviewCommit parse(RevWalk rw, ObjectId id)
+        throws MissingObjectException, IncorrectObjectTypeException,
+        IOException {
+      return (CodeReviewCommit) rw.parseCommit(id);
+    }
+  }
+
+  public static class MergeabilityWeigher
+      implements Weigher<EntryKey, Boolean> {
+    @Override
+    public int weigh(EntryKey k, Boolean v) {
+      return 16 + 2 * (16 + 20) + 3 * 8 // Size of EntryKey, 64-bit JVM.
+          + 8; // Size of Boolean.
+    }
+  }
+
+  private final LoadingCache<EntryKey, Boolean> cache;
+
+  @Inject
+  MergeabilityCacheImpl(@Named(CACHE_NAME) LoadingCache<EntryKey, Boolean> cache) {
+    this.cache = cache;
+  }
+
+  @Override
+  public boolean get(ObjectId commit, Ref intoRef, SubmitType submitType,
+      String mergeStrategy, Branch.NameKey dest, Repository repo, ReviewDb db) {
+    ObjectId into = intoRef != null ? intoRef.getObjectId() : ObjectId.zeroId();
+    EntryKey key =
+        new EntryKey(commit, into, submitType, mergeStrategy, dest, repo, db);
+    try {
+      return cache.get(key);
+    } catch (ExecutionException e) {
+      log.error(String.format("Error checking mergeability of %s into %s (%s)",
+            key.commit.name(), key.into.name(), key.submitType.name()),
+          e.getCause());
+      return false;
+    }
+  }
+
+  @Override
+  public Boolean getIfPresent(ObjectId commit, Ref intoRef,
+      SubmitType submitType, String mergeStrategy) {
+    return cache.getIfPresent(
+        new EntryKey(commit, toId(intoRef), submitType, mergeStrategy));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCheckQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCheckQueue.java
deleted file mode 100644
index 6598238..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCheckQueue.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import com.google.common.collect.Sets;
-import com.google.gerrit.reviewdb.client.Change;
-
-import java.util.Collection;
-import java.util.Set;
-
-import javax.inject.Singleton;
-
-@Singleton
-class MergeabilityCheckQueue {
-  private final Set<Change.Id> pending = Sets.newHashSet();
-  private final Set<Change.Id> forcePending = Sets.newHashSet();
-
-  synchronized Set<Change> addAll(Collection<Change> changes, boolean force) {
-    Set<Change> r = Sets.newLinkedHashSetWithExpectedSize(changes.size());
-    for (Change c : changes) {
-      if (force ? forcePending.add(c.getId()) : pending.add(c.getId())) {
-        r.add(c);
-      }
-    }
-    return r;
-  }
-
-  synchronized void updatingMergeabilityFlag(Change change, boolean force) {
-    if (force) {
-      forcePending.remove(change.getId());
-    }
-    pending.remove(change.getId());
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecker.java
deleted file mode 100644
index 0a6db2b..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecker.java
+++ /dev/null
@@ -1,373 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import com.google.common.base.Function;
-import com.google.common.base.Throwables;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.common.util.concurrent.AsyncFunction;
-import com.google.common.util.concurrent.CheckedFuture;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
-import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.change.MergeabilityChecksExecutor.Priority;
-import com.google.gerrit.server.change.Mergeable.MergeableInfo;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.git.WorkQueue.Executor;
-import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.util.RequestContext;
-import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.ObjectId;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-
-public class MergeabilityChecker implements GitReferenceUpdatedListener {
-  private static final Logger log = LoggerFactory
-      .getLogger(MergeabilityChecker.class);
-
-  private static final Function<Exception, IOException> MAPPER =
-      new Function<Exception, IOException>() {
-    @Override
-    public IOException apply(Exception in) {
-      if (in instanceof IOException) {
-        return (IOException) in;
-      } else if (in instanceof ExecutionException
-          && in.getCause() instanceof IOException) {
-        return (IOException) in.getCause();
-      } else {
-        return new IOException(in);
-      }
-    }
-  };
-
-  public class Check {
-    private List<Change> changes;
-    private List<Branch.NameKey> branches;
-    private List<Project.NameKey> projects;
-    private boolean force;
-    private boolean reindex;
-    private boolean interactive;
-
-    private Check() {
-      changes = Lists.newArrayListWithExpectedSize(1);
-      branches = Lists.newArrayListWithExpectedSize(1);
-      projects = Lists.newArrayListWithExpectedSize(1);
-      interactive = true;
-    }
-
-    public Check addChange(Change change) {
-      changes.add(change);
-      return this;
-    }
-
-    public Check addBranch(Branch.NameKey branch) {
-      branches.add(branch);
-      interactive = false;
-      return this;
-    }
-
-    public Check addProject(Project.NameKey project) {
-      projects.add(project);
-      interactive = false;
-      return this;
-    }
-
-    /** Force reindexing regardless of whether mergeable flag was modified. */
-    public Check reindex() {
-      reindex = true;
-      return this;
-    }
-
-    /** Force mergeability check even if change is not stale. */
-    private Check force() {
-      force = true;
-      return this;
-    }
-
-    private ListeningExecutorService getExecutor() {
-      return interactive ? interactiveExecutor : backgroundExecutor;
-    }
-
-    public CheckedFuture<?, IOException> runAsync() {
-      final ListeningExecutorService executor = getExecutor();
-      ListenableFuture<List<Change>> getChanges;
-      if (branches.isEmpty() && projects.isEmpty()) {
-        getChanges = Futures.immediateFuture(changes);
-      } else {
-        getChanges = executor.submit(
-            new Callable<List<Change>>() {
-              @Override
-              public List<Change> call() throws OrmException {
-                return getChanges();
-              }
-            });
-      }
-
-      return Futures.makeChecked(Futures.transform(getChanges,
-          new AsyncFunction<List<Change>, List<Object>>() {
-            @Override
-            public ListenableFuture<List<Object>> apply(List<Change> changes) {
-              List<ListenableFuture<?>> result =
-                  Lists.newArrayListWithCapacity(changes.size());
-              for (final Change c : changes) {
-                ListenableFuture<Boolean> b =
-                    executor.submit(new Task(c, force));
-                if (reindex) {
-                  result.add(Futures.transform(
-                      b, new AsyncFunction<Boolean, Object>() {
-                        @SuppressWarnings("unchecked")
-                        @Override
-                        public ListenableFuture<Object> apply(
-                            Boolean indexUpdated) throws Exception {
-                          if (!indexUpdated) {
-                            return (ListenableFuture<Object>)
-                                indexer.indexAsync(c.getId());
-                          }
-                          return Futures.immediateFuture(null);
-                        }
-                      }));
-                } else {
-                  result.add(b);
-                }
-              }
-              return Futures.allAsList(result);
-            }
-          }), MAPPER);
-    }
-
-    public void run() throws IOException {
-      try {
-        runAsync().checkedGet();
-      } catch (Exception e) {
-        Throwables.propagateIfPossible(e, IOException.class);
-        throw MAPPER.apply(e);
-      }
-    }
-
-    private List<Change> getChanges() throws OrmException {
-      ReviewDb db = schemaFactory.open();
-      try {
-        List<Change> results = Lists.newArrayList();
-        results.addAll(changes);
-        for (Project.NameKey p : projects) {
-          Iterables.addAll(results, db.changes().byProjectOpenAll(p));
-        }
-        for (Branch.NameKey b : branches) {
-          Iterables.addAll(results, db.changes().byBranchOpenAll(b));
-        }
-        return results;
-      } catch (OrmException e) {
-        log.error("Failed to fetch changes for mergeability check", e);
-        throw e;
-      } finally {
-        db.close();
-      }
-    }
-  }
-
-  private final ThreadLocalRequestContext tl;
-  private final SchemaFactory<ReviewDb> schemaFactory;
-  private final IdentifiedUser.GenericFactory identifiedUserFactory;
-  private final ChangeControl.GenericFactory changeControlFactory;
-  private final Provider<Mergeable> mergeable;
-  private final ChangeIndexer indexer;
-  private final ListeningExecutorService backgroundExecutor;
-  private final ListeningExecutorService interactiveExecutor;
-  private final MergeabilityCheckQueue mergeabilityCheckQueue;
-  private final MetaDataUpdate.Server metaDataUpdateFactory;
-
-  @Inject
-  public MergeabilityChecker(ThreadLocalRequestContext tl,
-      SchemaFactory<ReviewDb> schemaFactory,
-      IdentifiedUser.GenericFactory identifiedUserFactory,
-      ChangeControl.GenericFactory changeControlFactory,
-      Provider<Mergeable> mergeable, ChangeIndexer indexer,
-      @MergeabilityChecksExecutor(Priority.BACKGROUND)
-        Executor backgroundExecutor,
-      @MergeabilityChecksExecutor(Priority.INTERACTIVE)
-        Executor interactiveExecutor,
-      MergeabilityCheckQueue mergeabilityCheckQueue,
-      MetaDataUpdate.Server metaDataUpdateFactory) {
-    this.tl = tl;
-    this.schemaFactory = schemaFactory;
-    this.identifiedUserFactory = identifiedUserFactory;
-    this.changeControlFactory = changeControlFactory;
-    this.mergeable = mergeable;
-    this.indexer = indexer;
-    this.backgroundExecutor =
-        MoreExecutors.listeningDecorator(backgroundExecutor);
-    this.interactiveExecutor =
-        MoreExecutors.listeningDecorator(interactiveExecutor);
-    this.mergeabilityCheckQueue = mergeabilityCheckQueue;
-    this.metaDataUpdateFactory = metaDataUpdateFactory;
-  }
-
-  public Check newCheck() {
-    return new Check();
-  }
-
-  @Override
-  public void onGitReferenceUpdated(GitReferenceUpdatedListener.Event event) {
-    String ref = event.getRefName();
-    if (ref.startsWith(Constants.R_HEADS) || ref.equals(RefNames.REFS_CONFIG)) {
-      Branch.NameKey branch = new Branch.NameKey(
-          new Project.NameKey(event.getProjectName()), ref);
-      newCheck().addBranch(branch).runAsync();
-    }
-    if (ref.equals(RefNames.REFS_CONFIG)) {
-      Project.NameKey p = new Project.NameKey(event.getProjectName());
-      try {
-        ProjectConfig oldCfg = parseConfig(p, event.getOldObjectId());
-        ProjectConfig newCfg = parseConfig(p, event.getNewObjectId());
-        if (recheckMerges(oldCfg, newCfg)) {
-          newCheck().addProject(p).force().runAsync();
-        }
-      } catch (ConfigInvalidException | IOException e) {
-        String msg = "Failed to update mergeability flags for project " + p.get()
-            + " on update of " + RefNames.REFS_CONFIG;
-        log.error(msg, e);
-        throw new RuntimeException(msg, e);
-      }
-    }
-  }
-
-  private boolean recheckMerges(ProjectConfig oldCfg, ProjectConfig newCfg) {
-    if (oldCfg == null || newCfg == null) {
-      return true;
-    }
-    return !oldCfg.getProject().getSubmitType().equals(newCfg.getProject().getSubmitType())
-        || oldCfg.getProject().getUseContentMerge() != newCfg.getProject().getUseContentMerge()
-        || (oldCfg.getRulesId() == null
-            ? newCfg.getRulesId() != null
-            : !oldCfg.getRulesId().equals(newCfg.getRulesId()));
-  }
-
-  private ProjectConfig parseConfig(Project.NameKey p, String idStr)
-      throws IOException, ConfigInvalidException, RepositoryNotFoundException {
-    ObjectId id = ObjectId.fromString(idStr);
-    if (ObjectId.zeroId().equals(id)) {
-      return null;
-    }
-    return ProjectConfig.read(metaDataUpdateFactory.create(p), id);
-  }
-
-  private class Task implements Callable<Boolean> {
-    private final Change change;
-    private final boolean force;
-
-    private ReviewDb reviewDb;
-
-    Task(Change change, boolean force) {
-      this.change = change;
-      this.force = force;
-    }
-
-    @Override
-    public String toString() {
-      return "mergeability-check-change-" + change.getId().get() + "-project-"
-          + change.getDest().getParentKey();
-    }
-
-    @Override
-    public Boolean call() throws Exception {
-      mergeabilityCheckQueue.updatingMergeabilityFlag(change, force);
-
-      RequestContext context = new RequestContext() {
-        @Override
-        public CurrentUser getCurrentUser() {
-          return identifiedUserFactory.create(change.getOwner());
-        }
-
-        @Override
-        public Provider<ReviewDb> getReviewDbProvider() {
-          return new Provider<ReviewDb>() {
-            @Override
-            public ReviewDb get() {
-              if (reviewDb == null) {
-                try {
-                  reviewDb = schemaFactory.open();
-                } catch (OrmException e) {
-                  throw new ProvisionException("Cannot open ReviewDb", e);
-                }
-              }
-              return reviewDb;
-            }
-          };
-        }
-      };
-      RequestContext old = tl.setContext(context);
-      ReviewDb db = context.getReviewDbProvider().get();
-      try {
-        PatchSet ps = db.patchSets().get(change.currentPatchSetId());
-        if (ps == null) {
-          // Cannot compute mergeability if current patch set is missing.
-          return false;
-        }
-
-        Mergeable m = mergeable.get();
-        m.setForce(force);
-
-        ChangeControl control =
-            changeControlFactory.controlFor(change, context.getCurrentUser());
-        MergeableInfo info = m.apply(
-            new RevisionResource(new ChangeResource(control), ps));
-        return change.isMergeable() != info.mergeable;
-      } catch (ResourceConflictException e) {
-        // change is closed
-        return false;
-      } catch (Exception e) {
-        log.error(String.format(
-            "cannot update mergeability flag of change %d in project %s after update of %s",
-            change.getId().get(),
-            change.getDest().getParentKey(), change.getDest().get()), e);
-        throw e;
-      } finally {
-        tl.setContext(old);
-        if (reviewDb != null) {
-          reviewDb.close();
-          reviewDb = null;
-        }
-      }
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecksExecutor.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecksExecutor.java
deleted file mode 100644
index 632e6ac..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecksExecutor.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-import com.google.gerrit.server.git.WorkQueue.Executor;
-import com.google.inject.BindingAnnotation;
-
-import java.lang.annotation.Retention;
-
-/**
- * Marker on the global {@link Executor} used by
- * {@link MergeabilityChecker}.
- */
-@Retention(RUNTIME)
-@BindingAnnotation
-public @interface MergeabilityChecksExecutor {
-  public enum Priority {
-    BACKGROUND, INTERACTIVE;
-  }
-
-  Priority value();
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecksExecutorModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecksExecutorModule.java
deleted file mode 100644
index e5bcabe..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecksExecutorModule.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import com.google.gerrit.server.change.MergeabilityChecksExecutor.Priority;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.WorkQueue;
-import com.google.inject.AbstractModule;
-import com.google.inject.Provides;
-import com.google.inject.Singleton;
-
-import org.eclipse.jgit.lib.Config;
-
-/** Module providing the {@link MergeabilityChecksExecutor}. */
-public class MergeabilityChecksExecutorModule extends AbstractModule {
-  @Override
-  protected void configure() {
-  }
-
-  @Provides
-  @Singleton
-  @MergeabilityChecksExecutor(Priority.BACKGROUND)
-  public WorkQueue.Executor createMergeabilityChecksExecutor(
-      @GerritServerConfig Config config,
-      WorkQueue queues) {
-    int poolSize = config.getInt("changeMerge", null, "threadPoolSize", 1);
-    return queues.createQueue(poolSize, "MergeabilityChecks-Background");
-  }
-
-  @Provides
-  @Singleton
-  @MergeabilityChecksExecutor(Priority.INTERACTIVE)
-  public WorkQueue.Executor createMergeabilityChecksExecutor(
-      @GerritServerConfig Config config,
-      WorkQueue queues,
-      @MergeabilityChecksExecutor(Priority.BACKGROUND)
-        WorkQueue.Executor backgroundExecutor) {
-    int poolSize =
-        config.getInt("changeMerge", null, "interactiveThreadPoolSize", 1);
-    if (poolSize <= 0) {
-      return backgroundExecutor;
-    }
-    return queues.createQueue(poolSize, "MergeabilityChecks-Interactive");
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
index c17afb8..2653f1b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
@@ -14,93 +14,75 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.collect.Sets;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.MergeableInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.git.BranchOrderSection;
-import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MergeException;
-import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
+import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevFlag;
-import org.eclipse.jgit.revwalk.RevWalk;
 import org.kohsuke.args4j.Option;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.Objects;
 
 public class Mergeable implements RestReadView<RevisionResource> {
   private static final Logger log = LoggerFactory.getLogger(Mergeable.class);
 
-  public static class MergeableInfo {
-    public SubmitType submitType;
-    public boolean mergeable;
-    public List<String> mergeableInto;
-  }
-
   @Option(name = "--other-branches", aliases = {"-o"},
       usage = "test mergeability for other branches too")
   private boolean otherBranches;
 
-  @Option(name = "--force", aliases = {"-f"},
-      usage = "force recheck of mergeable field")
-  public void setForce(boolean force) {
-    this.force = force;
-  }
-
-  private final TestSubmitType.Get submitType;
   private final GitRepositoryManager gitManager;
   private final ProjectCache projectCache;
-  private final SubmitStrategyFactory submitStrategyFactory;
+  private final MergeUtil.Factory mergeUtilFactory;
+  private final ChangeData.Factory changeDataFactory;
   private final Provider<ReviewDb> db;
   private final ChangeIndexer indexer;
-
-  private boolean force;
+  private final MergeabilityCache cache;
 
   @Inject
-  Mergeable(TestSubmitType.Get submitType,
-      GitRepositoryManager gitManager,
+  Mergeable(GitRepositoryManager gitManager,
       ProjectCache projectCache,
-      SubmitStrategyFactory submitStrategyFactory,
+      MergeUtil.Factory mergeUtilFactory,
+      ChangeData.Factory changeDataFactory,
       Provider<ReviewDb> db,
-      ChangeIndexer indexer) {
-    this.submitType = submitType;
+      ChangeIndexer indexer,
+      MergeabilityCache cache) {
     this.gitManager = gitManager;
     this.projectCache = projectCache;
-    this.submitStrategyFactory = submitStrategyFactory;
+    this.mergeUtilFactory = mergeUtilFactory;
+    this.changeDataFactory = changeDataFactory;
     this.db = db;
     this.indexer = indexer;
+    this.cache = cache;
+  }
+
+  public void setOtherBranches(boolean otherBranches) {
+    this.otherBranches = otherBranches;
   }
 
   @Override
@@ -117,30 +99,49 @@
       return result;
     }
 
-    result.submitType = submitType.apply(resource);
-    result.mergeable = change.isMergeable();
+    ChangeData cd = changeDataFactory.create(db.get(), resource.getControl());
+    SubmitTypeRecord rec = new SubmitRuleEvaluator(cd)
+        .setPatchSet(ps)
+        .getSubmitType();
+    if (rec.status != SubmitTypeRecord.Status.OK) {
+      throw new OrmException("Submit type rule failed: " + rec);
+    }
+    result.submitType = rec.type;
 
     Repository git = gitManager.openRepository(change.getProject());
     try {
-      Map<String, Ref> refs = git.getRefDatabase().getRefs(RefDatabase.ALL);
-      Ref ref = refs.get(change.getDest().get());
-      if (force || isStale(change, ref)) {
-        result.mergeable =
-            refresh(change, ps, result.submitType, git, refs, ref);
+      ObjectId commit = toId(ps);
+      if (commit == null) {
+        result.mergeable = false;
+        return result;
+      }
+
+      Ref ref = git.getRef(change.getDest().get());
+      ProjectState projectState = projectCache.get(change.getProject());
+      String strategy = mergeUtilFactory.create(projectState)
+          .mergeStrategyName();
+      Boolean old =
+          cache.getIfPresent(commit, ref, result.submitType, strategy);
+
+      if (old == null) {
+        result.mergeable = refresh(change, commit, ref, result.submitType,
+            strategy, git, old);
+      } else {
+        result.mergeable = old;
       }
 
       if (otherBranches) {
         result.mergeableInto = new ArrayList<>();
-        BranchOrderSection branchOrder =
-            projectCache.get(change.getProject()).getBranchOrderSection();
+        BranchOrderSection branchOrder = projectState.getBranchOrderSection();
         if (branchOrder != null) {
           int prefixLen = Constants.R_HEADS.length();
           for (String n : branchOrder.getMoreStable(ref.getName())) {
-            Ref other = refs.get(n);
+            Ref other = git.getRef(n);
             if (other == null) {
               continue;
             }
-            if (isMergeable(change, ps, SubmitType.CHERRY_PICK, git, refs, other)) {
+            if (cache.get(commit, other, SubmitType.CHERRY_PICK, strategy,
+                change.getDest(), git, db.get())) {
               result.mergeableInto.add(other.getName().substring(prefixLen));
             }
           }
@@ -152,121 +153,25 @@
     return result;
   }
 
-  private static boolean isStale(Change change, Ref ref) {
-    return change.getLastSha1MergeTested() == null
-        || !toRevId(ref).equals(change.getLastSha1MergeTested());
+  private static ObjectId toId(PatchSet ps) {
+    try {
+      return ObjectId.fromString(ps.getRevision().get());
+    } catch (IllegalArgumentException e) {
+      log.error("Invalid revision on patch set " + ps);
+      return null;
+    }
   }
 
-  private static RevId toRevId(Ref ref) {
-    return new RevId(ref != null && ref.getObjectId() != null
-        ? ref.getObjectId().name()
-        : "");
-  }
-
-  private boolean refresh(Change change,
-      final PatchSet ps,
-      SubmitType type,
-      Repository git,
-      Map<String, Ref> refs,
-      final Ref ref) throws IOException, OrmException {
-
-    final boolean mergeable = isMergeable(change, ps, type, git, refs, ref);
-
-    Change c = db.get().changes().atomicUpdate(
-        change.getId(),
-        new AtomicUpdate<Change>() {
-          @Override
-          public Change update(Change c) {
-            if (c.getStatus().isOpen()
-                && ps.getId().equals(c.currentPatchSetId())) {
-              c.setMergeable(mergeable);
-              c.setLastSha1MergeTested(toRevId(ref));
-              return c;
-            } else {
-              return null;
-            }
-          }
-        });
-    if (c != null) {
-      indexer.index(db.get(), c);
+  private boolean refresh(final Change change, ObjectId commit,
+      final Ref ref, SubmitType type, String strategy, Repository git,
+      Boolean old) throws OrmException, IOException {
+    final boolean mergeable =
+        cache.get(commit, ref, type, strategy, change.getDest(), git, db.get());
+    if (!Objects.equals(mergeable, old)) {
+      // TODO(dborowitz): Include cache info in ETag somehow instead.
+      ChangeUtil.bumpRowVersionNotLastUpdatedOn(change.getId(), db.get());
+      indexer.index(db.get(), change);
     }
     return mergeable;
   }
-
-  private boolean isMergeable(Change change,
-      final PatchSet ps,
-      SubmitType type,
-      Repository git,
-      Map<String, Ref> refs,
-      final Ref ref) throws IOException, OrmException {
-    RevWalk rw = new RevWalk(git) {
-      @Override
-      protected CodeReviewCommit createCommit(AnyObjectId id) {
-        return new CodeReviewCommit(id);
-      }
-    };
-    try {
-      ObjectId id;
-      try {
-        id = ObjectId.fromString(ps.getRevision().get());
-      } catch (IllegalArgumentException e) {
-        log.error(String.format(
-            "Invalid revision on patch set %d of %d",
-            ps.getId().get(),
-            change.getId().get()));
-        return false;
-      }
-
-      RevFlag canMerge = rw.newFlag("CAN_MERGE");
-      CodeReviewCommit rev = parse(rw, id);
-      rev.add(canMerge);
-
-      final boolean mergeable;
-      if (ref == null || ref.getObjectId() == null) {
-        mergeable = true; // Assume yes on new branch.
-      } else {
-        CodeReviewCommit tip = parse(rw, ref.getObjectId());
-        Set<RevCommit> accepted = alreadyAccepted(rw, refs.values());
-        accepted.add(tip);
-        accepted.addAll(Arrays.asList(rev.getParents()));
-        mergeable = submitStrategyFactory.create(
-            type,
-            db.get(),
-            git,
-            rw,
-            null /*inserter*/,
-            canMerge,
-            accepted,
-            change.getDest()).dryRun(tip, rev);
-      }
-      return mergeable;
-    } catch (MergeException | IOException | NoSuchProjectException e) {
-      log.error(String.format(
-          "Cannot merge test change %d", change.getId().get()), e);
-      return false;
-    } finally {
-      rw.release();
-    }
-  }
-
-  private static Set<RevCommit> alreadyAccepted(RevWalk rw, Collection<Ref> refs)
-      throws MissingObjectException, IOException {
-    Set<RevCommit> accepted = Sets.newHashSet();
-    for (Ref r : refs) {
-      if (r.getName().startsWith(Constants.R_HEADS)
-          || r.getName().startsWith(Constants.R_TAGS)) {
-        try {
-          accepted.add(rw.parseCommit(r.getObjectId()));
-        } catch (IncorrectObjectTypeException nonCommit) {
-          // Not a commit? Skip over it.
-        }
-      }
-    }
-    return accepted;
-  }
-
-  private static CodeReviewCommit parse(RevWalk rw, ObjectId id)
-      throws MissingObjectException, IncorrectObjectTypeException, IOException {
-    return (CodeReviewCommit) rw.parseCommit(id);
-  }
 }
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 6880ca2..d0e4c99 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
@@ -14,16 +14,17 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.gerrit.server.change.ChangeEditResource.CHANGE_EDIT_KIND;
 import static com.google.gerrit.server.change.ChangeResource.CHANGE_KIND;
 import static com.google.gerrit.server.change.CommentResource.COMMENT_KIND;
-import static com.google.gerrit.server.change.DraftResource.DRAFT_KIND;
+import static com.google.gerrit.server.change.DraftCommentResource.DRAFT_COMMENT_KIND;
 import static com.google.gerrit.server.change.FileResource.FILE_KIND;
 import static com.google.gerrit.server.change.ReviewerResource.REVIEWER_KIND;
 import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiModule;
-import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.Reviewed.DeleteReviewed;
 import com.google.gerrit.server.change.Reviewed.PutReviewed;
 import com.google.gerrit.server.config.FactoryModule;
@@ -34,26 +35,31 @@
     bind(ChangesCollection.class);
     bind(Revisions.class);
     bind(Reviewers.class);
-    bind(Drafts.class);
+    bind(DraftComments.class);
     bind(Comments.class);
     bind(Files.class);
 
     DynamicMap.mapOf(binder(), CHANGE_KIND);
     DynamicMap.mapOf(binder(), COMMENT_KIND);
-    DynamicMap.mapOf(binder(), DRAFT_KIND);
+    DynamicMap.mapOf(binder(), DRAFT_COMMENT_KIND);
     DynamicMap.mapOf(binder(), FILE_KIND);
     DynamicMap.mapOf(binder(), REVIEWER_KIND);
     DynamicMap.mapOf(binder(), REVISION_KIND);
+    DynamicMap.mapOf(binder(), CHANGE_EDIT_KIND);
 
     get(CHANGE_KIND).to(GetChange.class);
     get(CHANGE_KIND, "detail").to(GetDetail.class);
     get(CHANGE_KIND, "topic").to(GetTopic.class);
     get(CHANGE_KIND, "in").to(IncludedIn.class);
+    get(CHANGE_KIND, "hashtags").to(GetHashtags.class);
+    get(CHANGE_KIND, "check").to(Check.class);
+    post(CHANGE_KIND, "check").to(Check.class);
     put(CHANGE_KIND, "topic").to(PutTopic.class);
     delete(CHANGE_KIND, "topic").to(PutTopic.class);
     delete(CHANGE_KIND).to(DeleteDraftChange.class);
     post(CHANGE_KIND, "abandon").to(Abandon.class);
-    post(CHANGE_KIND, "publish").to(Publish.CurrentRevision.class);
+    post(CHANGE_KIND, "hashtags").to(PostHashtags.class);
+    post(CHANGE_KIND, "publish").to(PublishDraftPatchSet.CurrentRevision.class);
     post(CHANGE_KIND, "restore").to(Restore.class);
     post(CHANGE_KIND, "revert").to(Revert.class);
     post(CHANGE_KIND, "submit").to(Submit.CurrentRevision.class);
@@ -67,28 +73,28 @@
     delete(REVIEWER_KIND).to(DeleteReviewer.class);
 
     child(CHANGE_KIND, "revisions").to(Revisions.class);
+    get(REVISION_KIND, "actions").to(GetRevisionActions.class);
     post(REVISION_KIND, "cherrypick").to(CherryPick.class);
     get(REVISION_KIND, "commit").to(GetCommit.class);
     delete(REVISION_KIND).to(DeleteDraftPatchSet.class);
     get(REVISION_KIND, "mergeable").to(Mergeable.class);
-    post(REVISION_KIND, "publish").to(Publish.class);
+    post(REVISION_KIND, "publish").to(PublishDraftPatchSet.class);
     get(REVISION_KIND, "related").to(GetRelated.class);
     get(REVISION_KIND, "review").to(GetReview.class);
     post(REVISION_KIND, "review").to(PostReview.class);
     post(REVISION_KIND, "submit").to(Submit.class);
     post(REVISION_KIND, "rebase").to(Rebase.class);
-    post(REVISION_KIND, "message").to(EditMessage.class);
     get(REVISION_KIND, "patch").to(GetPatch.class);
     get(REVISION_KIND, "submit_type").to(TestSubmitType.Get.class);
     post(REVISION_KIND, "test.submit_rule").to(TestSubmitRule.class);
     post(REVISION_KIND, "test.submit_type").to(TestSubmitType.class);
     get(REVISION_KIND, "archive").to(GetArchive.class);
 
-    child(REVISION_KIND, "drafts").to(Drafts.class);
-    put(REVISION_KIND, "drafts").to(CreateDraft.class);
-    get(DRAFT_KIND).to(GetDraft.class);
-    put(DRAFT_KIND).to(PutDraft.class);
-    delete(DRAFT_KIND).to(DeleteDraft.class);
+    child(REVISION_KIND, "drafts").to(DraftComments.class);
+    put(REVISION_KIND, "drafts").to(CreateDraftComment.class);
+    get(DRAFT_COMMENT_KIND).to(GetDraftComment.class);
+    put(DRAFT_COMMENT_KIND).to(PutDraftComment.class);
+    delete(DRAFT_COMMENT_KIND).to(DeleteDraftComment.class);
 
     child(REVISION_KIND, "comments").to(Comments.class);
     get(COMMENT_KIND).to(GetComment.class);
@@ -99,14 +105,27 @@
     get(FILE_KIND, "content").to(GetContent.class);
     get(FILE_KIND, "diff").to(GetDiff.class);
 
+    child(CHANGE_KIND, "edit").to(ChangeEdits.class);
+    delete(CHANGE_KIND, "edit").to(DeleteChangeEdit.class);
+    child(CHANGE_KIND, "edit:publish").to(PublishChangeEdit.class);
+    child(CHANGE_KIND, "edit:rebase").to(RebaseChangeEdit.class);
+    put(CHANGE_KIND, "edit:message").to(ChangeEdits.EditMessage.class);
+    get(CHANGE_KIND, "edit:message").to(ChangeEdits.GetMessage.class);
+    put(CHANGE_EDIT_KIND, "/").to(ChangeEdits.Put.class);
+    delete(CHANGE_EDIT_KIND).to(ChangeEdits.DeleteContent.class);
+    get(CHANGE_EDIT_KIND, "/").to(ChangeEdits.Get.class);
+    get(CHANGE_EDIT_KIND, "meta").to(ChangeEdits.GetMeta.class);
+
     install(new FactoryModule() {
       @Override
       protected void configure() {
         factory(ReviewerResource.Factory.class);
-        factory(AccountInfo.Loader.Factory.class);
+        factory(AccountLoader.Factory.class);
         factory(EmailReviewComments.Factory.class);
         factory(ChangeInserter.Factory.class);
         factory(PatchSetInserter.Factory.class);
+        factory(ChangeEdits.Create.Factory.class);
+        factory(ChangeEdits.DeleteFile.Factory.class);
       }
     });
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index 635421a..baddd40 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.SetMultimap;
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -33,18 +34,20 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.BanCommit;
 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.ReplacePatchSetSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.ReviewerState;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ChangeModifiedException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.ssh.NoSshInfo;
 import com.google.gerrit.server.ssh.SshInfo;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -53,6 +56,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
@@ -87,7 +91,7 @@
   private final ChangeControl.GenericFactory ctlFactory;
   private final GitReferenceUpdated gitRefUpdated;
   private final CommitValidators.Factory commitValidatorsFactory;
-  private final MergeabilityChecker mergeabilityChecker;
+  private final ChangeIndexer indexer;
   private final ReplacePatchSetSender.Factory replacePatchSetFactory;
   private final ApprovalsUtil approvalsUtil;
   private final ApprovalCopier approvalCopier;
@@ -101,7 +105,6 @@
 
   private PatchSet patchSet;
   private ChangeMessage changeMessage;
-  private boolean copyLabels;
   private SshInfo sshInfo;
   private ValidatePolicy validatePolicy = ValidatePolicy.GERRIT;
   private boolean draft;
@@ -120,7 +123,7 @@
       PatchSetInfoFactory patchSetInfoFactory,
       GitReferenceUpdated gitRefUpdated,
       CommitValidators.Factory commitValidatorsFactory,
-      MergeabilityChecker mergeabilityChecker,
+      ChangeIndexer indexer,
       ReplacePatchSetSender.Factory replacePatchSetFactory,
       @Assisted Repository git,
       @Assisted RevWalk revWalk,
@@ -139,7 +142,7 @@
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.gitRefUpdated = gitRefUpdated;
     this.commitValidatorsFactory = commitValidatorsFactory;
-    this.mergeabilityChecker = mergeabilityChecker;
+    this.indexer = indexer;
     this.replacePatchSetFactory = replacePatchSetFactory;
 
     this.git = git;
@@ -177,17 +180,11 @@
     return this;
   }
 
-  public PatchSetInserter setMessage(ChangeMessage changeMessage)
-      throws OrmException {
+  public PatchSetInserter setMessage(ChangeMessage changeMessage) {
     this.changeMessage = changeMessage;
     return this;
   }
 
-  public PatchSetInserter setCopyLabels(boolean copyLabels) {
-    this.copyLabels = copyLabels;
-    return this;
-  }
-
   public PatchSetInserter setSshInfo(SshInfo sshInfo) {
     this.sshInfo = sshInfo;
     return this;
@@ -267,7 +264,6 @@
               if (change.getStatus() != Change.Status.DRAFT) {
                 change.setStatus(Change.Status.NEW);
               }
-              change.setLastSha1MergeTested(null);
               change.setCurrentPatchSet(patchSetInfoFactory.get(commit,
                   patchSet.getId()));
               ChangeUtil.updated(change);
@@ -283,9 +279,7 @@
         cmUtil.addChangeMessage(db, update, changeMessage);
       }
 
-      if (copyLabels) {
-        approvalCopier.copy(db, ctl, patchSet);
-      }
+      approvalCopier.copy(db, ctl, patchSet);
       db.commit();
       if (messageIsForChange()) {
         update.commit();
@@ -315,10 +309,7 @@
     } finally {
       db.rollback();
     }
-    mergeabilityChecker.newCheck()
-        .addChange(updatedChange)
-        .reindex()
-        .run();
+    indexer.index(db, updatedChange);
     if (runHooks) {
       hooks.doPatchsetCreatedHook(updatedChange, patchSet, db);
     }
@@ -356,7 +347,7 @@
     }
   }
 
-  private void validate() throws InvalidChangeOperationException {
+  private void validate() throws InvalidChangeOperationException, IOException {
     CommitValidators cv =
         commitValidatorsFactory.create(ctl.getRefControl(), sshInfo, git);
 
@@ -372,7 +363,8 @@
     try {
       switch (validatePolicy) {
       case RECEIVE_COMMITS:
-        cv.validateForReceiveCommits(event);
+        NoteMap rejectCommits = BanCommit.loadRejectCommitsMap(git, revWalk);
+        cv.validateForReceiveCommits(event, rejectCommits);
         break;
       case GERRIT:
         cv.validateForGerritCommits(event);
@@ -389,12 +381,4 @@
     return changeMessage != null && changeMessage.getKey().getParentKey()
         .equals(patchSet.getId().getParentKey());
   }
-
-  public class ChangeModifiedException extends InvalidChangeOperationException {
-    private static final long serialVersionUID = 1L;
-
-    public ChangeModifiedException(String msg) {
-      super(msg);
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
new file mode 100644
index 0000000..6638f91
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.api.changes.HashtagsInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+import java.util.Set;
+
+@Singleton
+public class PostHashtags implements RestModifyView<ChangeResource, HashtagsInput> {
+  private HashtagsUtil hashtagsUtil;
+
+  @Inject
+  PostHashtags(HashtagsUtil hashtagsUtil) {
+    this.hashtagsUtil = hashtagsUtil;
+  }
+
+  @Override
+  public Response<? extends Set<String>> apply(ChangeResource req, HashtagsInput input)
+      throws AuthException, OrmException, IOException, BadRequestException,
+      ResourceConflictException {
+
+    try {
+      return Response.ok(hashtagsUtil.setHashtags(
+          req.getControl(), input, true, true));
+    } catch (IllegalArgumentException e) {
+      throw new BadRequestException(e.getMessage());
+    } catch (ValidationException e) {
+      throw new ResourceConflictException(e.getMessage());
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index 38070c0..8e989b6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -15,8 +15,9 @@
 package com.google.gerrit.server.change;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -24,6 +25,7 @@
 import com.google.common.util.concurrent.CheckedFuture;
 import com.google.common.util.concurrent.Futures;
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.Permission;
@@ -32,9 +34,10 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
 import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
-import com.google.gerrit.extensions.common.Comment.Side;
+import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.restapi.Url;
@@ -44,7 +47,6 @@
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
@@ -54,18 +56,14 @@
 import com.google.gerrit.server.account.AccountsCollection;
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.util.LabelVote;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.eclipse.jgit.lib.ObjectId;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -133,8 +131,19 @@
 
   @Override
   public Output apply(RevisionResource revision, ReviewInput input)
-      throws AuthException, BadRequestException, UnprocessableEntityException,
-      OrmException, IOException {
+      throws AuthException, BadRequestException, ResourceConflictException,
+      UnprocessableEntityException, OrmException, IOException {
+    return apply(revision, input, TimeUtil.nowTs());
+  }
+
+  public Output apply(RevisionResource revision, ReviewInput input,
+      Timestamp ts) throws AuthException, BadRequestException,
+      ResourceConflictException, UnprocessableEntityException, OrmException,
+      IOException {
+    timestamp = ts;
+    if (revision.getEdit().isPresent()) {
+      throw new ResourceConflictException("cannot post review on edit");
+    }
     if (input.onBehalfOf != null) {
       revision = onBehalfOf(revision, input);
     }
@@ -149,15 +158,15 @@
       input.notify = NotifyHandling.NONE;
     }
 
-    ChangeUpdate update = null;
     db.get().changes().beginTransaction(revision.getChange().getId());
     boolean dirty = false;
     try {
       change = db.get().changes().get(revision.getChange().getId());
-      ChangeUtil.updated(change);
-      timestamp = change.getLastUpdatedOn();
+      if (change.getLastUpdatedOn().before(timestamp)) {
+        change.setLastUpdatedOn(timestamp);
+      }
 
-      update = updateFactory.create(revision.getControl(), timestamp);
+      ChangeUpdate update = updateFactory.create(revision.getControl(), timestamp);
       update.setPatchSetId(revision.getPatchSet().getId());
       dirty |= insertComments(revision, update, input.comments, input.drafts);
       dirty |= updateLabels(revision, update, input.labels);
@@ -166,12 +175,10 @@
         db.get().changes().update(Collections.singleton(change));
         db.get().commit();
       }
+      update.commit();
     } finally {
       db.get().rollback();
     }
-    if (update != null) {
-      update.commit();
-    }
 
     CheckedFuture<?, IOException> indexWrite;
     if (dirty) {
@@ -295,7 +302,7 @@
         in.entrySet().iterator();
     Set<String> filePaths =
         Sets.newHashSet(changeDataFactory.create(
-            db.get(), revision.getChange()).filePaths(
+            db.get(), revision.getControl()).filePaths(
                 revision.getPatchSet()));
     while (mapItr.hasNext()) {
       Map.Entry<String, List<CommentInput>> ent = mapItr.next();
@@ -319,7 +326,7 @@
           listItr.remove();
           continue;
         }
-        if (c.line < 0) {
+        if (c.line != null && c.line < 0) {
           throw new BadRequestException(String.format(
               "negative line number %d not allowed on %s",
               c.line, path));
@@ -337,7 +344,7 @@
 
   private boolean insertComments(RevisionResource rsrc,
       ChangeUpdate update, Map<String, List<CommentInput>> in, DraftHandling draftsHandling)
-      throws OrmException, IOException {
+      throws OrmException {
     if (in == null) {
       in = Collections.emptyMap();
     }
@@ -350,15 +357,6 @@
     List<PatchLineComment> del = Lists.newArrayList();
     List<PatchLineComment> ups = Lists.newArrayList();
 
-    PatchList patchList = null;
-    try {
-      patchList = patchListCache.get(rsrc.getChange(), rsrc.getPatchSet());
-    } catch (PatchListNotAvailableException e) {
-      throw new OrmException("could not load PatchList for this patchset", e);
-    }
-    RevId patchSetCommit = new RevId(ObjectId.toString(patchList.getNewId()));
-    RevId baseCommit = new RevId(ObjectId.toString(patchList.getOldId()));;
-
     for (Map.Entry<String, List<CommentInput>> ent : in.entrySet()) {
       String path = ent.getKey();
       for (CommentInput c : ent.getValue()) {
@@ -369,7 +367,7 @@
               new PatchLineComment.Key(
                   new Patch.Key(rsrc.getPatchSet().getId(), path),
                   ChangeUtil.messageUUID(db.get())),
-              c.line,
+              c.line != null ? c.line : 0,
               rsrc.getAccountId(),
               parent, timestamp);
         } else if (parent != null) {
@@ -378,7 +376,8 @@
         e.setStatus(PatchLineComment.Status.PUBLISHED);
         e.setWrittenOn(timestamp);
         e.setSide(c.side == Side.PARENT ? (short) 0 : (short) 1);
-        e.setRevId(c.side == Side.PARENT ? baseCommit : patchSetCommit);
+        setCommentRevId(e, patchListCache, rsrc.getChange(),
+            rsrc.getPatchSet());
         e.setMessage(c.message);
         if (c.range != null) {
           e.setRange(new CommentRange(
@@ -392,7 +391,7 @@
       }
     }
 
-    switch (Objects.firstNonNull(draftsHandling, DraftHandling.DELETE)) {
+    switch (MoreObjects.firstNonNull(draftsHandling, DraftHandling.DELETE)) {
       case KEEP:
       default:
         break;
@@ -403,13 +402,14 @@
         for (PatchLineComment e : drafts.values()) {
           e.setStatus(PatchLineComment.Status.PUBLISHED);
           e.setWrittenOn(timestamp);
-          e.setRevId(e.getSide() == (short) 0 ? baseCommit : patchSetCommit);
+          setCommentRevId(e, patchListCache, rsrc.getChange(),
+              rsrc.getPatchSet());
           ups.add(e);
         }
         break;
     }
-    db.get().patchComments().delete(del);
-    plcUtil.addPublishedComments(db.get(), update, ups);
+    plcUtil.deleteComments(db.get(), update, del);
+    plcUtil.upsertComments(db.get(), update, ups);
     comments.addAll(ups);
     return !del.isEmpty() || !ups.isEmpty();
   }
@@ -417,9 +417,8 @@
   private Map<String, PatchLineComment> scanDraftComments(
       RevisionResource rsrc) throws OrmException {
     Map<String, PatchLineComment> drafts = Maps.newHashMap();
-    for (PatchLineComment c : db.get().patchComments().draftByPatchSetAuthor(
-          rsrc.getPatchSet().getId(),
-          rsrc.getAccountId())) {
+    for (PatchLineComment c : plcUtil.draftByPatchSetAuthor(db.get(),
+        rsrc.getPatchSet().getId(), rsrc.getAccountId(), rsrc.getNotes())) {
       drafts.put(c.getKey().get(), c);
     }
     return drafts;
@@ -535,7 +534,7 @@
   }
 
   private void addLabelDelta(String name, short value) {
-    labelDelta.add(new LabelVote(name, value).format());
+    labelDelta.add(LabelVote.create(name, value).format());
   }
 
   private boolean insertMessage(RevisionResource rsrc, String msg,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index 5b95ab2..ecfb43c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -39,7 +39,7 @@
 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.AccountInfo;
+import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.AccountsCollection;
 import com.google.gerrit.server.account.GroupMembers;
 import com.google.gerrit.server.change.ReviewerJson.PostResult;
@@ -81,7 +81,7 @@
   private final AddReviewerSender.Factory addReviewerSenderFactory;
   private final GroupsCollection groupsCollection;
   private final GroupMembers.Factory groupMembersFactory;
-  private final AccountInfo.Loader.Factory accountLoaderFactory;
+  private final AccountLoader.Factory accountLoaderFactory;
   private final Provider<ReviewDb> dbProvider;
   private final ChangeUpdate.Factory updateFactory;
   private final Provider<CurrentUser> currentUser;
@@ -99,7 +99,7 @@
       AddReviewerSender.Factory addReviewerSenderFactory,
       GroupsCollection groupsCollection,
       GroupMembers.Factory groupMembersFactory,
-      AccountInfo.Loader.Factory accountLoaderFactory,
+      AccountLoader.Factory accountLoaderFactory,
       Provider<ReviewDb> db,
       ChangeUpdate.Factory updateFactory,
       Provider<CurrentUser> currentUser,
@@ -150,7 +150,7 @@
   }
 
   private PostResult putAccount(ReviewerResource rsrc) throws OrmException,
-      EmailException, IOException {
+      IOException {
     Account member = rsrc.getUser().getAccount();
     ChangeControl control = rsrc.getControl();
     PostResult result = new PostResult();
@@ -162,7 +162,7 @@
 
   private PostResult putGroup(ChangeResource rsrc, AddReviewerInput input)
       throws BadRequestException,
-      UnprocessableEntityException, OrmException, EmailException, IOException {
+      UnprocessableEntityException, OrmException, IOException {
     GroupDescription.Basic group = groupsCollection.parseInternal(input.reviewer);
     PostResult result = new PostResult();
     if (!isLegalReviewerGroup(group.getGroupUUID())) {
@@ -228,7 +228,7 @@
 
   private void addReviewers(ChangeResource rsrc, PostResult result,
       Map<Account.Id, ChangeControl> reviewers)
-      throws OrmException, EmailException, IOException {
+      throws OrmException, IOException {
     ReviewDb db = dbProvider.get();
     ChangeUpdate update = updateFactory.create(rsrc.getControl());
     List<PatchSetApproval> added;
@@ -266,8 +266,7 @@
     }
   }
 
-  private void emailReviewers(Change change, List<PatchSetApproval> added)
-      throws OrmException, EmailException {
+  private void emailReviewers(Change change, List<PatchSetApproval> added) {
     if (added.isEmpty()) {
       return;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
new file mode 100644
index 0000000..952636f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Optional;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsPost;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.edit.ChangeEdit;
+import com.google.gerrit.server.edit.ChangeEditUtil;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+
+@Singleton
+public class PublishChangeEdit implements
+    ChildCollection<ChangeResource, ChangeEditResource>,
+    AcceptsPost<ChangeResource> {
+
+  private final Publish publish;
+
+  @Inject
+  PublishChangeEdit(Publish publish) {
+    this.publish = publish;
+  }
+
+  @Override
+  public DynamicMap<RestView<ChangeEditResource>> views() {
+    throw new NotImplementedException();
+  }
+
+  @Override
+  public RestView<ChangeResource> list() {
+    throw new NotImplementedException();
+  }
+
+  @Override
+  public ChangeEditResource parse(ChangeResource parent, IdString id) {
+    throw new NotImplementedException();
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public Publish post(ChangeResource parent) throws RestApiException {
+    return publish;
+  }
+
+  @Singleton
+  public static class Publish implements RestModifyView<ChangeResource, Publish.Input> {
+    public static class Input {
+    }
+
+    private final ChangeEditUtil editUtil;
+
+    @Inject
+    Publish(ChangeEditUtil editUtil) {
+      this.editUtil = editUtil;
+    }
+
+    @Override
+    public Response<?> apply(ChangeResource rsrc, Publish.Input in)
+        throws AuthException, ResourceConflictException, NoSuchChangeException,
+        IOException, InvalidChangeOperationException, OrmException {
+      Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
+      if (!edit.isPresent()) {
+        throw new ResourceConflictException(String.format(
+            "no edit exists for change %s",
+            rsrc.getChange().getChangeId()));
+      }
+      editUtil.publish(edit.get());
+      return Response.none();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Publish.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
similarity index 78%
rename from gerrit-server/src/main/java/com/google/gerrit/server/change/Publish.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
index 88da094..9f77f0e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Publish.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
@@ -26,24 +26,20 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.change.Publish.Input;
-import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.change.PublishDraftPatchSet.Input;
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.PatchSetNotificationSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.lib.Config;
-
 import java.io.IOException;
 
 @Singleton
-public class Publish implements RestModifyView<RevisionResource, Input>,
+public class PublishDraftPatchSet implements RestModifyView<RevisionResource, Input>,
     UiAction<RevisionResource> {
   public static class Input {
   }
@@ -53,21 +49,18 @@
   private final PatchSetNotificationSender sender;
   private final ChangeHooks hooks;
   private final ChangeIndexer indexer;
-  private final boolean allowDrafts;
 
   @Inject
-  public Publish(Provider<ReviewDb> dbProvider,
+  public PublishDraftPatchSet(Provider<ReviewDb> dbProvider,
       ChangeUpdate.Factory updateFactory,
       PatchSetNotificationSender sender,
       ChangeHooks hooks,
-      ChangeIndexer indexer,
-      @GerritServerConfig Config cfg) {
+      ChangeIndexer indexer) {
     this.dbProvider = dbProvider;
     this.updateFactory = updateFactory;
     this.sender = sender;
     this.hooks = hooks;
     this.indexer = indexer;
-    this.allowDrafts = cfg.getBoolean("change", "allowDrafts", true);
   }
 
   @Override
@@ -82,30 +75,22 @@
       throw new AuthException("Cannot publish this draft patch set");
     }
 
-    if (!allowDrafts) {
-      throw new ResourceConflictException("Draft workflow is disabled.");
-    }
-
     PatchSet updatedPatchSet = updateDraftPatchSet(rsrc);
     Change updatedChange = updateDraftChange(rsrc);
     ChangeUpdate update = updateFactory.create(rsrc.getControl(),
         updatedChange.getLastUpdatedOn());
 
-    try {
-      if (!updatedPatchSet.isDraft()
-          || updatedChange.getStatus() == Change.Status.NEW) {
-        CheckedFuture<?, IOException> indexFuture =
-            indexer.indexAsync(updatedChange.getId());
-        sender.send(rsrc.getNotes(), update,
-            rsrc.getChange().getStatus() == Change.Status.DRAFT,
-            rsrc.getUser(), updatedChange, updatedPatchSet,
-            rsrc.getControl().getLabelTypes());
-        indexFuture.checkedGet();
-        hooks.doDraftPublishedHook(updatedChange, updatedPatchSet,
-            dbProvider.get());
-      }
-    } catch (PatchSetInfoNotAvailableException e) {
-      throw new ResourceNotFoundException(e.getMessage());
+    if (!updatedPatchSet.isDraft()
+        || updatedChange.getStatus() == Change.Status.NEW) {
+      CheckedFuture<?, IOException> indexFuture =
+          indexer.indexAsync(updatedChange.getId());
+      sender.send(rsrc.getNotes(), update,
+          rsrc.getChange().getStatus() == Change.Status.DRAFT,
+          rsrc.getUser(), updatedChange, updatedPatchSet,
+          rsrc.getControl().getLabelTypes());
+      indexFuture.checkedGet();
+      hooks.doDraftPublishedHook(updatedChange, updatedPatchSet,
+          dbProvider.get());
     }
 
     return Response.none();
@@ -154,11 +139,11 @@
   public static class CurrentRevision implements
       RestModifyView<ChangeResource, Input> {
     private final Provider<ReviewDb> dbProvider;
-    private final Publish publish;
+    private final PublishDraftPatchSet publish;
 
     @Inject
     CurrentRevision(Provider<ReviewDb> dbProvider,
-        Publish publish) {
+        PublishDraftPatchSet publish) {
       this.dbProvider = dbProvider;
       this.publish = publish;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
deleted file mode 100644
index c1fb304..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import com.google.gerrit.common.changes.Side;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.DefaultInput;
-import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.CommentRange;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.PutDraft.Input;
-import com.google.gerrit.server.util.TimeUtil;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-import java.sql.Timestamp;
-import java.util.Collections;
-
-@Singleton
-class PutDraft implements RestModifyView<DraftResource, Input> {
-  static class Input {
-    String id;
-    String path;
-    Side side;
-    Integer line;
-    String inReplyTo;
-    Timestamp updated; // Accepted but ignored.
-    CommentRange range;
-
-    @DefaultInput
-    String message;
-  }
-
-  private final Provider<ReviewDb> db;
-  private final DeleteDraft delete;
-
-  @Inject
-  PutDraft(Provider<ReviewDb> db, DeleteDraft delete) {
-    this.db = db;
-    this.delete = delete;
-  }
-
-  @Override
-  public Response<CommentInfo> apply(DraftResource rsrc, Input in) throws
-      BadRequestException, OrmException {
-    PatchLineComment c = rsrc.getComment();
-    if (in == null || in.message == null || in.message.trim().isEmpty()) {
-      return delete.apply(rsrc, null);
-    } else if (in.id != null && !rsrc.getId().equals(in.id)) {
-      throw new BadRequestException("id must match URL");
-    } else if (in.line != null && in.line < 0) {
-      throw new BadRequestException("line must be >= 0");
-    } else if (in.line != null && in.range != null && in.line != in.range.getEndLine()) {
-      throw new BadRequestException("range endLine must be on the same line as the comment");
-    }
-
-    if (in.path != null
-        && !in.path.equals(c.getKey().getParentKey().getFileName())) {
-      // Updating the path alters the primary key, which isn't possible.
-      // Delete then recreate the comment instead of an update.
-      db.get().patchComments().delete(Collections.singleton(c));
-      c = new PatchLineComment(
-          new PatchLineComment.Key(
-              new Patch.Key(rsrc.getPatchSet().getId(), in.path),
-              c.getKey().get()),
-          c.getLine(),
-          rsrc.getAuthorId(),
-          c.getParentUuid(), TimeUtil.nowTs());
-      db.get().patchComments().insert(Collections.singleton(update(c, in)));
-    } else {
-      db.get().patchComments().update(Collections.singleton(update(c, in)));
-    }
-    return Response.ok(new CommentInfo(c, null));
-  }
-
-  private PatchLineComment update(PatchLineComment e, Input in) {
-    if (in.side != null) {
-      e.setSide(in.side == Side.PARENT ? (short) 0 : (short) 1);
-    }
-    if (in.inReplyTo != null) {
-      e.setParentUuid(Url.decode(in.inReplyTo));
-    }
-    e.setMessage(in.message.trim());
-    if (in.range != null || in.line != null) {
-      e.setRange(in.range);
-      e.setLine(in.range != null ? in.range.getEndLine() : in.line);
-    }
-    e.setWrittenOn(TimeUtil.nowTs());
-    return e;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
new file mode 100644
index 0000000..2a0bcb3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
@@ -0,0 +1,124 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
+
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+import java.util.Collections;
+
+@Singleton
+public class PutDraftComment implements RestModifyView<DraftCommentResource, DraftInput> {
+
+  private final Provider<ReviewDb> db;
+  private final DeleteDraftComment delete;
+  private final PatchLineCommentsUtil plcUtil;
+  private final ChangeUpdate.Factory updateFactory;
+  private final CommentJson commentJson;
+  private final PatchListCache patchListCache;
+
+  @Inject
+  PutDraftComment(Provider<ReviewDb> db,
+      DeleteDraftComment delete,
+      PatchLineCommentsUtil plcUtil,
+      ChangeUpdate.Factory updateFactory,
+      CommentJson commentJson,
+      PatchListCache patchListCache) {
+    this.db = db;
+    this.delete = delete;
+    this.plcUtil = plcUtil;
+    this.updateFactory = updateFactory;
+    this.commentJson = commentJson;
+    this.patchListCache = patchListCache;
+  }
+
+  @Override
+  public Response<CommentInfo> apply(DraftCommentResource rsrc, DraftInput in) throws
+      BadRequestException, OrmException, IOException {
+    PatchLineComment c = rsrc.getComment();
+    ChangeUpdate update = updateFactory.create(rsrc.getControl());
+    if (in == null || in.message == null || in.message.trim().isEmpty()) {
+      return delete.apply(rsrc, null);
+    } else if (in.id != null && !rsrc.getId().equals(in.id)) {
+      throw new BadRequestException("id must match URL");
+    } else if (in.line != null && in.line < 0) {
+      throw new BadRequestException("line must be >= 0");
+    } else if (in.line != null && in.range != null && in.line != in.range.endLine) {
+      throw new BadRequestException("range endLine must be on the same line as the comment");
+    }
+
+    if (in.path != null
+        && !in.path.equals(c.getKey().getParentKey().getFileName())) {
+      // Updating the path alters the primary key, which isn't possible.
+      // Delete then recreate the comment instead of an update.
+
+      plcUtil.deleteComments(db.get(), update, Collections.singleton(c));
+      c = new PatchLineComment(
+          new PatchLineComment.Key(
+              new Patch.Key(rsrc.getPatchSet().getId(), in.path),
+              c.getKey().get()),
+          c.getLine(),
+          rsrc.getAuthorId(),
+          c.getParentUuid(), TimeUtil.nowTs());
+      setCommentRevId(c, patchListCache, rsrc.getChange(), rsrc.getPatchSet());
+      plcUtil.insertComments(db.get(), update,
+          Collections.singleton(update(c, in)));
+    } else {
+      if (c.getRevId() == null) {
+        setCommentRevId(c, patchListCache, rsrc.getChange(),
+            rsrc.getPatchSet());
+      }
+      plcUtil.updateComments(db.get(), update,
+          Collections.singleton(update(c, in)));
+    }
+    update.commit();
+    return Response.ok(commentJson.format(c, false));
+  }
+
+  private PatchLineComment update(PatchLineComment e, DraftInput in) {
+    if (in.side != null) {
+      e.setSide(in.side == Side.PARENT ? (short) 0 : (short) 1);
+    }
+    if (in.inReplyTo != null) {
+      e.setParentUuid(Url.decode(in.inReplyTo));
+    }
+    e.setMessage(in.message.trim());
+    if (in.range != null || in.line != null) {
+      e.setRange(in.range);
+      e.setLine(in.range != null ? in.range.endLine : in.line);
+    }
+    e.setWrittenOn(TimeUtil.nowTs());
+    return e;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
index 7f7c5fb..d171785 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.Response;
@@ -31,7 +32,6 @@
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -41,7 +41,7 @@
 import java.io.IOException;
 
 @Singleton
-class PutTopic implements RestModifyView<ChangeResource, Input>,
+public class PutTopic implements RestModifyView<ChangeResource, Input>,
     UiAction<ChangeResource> {
   private final Provider<ReviewDb> dbProvider;
   private final ChangeIndexer indexer;
@@ -49,9 +49,9 @@
   private final ChangeUpdate.Factory updateFactory;
   private final ChangeMessagesUtil cmUtil;
 
-  static class Input {
+  public static class Input {
     @DefaultInput
-    String topic;
+    public String topic;
   }
 
   @Inject
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
index 295189b..d2afd9a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
@@ -15,17 +15,20 @@
 package com.google.gerrit.server.change;
 
 import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.api.changes.RebaseInput;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetAncestor;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
-import com.google.gerrit.server.change.Rebase.Input;
 import com.google.gerrit.server.changedetail.RebaseChange;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
@@ -35,27 +38,36 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.io.IOException;
 
+import java.util.ArrayList;
+
 @Singleton
-public class Rebase implements RestModifyView<RevisionResource, Input>,
+public class Rebase implements RestModifyView<RevisionResource, RebaseInput>,
     UiAction<RevisionResource> {
-  public static class Input {
-  }
+
+  private static final Logger log =
+      LoggerFactory.getLogger(Rebase.class);
 
   private final Provider<RebaseChange> rebaseChange;
   private final ChangeJson json;
+  private final Provider<ReviewDb> dbProvider;
 
   @Inject
-  public Rebase(Provider<RebaseChange> rebaseChange, ChangeJson json) {
+  public Rebase(Provider<RebaseChange> rebaseChange, ChangeJson json,
+      Provider<ReviewDb> dbProvider) {
     this.rebaseChange = rebaseChange;
     this.json = json
         .addOption(ListChangesOption.CURRENT_REVISION)
         .addOption(ListChangesOption.CURRENT_COMMIT);
+    this.dbProvider = dbProvider;
   }
 
   @Override
-  public ChangeInfo apply(RevisionResource rsrc, Input input)
+  public ChangeInfo apply(RevisionResource rsrc, RebaseInput input)
       throws AuthException, ResourceNotFoundException,
       ResourceConflictException, EmailException, OrmException {
     ChangeControl control = rsrc.getControl();
@@ -65,11 +77,56 @@
     } else if (!change.getStatus().isOpen()) {
       throw new ResourceConflictException("change is "
           + change.getStatus().name().toLowerCase());
+    } else if (!hasOneParent(rsrc.getPatchSet().getId())) {
+      throw new ResourceConflictException(
+          "cannot rebase merge commits or commit with no ancestor");
+    }
+
+    String baseRev = null;
+    if (input != null && input.base != null) {
+      String base = input.base.trim();
+      do {
+        if (base.equals("")) {
+          // remove existing dependency to other patch set
+          baseRev = change.getDest().get();
+          break;
+        }
+
+        ReviewDb db = dbProvider.get();
+        PatchSet basePatchSet = parseBase(base);
+        if (basePatchSet == null) {
+          throw new ResourceConflictException("base revision is missing: " + base);
+        } else if (!rsrc.getControl().isPatchVisible(basePatchSet, db)) {
+          throw new AuthException("base revision not accessible: " + base);
+        } else if (change.getId().equals(basePatchSet.getId().getParentKey())) {
+          throw new ResourceConflictException("cannot depend on self");
+        }
+
+        Change baseChange = db.changes().get(basePatchSet.getId().getParentKey());
+        if (baseChange != null) {
+          if (!baseChange.getProject().equals(change.getProject())) {
+            throw new ResourceConflictException("base change is in wrong project: "
+                                                + baseChange.getProject());
+          } else if (!baseChange.getDest().equals(change.getDest())) {
+            throw new ResourceConflictException("base change is targetting wrong branch: "
+                                                + baseChange.getDest());
+          } else if (baseChange.getStatus() == Status.ABANDONED) {
+            throw new ResourceConflictException("base change is abandoned: "
+                                                + baseChange.getKey());
+          } else if (isDescendantOf(baseChange.getId(), rsrc.getPatchSet().getRevision())) {
+            throw new ResourceConflictException("base change " + baseChange.getKey()
+                                                + " is a descendant of the current "
+                                                + " change - recursion not allowed");
+          }
+          baseRev = basePatchSet.getRevision().get();
+          break;
+        }
+      } while (false);  // just wanted to use the break statement
     }
 
     try {
-      rebaseChange.get().rebase(rsrc.getChange(), rsrc.getPatchSet().getId(),
-          rsrc.getUser());
+      rebaseChange.get().rebase(change, rsrc.getPatchSet().getId(),
+          rsrc.getUser(), baseRev);
     } catch (InvalidChangeOperationException e) {
       throw new ResourceConflictException(e.getMessage());
     } catch (IOException e) {
@@ -81,37 +138,115 @@
     return json.format(change.getId());
   }
 
+  private boolean isDescendantOf(Change.Id child, RevId ancestor)
+      throws OrmException {
+    ReviewDb db = dbProvider.get();
+
+    ArrayList<RevId> parents = new ArrayList<>();
+    parents.add(ancestor);
+    while (!parents.isEmpty()) {
+      RevId parent = parents.remove(0);
+      // get direct descendants of change
+      for (PatchSetAncestor desc : db.patchSetAncestors().descendantsOf(parent)) {
+        PatchSet descPatchSet = db.patchSets().get(desc.getPatchSet());
+        Change.Id descChangeId = descPatchSet.getId().getParentKey();
+        if (child.equals(descChangeId)) {
+          PatchSet.Id descCurrentPatchSetId =
+              db.changes().get(descChangeId).currentPatchSetId();
+          // it's only bad if the descendant patch set is current
+          return descPatchSet.getId().equals(descCurrentPatchSetId);
+        } else {
+          // process indirect descendants as well
+          parents.add(descPatchSet.getRevision());
+        }
+      }
+    }
+
+    return false;
+  }
+
+  private PatchSet parseBase(final String base) throws OrmException {
+    ReviewDb db = dbProvider.get();
+
+    PatchSet.Id basePatchSetId = PatchSet.Id.fromRef(base);
+    if (basePatchSetId != null) {
+      // try parsing the base as a ref string
+      return db.patchSets().get(basePatchSetId);
+    }
+
+    // try parsing base as a change number (assume current patch set)
+    PatchSet basePatchSet = null;
+    try {
+      Change.Id baseChangeId = Change.Id.parse(base);
+      if (baseChangeId != null) {
+        for (PatchSet ps : db.patchSets().byChange(baseChangeId)) {
+          if (basePatchSet == null || basePatchSet.getId().get() < ps.getId().get()){
+            basePatchSet = ps;
+          }
+        }
+      }
+    } catch (NumberFormatException e) {  // probably a SHA1
+    }
+
+    // try parsing as SHA1
+    if (basePatchSet == null) {
+      for (PatchSet ps : db.patchSets().byRevision(new RevId(base))) {
+        if (basePatchSet == null || basePatchSet.getId().get() < ps.getId().get()) {
+          basePatchSet = ps;
+        }
+      }
+    }
+
+    return basePatchSet;
+  }
+
+  private boolean hasOneParent(final PatchSet.Id patchSetId) {
+    try {
+      // prevent rebase of exotic changes (merge commit, no ancestor).
+      return (dbProvider.get().patchSetAncestors()
+          .ancestorsOf(patchSetId).toList().size() == 1);
+    } catch (OrmException e) {
+      log.error("Failed to get ancestors of patch set "
+          + patchSetId.toRefName(), e);
+      return false;
+    }
+  }
+
   @Override
   public UiAction.Description getDescription(RevisionResource resource) {
-    return new UiAction.Description()
+    UiAction.Description descr = new UiAction.Description()
       .setLabel("Rebase")
       .setTitle("Rebase onto tip of branch or parent change")
       .setVisible(resource.getChange().getStatus().isOpen()
           && resource.getControl().canRebase()
-          && rebaseChange.get().canRebase(resource));
+          && hasOneParent(resource.getPatchSet().getId()));
+    if (descr.isVisible()) {
+      // Disable the rebase button in the RebaseDialog if
+      // the change cannot be rebased.
+      descr.setEnabled(rebaseChange.get().canRebase(resource));
+    }
+    return descr;
   }
 
   public static class CurrentRevision implements
-      RestModifyView<ChangeResource, Input> {
-    private final Provider<ReviewDb> dbProvider;
+      RestModifyView<ChangeResource, RebaseInput> {
     private final Rebase rebase;
 
     @Inject
-    CurrentRevision(Provider<ReviewDb> dbProvider, Rebase rebase) {
-      this.dbProvider = dbProvider;
+    CurrentRevision(Rebase rebase) {
       this.rebase = rebase;
     }
 
     @Override
-    public ChangeInfo apply(ChangeResource rsrc, Input input)
+    public ChangeInfo apply(ChangeResource rsrc, RebaseInput input)
         throws AuthException, ResourceNotFoundException,
         ResourceConflictException, EmailException, OrmException {
       PatchSet ps =
-          dbProvider.get().patchSets()
+          rebase.dbProvider.get().patchSets()
               .get(rsrc.getChange().currentPatchSetId());
       if (ps == null) {
         throw new ResourceConflictException("current revision is missing");
-      } else if (!rsrc.getControl().isPatchVisible(ps, dbProvider.get())) {
+      } else if (!rsrc.getControl().isPatchVisible(ps, rebase.dbProvider.get())) {
         throw new AuthException("current revision not accessible");
       }
       return rebase.apply(new RevisionResource(rsrc, ps), input);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java
new file mode 100644
index 0000000..1420b6c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Optional;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsPost;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.edit.ChangeEdit;
+import com.google.gerrit.server.edit.ChangeEditModifier;
+import com.google.gerrit.server.edit.ChangeEditUtil;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+
+@Singleton
+public class RebaseChangeEdit implements
+    ChildCollection<ChangeResource, ChangeEditResource>,
+    AcceptsPost<ChangeResource> {
+
+  private final Rebase rebase;
+
+  @Inject
+  RebaseChangeEdit(Rebase rebase) {
+    this.rebase = rebase;
+  }
+
+  @Override
+  public DynamicMap<RestView<ChangeEditResource>> views() {
+    throw new NotImplementedException();
+  }
+
+  @Override
+  public RestView<ChangeResource> list() {
+    throw new NotImplementedException();
+  }
+
+  @Override
+  public ChangeEditResource parse(ChangeResource parent, IdString id) {
+    throw new NotImplementedException();
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public Rebase post(ChangeResource parent) throws RestApiException {
+    return rebase;
+  }
+
+  @Singleton
+  public static class Rebase implements RestModifyView<ChangeResource, Rebase.Input> {
+    public static class Input {
+    }
+
+    private final ChangeEditModifier editModifier;
+    private final ChangeEditUtil editUtil;
+    private final Provider<ReviewDb> db;
+
+    @Inject
+    Rebase(ChangeEditModifier editModifier,
+        ChangeEditUtil editUtil,
+        Provider<ReviewDb> db) {
+      this.editModifier = editModifier;
+      this.editUtil = editUtil;
+      this.db = db;
+    }
+
+    @Override
+    public Response<?> apply(ChangeResource rsrc, Rebase.Input in)
+        throws AuthException, ResourceConflictException, IOException,
+        InvalidChangeOperationException, OrmException {
+      Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange());
+      if (!edit.isPresent()) {
+        throw new ResourceConflictException(String.format(
+            "no edit exists for change %s",
+            rsrc.getChange().getChangeId()));
+      }
+
+      PatchSet current = db.get().patchSets().get(
+          rsrc.getChange().currentPatchSetId());
+      if (current.getId().equals(edit.get().getBasePatchSet().getId())) {
+        throw new ResourceConflictException(String.format(
+            "edit for change %s is already on latest patch set: %s",
+            rsrc.getChange().getChangeId(),
+            current.getId()));
+      }
+      editModifier.rebaseEdit(edit.get(), current);
+      return Response.none();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
index 88e2137..cce362a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
@@ -18,6 +18,7 @@
 import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.extensions.api.changes.RestoreInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -29,7 +30,7 @@
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.ReplyToChangeSender;
 import com.google.gerrit.server.mail.RestoredSender;
 import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -54,7 +55,7 @@
   private final RestoredSender.Factory restoredSenderFactory;
   private final Provider<ReviewDb> dbProvider;
   private final ChangeJson json;
-  private final MergeabilityChecker mergeabilityChecker;
+  private final ChangeIndexer indexer;
   private final ChangeMessagesUtil cmUtil;
   private final ChangeUpdate.Factory updateFactory;
 
@@ -63,14 +64,14 @@
       RestoredSender.Factory restoredSenderFactory,
       Provider<ReviewDb> dbProvider,
       ChangeJson json,
-      MergeabilityChecker mergeabilityChecker,
+      ChangeIndexer indexer,
       ChangeMessagesUtil cmUtil,
       ChangeUpdate.Factory updateFactory) {
     this.hooks = hooks;
     this.restoredSenderFactory = restoredSenderFactory;
     this.dbProvider = dbProvider;
     this.json = json;
-    this.mergeabilityChecker = mergeabilityChecker;
+    this.indexer = indexer;
     this.cmUtil = cmUtil;
     this.updateFactory = updateFactory;
   }
@@ -121,10 +122,7 @@
     }
     update.commit();
 
-    CheckedFuture<?, IOException> f = mergeabilityChecker.newCheck()
-        .addChange(change)
-        .reindex()
-        .runAsync();
+    CheckedFuture<?, IOException> f = indexer.indexAsync(change.getId());
 
     try {
       ReplyToChangeSender cm = restoredSenderFactory.create(change);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index 326c872..e6846e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -15,8 +15,10 @@
 package com.google.gerrit.server.change;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.RevertInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -27,12 +29,10 @@
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.ssh.NoSshInfo;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
index 4612c96..65b390a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -23,14 +23,15 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
-import com.google.gerrit.server.account.AccountInfo;
-import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -47,13 +48,13 @@
   private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
   private final ApprovalsUtil approvalsUtil;
-  private final AccountInfo.Loader.Factory accountLoaderFactory;
+  private final AccountLoader.Factory accountLoaderFactory;
 
   @Inject
   ReviewerJson(Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       ApprovalsUtil approvalsUtil,
-      AccountInfo.Loader.Factory accountLoaderFactory) {
+      AccountLoader.Factory accountLoaderFactory) {
     this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.approvalsUtil = approvalsUtil;
@@ -63,12 +64,11 @@
   public List<ReviewerInfo> format(Collection<ReviewerResource> rsrcs)
       throws OrmException {
     List<ReviewerInfo> infos = Lists.newArrayListWithCapacity(rsrcs.size());
-    AccountInfo.Loader loader = accountLoaderFactory.create(true);
+    AccountLoader loader = accountLoaderFactory.create(true);
     for (ReviewerResource rsrc : rsrcs) {
       ReviewerInfo info = format(new ReviewerInfo(
           rsrc.getUser().getAccountId()),
-          rsrc.getUserControl(),
-          rsrc.getNotes());
+          rsrc.getUserControl());
       loader.put(info);
       infos.add(info);
     }
@@ -80,11 +80,11 @@
     return format(ImmutableList.<ReviewerResource> of(rsrc));
   }
 
-  public ReviewerInfo format(ReviewerInfo out, ChangeControl ctl,
-      ChangeNotes changeNotes) throws OrmException {
+  public ReviewerInfo format(ReviewerInfo out, ChangeControl ctl) throws OrmException {
     PatchSet.Id psId = ctl.getChange().currentPatchSetId();
     return format(out, ctl,
-        approvalsUtil.byPatchSetUser(db.get(), ctl, psId, out._id));
+        approvalsUtil.byPatchSetUser(db.get(), ctl, psId,
+            new Account.Id(out._accountId)));
   }
 
   public ReviewerInfo format(ReviewerInfo out, ChangeControl ctl,
@@ -109,8 +109,11 @@
     ChangeData cd = changeDataFactory.create(db.get(), ctl);
     PatchSet ps = cd.currentPatchSet();
     if (ps != null) {
-      for (SubmitRecord rec :
-          ctl.canSubmit(db.get(), ps, cd, true, false, true)) {
+      for (SubmitRecord rec : new SubmitRuleEvaluator(cd)
+          .setPatchSet(ps)
+          .setFastEvalLabels(true)
+          .setAllowDraft(true)
+          .canSubmit()) {
         if (rec.labels == null) {
           continue;
         }
@@ -135,7 +138,7 @@
     Map<String, String> approvals;
 
     protected ReviewerInfo(Account.Id id) {
-      super(id);
+      super(id.get());
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
new file mode 100644
index 0000000..554d760
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Predicate;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+/**
+ * The suggest oracle may be called many times in rapid succession during the
+ * course of one operation.
+ * It would be easy to have a simple Cache<Boolean, List<Account>> with a short
+ * expiration time of 30s.
+ * Cache only has a single key we're just using Cache for the expiration behavior.
+ */
+@Singleton
+public class ReviewerSuggestionCache {
+  private static final Logger log = LoggerFactory
+      .getLogger(ReviewerSuggestionCache.class);
+  private final LoadingCache<Boolean, List<Account>> cache;
+
+  @Inject
+  ReviewerSuggestionCache(final Provider<ReviewDb> dbProvider) {
+    this.cache =
+        CacheBuilder.newBuilder().maximumSize(1)
+            .expireAfterWrite(30, TimeUnit.SECONDS)
+            .build(new CacheLoader<Boolean, List<Account>>() {
+              @Override
+              public List<Account> load(Boolean key) throws Exception {
+                return ImmutableList.copyOf(Iterables.filter(
+                    dbProvider.get().accounts().all(),
+                    new Predicate<Account>() {
+                      @Override
+                      public boolean apply(Account in) {
+                        return in.isActive();
+                      }
+                    }));
+              }
+            });
+  }
+
+  List<Account> get() {
+    try {
+      return cache.get(true);
+    } catch (ExecutionException e) {
+      log.warn("Cannot fetch reviewers from cache", e);
+      return Collections.emptyList();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
index eb9f4ea..e58d1a1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.base.Optional;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestResource.HasETag;
 import com.google.gerrit.extensions.restapi.RestView;
@@ -21,6 +22,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.inject.TypeLiteral;
@@ -31,11 +33,18 @@
 
   private final ChangeResource change;
   private final PatchSet ps;
+  private final Optional<ChangeEdit> edit;
   private boolean cacheable = true;
 
   public RevisionResource(ChangeResource change, PatchSet ps) {
+    this(change, ps, Optional.<ChangeEdit> absent());
+  }
+
+  public RevisionResource(ChangeResource change, PatchSet ps,
+      Optional<ChangeEdit> edit) {
     this.change = change;
     this.ps = ps;
+    this.edit = edit;
   }
 
   public boolean isCacheable() {
@@ -82,4 +91,17 @@
     cacheable = false;
     return this;
   }
+
+  Optional<ChangeEdit> getEdit() {
+    return edit;
+  }
+
+  @Override
+  public String toString() {
+    String s = ps.getId().toString();
+    if (edit.isPresent()) {
+      s = "edit:" + s;
+    }
+    return s;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
index 239aae2..bb5775b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
@@ -14,8 +14,14 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -24,11 +30,15 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.edit.ChangeEdit;
+import com.google.gerrit.server.edit.ChangeEditUtil;
 import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
 
@@ -36,12 +46,15 @@
 public class Revisions implements ChildCollection<ChangeResource, RevisionResource> {
   private final DynamicMap<RestView<RevisionResource>> views;
   private final Provider<ReviewDb> dbProvider;
+  private final ChangeEditUtil editUtil;
 
   @Inject
   Revisions(DynamicMap<RestView<RevisionResource>> views,
-      Provider<ReviewDb> dbProvider) {
+      Provider<ReviewDb> dbProvider,
+      ChangeEditUtil editUtil) {
     this.views = views;
     this.dbProvider = dbProvider;
+    this.editUtil = editUtil;
   }
 
   @Override
@@ -56,7 +69,8 @@
 
   @Override
   public RevisionResource parse(ChangeResource change, IdString id)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException, AuthException, OrmException,
+      IOException {
     if (id.equals("current")) {
       PatchSet.Id p = change.getChange().currentPatchSetId();
       PatchSet ps = p != null ? dbProvider.get().patchSets().get(p) : null;
@@ -65,17 +79,23 @@
       }
       throw new ResourceNotFoundException(id);
     }
-    List<PatchSet> match = Lists.newArrayListWithExpectedSize(2);
-    for (PatchSet ps : find(change, id.get())) {
-      Change.Id changeId = ps.getId().getParentKey();
-      if (changeId.equals(change.getChange().getId()) && visible(change, ps)) {
-        match.add(ps);
+
+    List<RevisionResource> match = Lists.newArrayListWithExpectedSize(2);
+    for (RevisionResource rsrc : find(change, id.get())) {
+      if (visible(change, rsrc.getPatchSet())) {
+        match.add(rsrc);
       }
     }
-    if (match.size() != 1) {
-      throw new ResourceNotFoundException(id);
+    switch (match.size()) {
+      case 0:
+        throw new ResourceNotFoundException(id);
+      case 1:
+        return match.get(0);
+      default:
+        throw new ResourceNotFoundException(
+            "Multiple patch sets for \"" + id.get() + "\": "
+            + Joiner.on("; ").join(match));
     }
-    return new RevisionResource(change, match.get(0));
   }
 
   private boolean visible(ChangeResource change, PatchSet ps)
@@ -83,19 +103,13 @@
     return change.getControl().isPatchVisible(ps, dbProvider.get());
   }
 
-  private List<PatchSet> find(ChangeResource change, String id)
-      throws OrmException {
-    ReviewDb db = dbProvider.get();
-
-    if (id.length() < 6 && id.matches("^[1-9][0-9]{0,4}$")) {
+  private List<RevisionResource> find(ChangeResource change, String id)
+      throws OrmException, IOException, AuthException {
+    if (id.equals("0")) {
+      return loadEdit(change, null);
+    } else if (id.length() < 6 && id.matches("^[1-9][0-9]{0,4}$")) {
       // Legacy patch set number syntax.
-      PatchSet ps = dbProvider.get().patchSets().get(new PatchSet.Id(
-          change.getChange().getId(),
-          Integer.parseInt(id)));
-      if (ps != null) {
-        return Collections.singletonList(ps);
-      }
-      return Collections.emptyList();
+      return byLegacyPatchSetId(change, id);
     } else if (id.length() < 4 || id.length() > RevId.LEN) {
       // Require a minimum of 4 digits.
       // Impossibly long identifier will never match.
@@ -107,19 +121,71 @@
       // for all patch sets in the change.
       RevId revid = new RevId(id);
       if (revid.isComplete()) {
-        return db.patchSets().byRevision(revid).toList();
-      } else {
-        return db.patchSets().byRevisionRange(revid, revid.max()).toList();
+        List<RevisionResource> m = toResources(change, findExactMatch(revid));
+        return m.isEmpty() ? loadEdit(change, revid)  : m;
       }
+      return toResources(change, findByPrefix(revid));
     } else {
       // Chance of collision rises; look at all patch sets on the change.
-      List<PatchSet> out = Lists.newArrayList();
-      for (PatchSet ps : db.patchSets().byChange(change.getChange().getId())) {
+      List<RevisionResource> out = Lists.newArrayList();
+      for (PatchSet ps : dbProvider.get().patchSets()
+          .byChange(change.getChange().getId())) {
         if (ps.getRevision() != null && ps.getRevision().get().startsWith(id)) {
-          out.add(ps);
+          out.add(new RevisionResource(change, ps));
         }
       }
       return out;
     }
   }
+
+  private List<RevisionResource> byLegacyPatchSetId(ChangeResource change,
+      String id) throws OrmException {
+    PatchSet ps = dbProvider.get().patchSets().get(new PatchSet.Id(
+        change.getChange().getId(),
+        Integer.parseInt(id)));
+    if (ps != null) {
+      return Collections.singletonList(new RevisionResource(change, ps));
+    }
+    return Collections.emptyList();
+  }
+
+  private ResultSet<PatchSet> findExactMatch(RevId revid) throws OrmException {
+    return dbProvider.get().patchSets().byRevision(revid);
+  }
+
+  private ResultSet<PatchSet> findByPrefix(RevId revid) throws OrmException {
+    return dbProvider.get().patchSets().byRevisionRange(revid, revid.max());
+  }
+
+  private List<RevisionResource> loadEdit(ChangeResource change, RevId revid)
+      throws AuthException, IOException {
+    Optional<ChangeEdit> edit = editUtil.byChange(change.getChange());
+    if (edit.isPresent()) {
+      PatchSet ps = new PatchSet(new PatchSet.Id(
+          change.getChange().getId(), 0));
+      ps.setRevision(edit.get().getRevision());
+      if (revid == null || edit.get().getRevision().equals(revid)) {
+        return Collections.singletonList(
+            new RevisionResource(change, ps, edit));
+      }
+    }
+    return Collections.emptyList();
+  }
+
+  private static List<RevisionResource> toResources(final ChangeResource change,
+      Iterable<PatchSet> patchSets) {
+    final Change.Id changeId = change.getChange().getId();
+    return FluentIterable.from(patchSets)
+        .filter(new Predicate<PatchSet>() {
+          @Override
+          public boolean apply(PatchSet in) {
+            return changeId.equals(in.getId().getParentKey());
+          }
+        }).transform(new Function<PatchSet, RevisionResource>() {
+          @Override
+          public RevisionResource apply(PatchSet in) {
+            return new RevisionResource(change, in);
+          }
+        }).toList();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index 3719028..41151ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -16,8 +16,9 @@
 
 import static com.google.gerrit.common.data.SubmitRecord.Status.OK;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
 import com.google.common.collect.FluentIterable;
@@ -27,9 +28,11 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Table;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -38,19 +41,17 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ProjectUtil;
 import com.google.gerrit.server.account.AccountsCollection;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.LabelNormalizer;
@@ -59,9 +60,12 @@
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.util.TimeUtil;
+import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -71,9 +75,13 @@
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -81,8 +89,16 @@
 @Singleton
 public class Submit implements RestModifyView<RevisionResource, SubmitInput>,
     UiAction<RevisionResource> {
+  private static final Logger log = LoggerFactory.getLogger(Submit.class);
+
   private static final String DEFAULT_TOOLTIP =
       "Submit patch set ${patchSet} into ${branch}";
+  private static final String DEFAULT_TOPIC_TOOLTIP =
+      "Submit all ${topicSize} changes of the same topic";
+  private static final String BLOCKED_TOPIC_TOOLTIP =
+      "Other changes in this topic are not ready";
+  private static final String BLOCKED_HIDDEN_TOPIC_TOOLTIP =
+      "Other hidden changes in this topic are not ready";
 
   public enum Status {
     SUBMITTED, MERGED
@@ -102,6 +118,7 @@
   private final Provider<ReviewDb> dbProvider;
   private final GitRepositoryManager repoManager;
   private final IdentifiedUser.GenericFactory userFactory;
+  private final ChangeData.Factory changeDataFactory;
   private final ChangeUpdate.Factory updateFactory;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeMessagesUtil cmUtil;
@@ -112,12 +129,17 @@
   private final ChangesCollection changes;
   private final String label;
   private final ParameterizedString titlePattern;
+  private final String submitTopicLabel;
+  private final ParameterizedString submitTopicTooltip;
+  private final boolean submitWholeTopic;
+  private final Provider<InternalChangeQuery> queryProvider;
 
   @Inject
   Submit(@GerritPersonIdent PersonIdent serverIdent,
       Provider<ReviewDb> dbProvider,
       GitRepositoryManager repoManager,
       IdentifiedUser.GenericFactory userFactory,
+      ChangeData.Factory changeDataFactory,
       ChangeUpdate.Factory updateFactory,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
@@ -126,11 +148,13 @@
       ChangesCollection changes,
       ChangeIndexer indexer,
       LabelNormalizer labelNormalizer,
-      @GerritServerConfig Config cfg) {
+      @GerritServerConfig Config cfg,
+      Provider<InternalChangeQuery> queryProvider) {
     this.serverIdent = serverIdent;
     this.dbProvider = dbProvider;
     this.repoManager = repoManager;
     this.userFactory = userFactory;
+    this.changeDataFactory = changeDataFactory;
     this.updateFactory = updateFactory;
     this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
@@ -139,12 +163,20 @@
     this.changes = changes;
     this.indexer = indexer;
     this.labelNormalizer = labelNormalizer;
-    this.label = Objects.firstNonNull(
+    this.label = MoreObjects.firstNonNull(
         Strings.emptyToNull(cfg.getString("change", null, "submitLabel")),
         "Submit");
-    this.titlePattern = new ParameterizedString(Objects.firstNonNull(
+    this.titlePattern = new ParameterizedString(MoreObjects.firstNonNull(
         cfg.getString("change", null, "submitTooltip"),
         DEFAULT_TOOLTIP));
+    submitWholeTopic = cfg.getBoolean("change", null, "submitWholeTopic" , false);
+    this.submitTopicLabel = MoreObjects.firstNonNull(
+        Strings.emptyToNull(cfg.getString("change", null, "submitTopicLabel")),
+        "Submit whole topic");
+    this.submitTopicTooltip = new ParameterizedString(MoreObjects.firstNonNull(
+        cfg.getString("change", null, "submitTopicTooltip"),
+        DEFAULT_TOPIC_TOOLTIP));
+    this.queryProvider = queryProvider;
   }
 
   @Override
@@ -200,27 +232,98 @@
         if (msg != null) {
           throw new ResourceConflictException(msg.getMessage());
         }
+        //$FALL-THROUGH$
       default:
         throw new ResourceConflictException("change is " + status(change));
     }
   }
 
+  /**
+   * @param changes list of changes to be submitted at once
+   * @param identifiedUser the user who is checking to submit
+   * @return a reason why any of the changes is not submittable or null
+   */
+  private String problemsForSubmittingChanges(List<ChangeData> changes,
+      IdentifiedUser identifiedUser) {
+    for (ChangeData c : changes) {
+      try {
+        ChangeControl changeControl = c.changeControl().forUser(
+            identifiedUser);
+        if (!changeControl.isVisible(dbProvider.get())) {
+          return BLOCKED_HIDDEN_TOPIC_TOOLTIP;
+        }
+        if (!changeControl.canSubmit()) {
+          return BLOCKED_TOPIC_TOOLTIP;
+        }
+        checkSubmitRule(c, c.currentPatchSet(), false);
+      } catch (OrmException e) {
+        log.error("Error checking if change is submittable", e);
+        throw new OrmRuntimeException(e);
+      } catch (ResourceConflictException e) {
+        return BLOCKED_TOPIC_TOOLTIP;
+      }
+    }
+    return null;
+  }
+
   @Override
   public UiAction.Description getDescription(RevisionResource resource) {
     PatchSet.Id current = resource.getChange().currentPatchSetId();
-    RevId revId = resource.getPatchSet().getRevision();
-    Map<String, String> params = ImmutableMap.of(
-        "patchSet", String.valueOf(resource.getPatchSet().getPatchSetId()),
-        "branch", resource.getChange().getDest().getShortName(),
-        "commit", ObjectId.fromString(revId.get()).abbreviate(7).name());
+    String topic = resource.getChange().getTopic();
+    boolean visible = !resource.getPatchSet().isDraft()
+        && resource.getChange().getStatus().isOpen()
+        && resource.getPatchSet().getId().equals(current)
+        && resource.getControl().canSubmit();
 
-    return new UiAction.Description()
-      .setLabel(label)
-      .setTitle(Strings.emptyToNull(titlePattern.replace(params)))
-      .setVisible(!resource.getPatchSet().isDraft()
-          && resource.getChange().getStatus().isOpen()
-          && resource.getPatchSet().getId().equals(current)
-          && resource.getControl().canSubmit());
+    ReviewDb db = dbProvider.get();
+    ChangeData cd = changeDataFactory.create(db, resource.getControl());
+    if (problemsForSubmittingChanges(Arrays.asList(cd), resource.getUser())
+        != null) {
+      visible = false;
+    }
+
+    if (!visible) {
+      return new UiAction.Description()
+        .setLabel("")
+        .setTitle("")
+        .setVisible(false);
+    }
+    if (submitWholeTopic && !Strings.isNullOrEmpty(topic)) {
+      List<ChangeData> changesByTopic = null;
+      try {
+        changesByTopic = queryProvider.get().byTopicOpen(topic);
+      } catch (OrmException e) {
+        throw new OrmRuntimeException(e);
+      }
+      Map<String, String> params = ImmutableMap.of(
+          "topicSize", String.valueOf(changesByTopic.size()));
+      String topicProblems = problemsForSubmittingChanges(changesByTopic,
+          resource.getUser());
+      if (topicProblems != null) {
+        return new UiAction.Description()
+          .setLabel(submitTopicLabel)
+          .setTitle(topicProblems)
+          .setVisible(true)
+          .setEnabled(false);
+      } else {
+        return new UiAction.Description()
+          .setLabel(submitTopicLabel)
+          .setTitle(Strings.emptyToNull(
+              submitTopicTooltip.replace(params)))
+          .setVisible(true)
+          .setEnabled(true);
+      }
+    } else {
+      RevId revId = resource.getPatchSet().getRevision();
+      Map<String, String> params = ImmutableMap.of(
+          "patchSet", String.valueOf(resource.getPatchSet().getPatchSetId()),
+          "branch", resource.getChange().getDest().getShortName(),
+          "commit", ObjectId.fromString(revId.get()).abbreviate(7).name());
+      return new UiAction.Description()
+        .setLabel(label)
+        .setTitle(Strings.emptyToNull(titlePattern.replace(params)))
+        .setVisible(true);
+    }
   }
 
   /**
@@ -242,36 +345,41 @@
         .orNull();
   }
 
-  public Change submit(RevisionResource rsrc, IdentifiedUser caller,
+  private Change submitToDatabase(ReviewDb db, Change.Id changeId,
+      final Timestamp timestamp) throws OrmException {
+    return db.changes().atomicUpdate(changeId,
+      new AtomicUpdate<Change>() {
+        @Override
+        public Change update(Change change) {
+          if (change.getStatus().isOpen()) {
+            change.setStatus(Change.Status.SUBMITTED);
+            change.setLastUpdatedOn(timestamp);
+            return change;
+          }
+          return null;
+        }
+      });
+  }
+
+  private Change submitThisChange(RevisionResource rsrc, IdentifiedUser caller,
       boolean force) throws ResourceConflictException, OrmException,
       IOException {
-    List<SubmitRecord> submitRecords = checkSubmitRule(rsrc, force);
+    ReviewDb db = dbProvider.get();
+    ChangeData cd = changeDataFactory.create(db, rsrc.getControl());
+    List<SubmitRecord> submitRecords = checkSubmitRule(cd,
+        rsrc.getPatchSet(), force);
+
     final Timestamp timestamp = TimeUtil.nowTs();
     Change change = rsrc.getChange();
     ChangeUpdate update = updateFactory.create(rsrc.getControl(), timestamp);
     update.submit(submitRecords);
 
-    ReviewDb db = dbProvider.get();
     db.changes().beginTransaction(change.getId());
     try {
       BatchMetaDataUpdate batch = approve(rsrc, update, caller, timestamp);
       // Write update commit after all normalized label commits.
       batch.write(update, new CommitBuilder());
-
-      change = db.changes().atomicUpdate(
-        change.getId(),
-        new AtomicUpdate<Change>() {
-          @Override
-          public Change update(Change change) {
-            if (change.getStatus().isOpen()) {
-              change.setStatus(Change.Status.SUBMITTED);
-              change.setLastUpdatedOn(timestamp);
-              ChangeUtil.computeSortKey(change);
-              return change;
-            }
-            return null;
-          }
-        });
+      change = submitToDatabase(db, change.getId(), timestamp);
       if (change == null) {
         return null;
       }
@@ -283,6 +391,62 @@
     return change;
   }
 
+  private Change submitWholeTopic(RevisionResource rsrc, IdentifiedUser caller,
+      boolean force, String topic) throws ResourceConflictException, OrmException,
+      IOException {
+    Preconditions.checkNotNull(topic);
+    final Timestamp timestamp = TimeUtil.nowTs();
+
+    ReviewDb db = dbProvider.get();
+    ChangeData cd = changeDataFactory.create(db, rsrc.getControl());
+
+    List<ChangeData> changesByTopic = queryProvider.get().byTopicOpen(topic);
+    String problems = problemsForSubmittingChanges(changesByTopic, caller);
+    if (problems != null) {
+      throw new ResourceConflictException(problems);
+    }
+
+    Change change = rsrc.getChange();
+    ChangeUpdate update = updateFactory.create(rsrc.getControl(), timestamp);
+
+    List<SubmitRecord> submitRecords = checkSubmitRule(cd,
+        rsrc.getPatchSet(), force);
+    update.submit(submitRecords);
+
+    db.changes().beginTransaction(change.getId());
+    try {
+      BatchMetaDataUpdate batch = approve(rsrc, update, caller, timestamp);
+      // Write update commit after all normalized label commits.
+      batch.write(update, new CommitBuilder());
+
+      for (ChangeData c : changesByTopic) {
+        if (submitToDatabase(db, c.getId(), timestamp) == null) {
+          return null;
+        }
+      }
+      db.commit();
+    } finally {
+      db.rollback();
+    }
+    List<Change.Id> ids = new ArrayList<>(changesByTopic.size());
+    for (ChangeData c : changesByTopic) {
+      ids.add(c.getId());
+    }
+    indexer.indexAsync(ids).checkedGet();
+    return change;
+  }
+
+  public Change submit(RevisionResource rsrc, IdentifiedUser caller,
+      boolean force) throws ResourceConflictException, OrmException,
+      IOException {
+    String topic = rsrc.getChange().getTopic();
+    if (submitWholeTopic && !Strings.isNullOrEmpty(topic)) {
+      return submitWholeTopic(rsrc, caller, force, topic);
+    } else {
+      return submitThisChange(rsrc, caller, force);
+    }
+  }
+
   private BatchMetaDataUpdate approve(RevisionResource rsrc,
       ChangeUpdate update, IdentifiedUser caller, Timestamp timestamp)
       throws OrmException {
@@ -296,7 +460,8 @@
     }
 
     PatchSetApproval submit = ApprovalsUtil.getSubmitter(psId, byKey.values());
-    if (submit == null || submit.getAccountId() != caller.getAccountId()) {
+    if (submit == null
+        || !submit.getAccountId().equals(caller.getAccountId())) {
       submit = new PatchSetApproval(
           new PatchSetApproval.Key(
               rsrc.getPatchSet().getId(),
@@ -321,7 +486,7 @@
     update.putApproval(submit.getLabel(), submit.getValue());
 
     dbProvider.get().patchSetApprovals().upsert(normalized.getNormalized());
-    dbProvider.get().patchSetApprovals().delete(normalized.getDeleted());
+    dbProvider.get().patchSetApprovals().delete(normalized.deleted());
 
     try {
       return saveToBatch(rsrc, update, normalized, timestamp);
@@ -334,11 +499,11 @@
       ChangeUpdate callerUpdate, LabelNormalizer.Result normalized,
       Timestamp timestamp) throws IOException {
     Table<Account.Id, String, Optional<Short>> byUser = HashBasedTable.create();
-    for (PatchSetApproval psa : normalized.getUpdated()) {
+    for (PatchSetApproval psa : normalized.updated()) {
       byUser.put(psa.getAccountId(), psa.getLabel(),
           Optional.of(psa.getValue()));
     }
-    for (PatchSetApproval psa : normalized.getDeleted()) {
+    for (PatchSetApproval psa : normalized.deleted()) {
       byUser.put(psa.getAccountId(), psa.getLabel(), Optional.<Short> absent());
     }
 
@@ -373,11 +538,12 @@
     }
   }
 
-  private List<SubmitRecord> checkSubmitRule(RevisionResource rsrc,
-      boolean force) throws ResourceConflictException {
-    List<SubmitRecord> results = rsrc.getControl().canSubmit(
-        dbProvider.get(),
-        rsrc.getPatchSet());
+  private List<SubmitRecord> checkSubmitRule(ChangeData cd,
+      PatchSet patchSet, boolean force)
+          throws ResourceConflictException, OrmException {
+    List<SubmitRecord> results = new SubmitRuleEvaluator(cd)
+        .setPatchSet(patchSet)
+        .canSubmit();
     Optional<SubmitRecord> ok = findOkRecord(results);
     if (ok.isPresent()) {
       // Rules supplied a valid solution.
@@ -387,8 +553,8 @@
     } else if (results.isEmpty()) {
       throw new IllegalStateException(String.format(
           "ChangeControl.canSubmit returned empty list for %s in %s",
-          rsrc.getPatchSet().getId(),
-          rsrc.getChange().getProject().get()));
+          patchSet.getId(),
+          cd.change().getProject().get()));
     }
 
     for (SubmitRecord record : results) {
@@ -410,17 +576,23 @@
                 continue;
 
               case REJECT:
-                if (msg.length() > 0) msg.append("; ");
+                if (msg.length() > 0) {
+                  msg.append("; ");
+                }
                 msg.append("blocked by ").append(lbl.label);
                 continue;
 
               case NEED:
-                if (msg.length() > 0) msg.append("; ");
+                if (msg.length() > 0) {
+                  msg.append("; ");
+                }
                 msg.append("needs ").append(lbl.label);
                 continue;
 
               case IMPOSSIBLE:
-                if (msg.length() > 0) msg.append("; ");
+                if (msg.length() > 0) {
+                  msg.append("; ");
+                }
                 msg.append("needs ").append(lbl.label)
                    .append(" (check project access)");
                 continue;
@@ -429,8 +601,8 @@
                 throw new IllegalStateException(String.format(
                     "Unsupported SubmitRecord.Label %s for %s in %s",
                     lbl.toString(),
-                    rsrc.getPatchSet().getId(),
-                    rsrc.getChange().getProject().get()));
+                    patchSet.getId(),
+                    cd.change().getProject().get()));
             }
           }
           throw new ResourceConflictException(msg.toString());
@@ -439,8 +611,8 @@
           throw new IllegalStateException(String.format(
               "Unsupported SubmitRecord %s for %s in %s",
               record,
-              rsrc.getPatchSet().getId(),
-              rsrc.getChange().getProject().get()));
+              patchSet.getId().getId(),
+              cd.change().getProject().get()));
       }
     }
     throw new IllegalStateException();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
index b95d664..2c82f58 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
@@ -14,13 +14,18 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.base.Objects;
+import com.google.common.base.Function;
+import com.google.common.base.MoreObjects;
 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.common.collect.Ordering;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.GroupBaseInfo;
+import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.Url;
@@ -32,12 +37,11 @@
 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.AccountLoader;
 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;
@@ -48,19 +52,35 @@
 import org.kohsuke.args4j.Option;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 public class SuggestReviewers implements RestReadView<ChangeResource> {
-
   private static final String MAX_SUFFIX = "\u9fa5";
-  private static final int MAX = 10;
+  private static final int DEFAULT_MAX_SUGGESTED = 10;
+  private static final int DEFAULT_MAX_MATCHES = 100;
+  private static final Ordering<SuggestedReviewerInfo> ORDERING =
+      Ordering.natural().onResultOf(new Function<SuggestedReviewerInfo, String>() {
+        @Nullable
+        @Override
+        public String apply(@Nullable SuggestedReviewerInfo suggestedReviewerInfo) {
+          if (suggestedReviewerInfo == null) {
+            return null;
+          }
+          return suggestedReviewerInfo.account != null
+              ? MoreObjects.firstNonNull(suggestedReviewerInfo.account.email,
+              Strings.nullToEmpty(suggestedReviewerInfo.account.name))
+              : Strings.nullToEmpty(suggestedReviewerInfo.group.name);
+        }
+      });
 
-  private final AccountInfo.Loader.Factory accountLoaderFactory;
-  private final AccountControl.Factory accountControlFactory;
+  private final AccountLoader accountLoader;
+  private final AccountControl accountControl;
   private final GroupMembers.Factory groupMembersFactory;
   private final AccountCache accountCache;
   private final Provider<ReviewDb> dbProvider;
@@ -72,11 +92,17 @@
   private final int maxAllowed;
   private int limit;
   private String query;
+  private boolean useFullTextSearch;
+  private final int fullTextMaxMatches;
+  private final int maxSuggestedReviewers;
+  private final ReviewerSuggestionCache reviewerSuggestionCache;
 
   @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);
+    this.limit =
+        l <= 0 ? maxSuggestedReviewers : Math.min(l,
+            maxSuggestedReviewers);
   }
 
   @Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY",
@@ -87,7 +113,7 @@
 
   @Inject
   SuggestReviewers(AccountVisibility av,
-      AccountInfo.Loader.Factory accountLoaderFactory,
+      AccountLoader.Factory accountLoaderFactory,
       AccountControl.Factory accountControlFactory,
       AccountCache accountCache,
       GroupMembers.Factory groupMembersFactory,
@@ -95,21 +121,29 @@
       Provider<CurrentUser> currentUser,
       Provider<ReviewDb> dbProvider,
       @GerritServerConfig Config cfg,
-      GroupBackend groupBackend) {
-    this.accountLoaderFactory = accountLoaderFactory;
-    this.accountControlFactory = accountControlFactory;
+      GroupBackend groupBackend,
+      ReviewerSuggestionCache reviewerSuggestionCache) {
+    this.accountLoader = accountLoaderFactory.create(true);
+    this.accountControl = accountControlFactory.get();
     this.accountCache = accountCache;
     this.groupMembersFactory = groupMembersFactory;
     this.dbProvider = dbProvider;
     this.identifiedUserFactory = identifiedUserFactory;
     this.currentUser = currentUser;
     this.groupBackend = groupBackend;
-
+    this.reviewerSuggestionCache = reviewerSuggestionCache;
+    this.maxSuggestedReviewers =
+        cfg.getInt("suggest", "maxSuggestedReviewers", DEFAULT_MAX_SUGGESTED);
+    this.limit = this.maxSuggestedReviewers;
+    this.fullTextMaxMatches =
+        cfg.getInt("suggest", "fullTextSearchMaxMatches",
+            DEFAULT_MAX_MATCHES);
     String suggest = cfg.getString("suggest", null, "accounts");
     if ("OFF".equalsIgnoreCase(suggest)
         || "false".equalsIgnoreCase(suggest)) {
       this.suggestAccounts = false;
     } else {
+      this.useFullTextSearch = cfg.getBoolean("suggest", "fullTextSearch", false);
       this.suggestAccounts = (av != AccountVisibility.NONE);
     }
 
@@ -119,7 +153,7 @@
   }
 
   private interface VisibilityControl {
-    boolean isVisibleTo(Account account) throws OrmException;
+    boolean isVisibleTo(Account.Id account) throws OrmException;
   }
 
   @Override
@@ -134,12 +168,18 @@
     }
 
     VisibilityControl visibilityControl = getVisibility(rsrc);
-    List<AccountInfo> suggestedAccounts = suggestAccount(visibilityControl);
-    accountLoaderFactory.create(true).fill(suggestedAccounts);
+    List<AccountInfo> suggestedAccounts;
+    if (useFullTextSearch) {
+      suggestedAccounts = suggestAccountFullTextSearch(visibilityControl);
+    } else {
+      suggestedAccounts = suggestAccount(visibilityControl);
+    }
 
     List<SuggestedReviewerInfo> reviewer = Lists.newArrayList();
     for (AccountInfo a : suggestedAccounts) {
-      reviewer.add(new SuggestedReviewerInfo(a));
+      SuggestedReviewerInfo info = new SuggestedReviewerInfo();
+      info.account = a;
+      reviewer.add(info);
     }
 
     Project p = rsrc.getControl().getProject();
@@ -149,11 +189,13 @@
         GroupBaseInfo info = new GroupBaseInfo();
         info.id = Url.encode(g.getUUID().get());
         info.name = g.getName();
-        reviewer.add(new SuggestedReviewerInfo(info));
+        SuggestedReviewerInfo suggestedReviewerInfo = new SuggestedReviewerInfo();
+        suggestedReviewerInfo.group = info;
+        reviewer.add(suggestedReviewerInfo);
       }
     }
 
-    Collections.sort(reviewer);
+    reviewer = ORDERING.immutableSortedCopy(reviewer);
     if (reviewer.size() <= limit) {
       return reviewer;
     } else {
@@ -165,16 +207,16 @@
     if (rsrc.getControl().getRefControl().isVisibleByRegisteredUsers()) {
       return new VisibilityControl() {
         @Override
-        public boolean isVisibleTo(Account account) throws OrmException {
+        public boolean isVisibleTo(Account.Id account) throws OrmException {
           return true;
         }
       };
     } else {
       return new VisibilityControl() {
         @Override
-        public boolean isVisibleTo(Account account) throws OrmException {
+        public boolean isVisibleTo(Account.Id account) throws OrmException {
           IdentifiedUser who =
-              identifiedUserFactory.create(dbProvider, account.getId());
+              identifiedUserFactory.create(dbProvider, account);
           // we can't use changeControl directly as it won't suggest reviewers
           // to drafts
           return rsrc.getControl().forUser(who).isRefVisible();
@@ -193,16 +235,22 @@
     String a = query;
     String b = a + MAX_SUFFIX;
 
-    LinkedHashMap<Account.Id, AccountInfo> r = Maps.newLinkedHashMap();
+    Map<Account.Id, AccountInfo> r = new LinkedHashMap<>();
+    Map<Account.Id, String> queryEmail = new HashMap<>();
+
     for (Account p : dbProvider.get().accounts()
         .suggestByFullName(a, b, limit)) {
-      addSuggestion(r, p, new AccountInfo(p.getId()), visibilityControl);
+      if (p.isActive()) {
+        addSuggestion(r, 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 (p.isActive()) {
+          addSuggestion(r, p.getId(), visibilityControl);
+        }
       }
     }
 
@@ -211,26 +259,75 @@
           .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);
+          if (p.isActive()) {
+            if (addSuggestion(r, p.getId(), visibilityControl)) {
+              queryEmail.put(e.getAccountId(), e.getEmailAddress());
+            }
+          }
         }
       }
     }
 
-    return Lists.newArrayList(r.values());
+    accountLoader.fill();
+    for (Map.Entry<Account.Id, String> p : queryEmail.entrySet()) {
+      AccountInfo info = r.get(p.getKey());
+      if (info != null) {
+        info.email = p.getValue();
+      }
+    }
+    return new ArrayList<>(r.values());
   }
 
-  private void addSuggestion(Map<Account.Id, AccountInfo> map, Account account,
-      AccountInfo info, VisibilityControl visibilityControl)
+  private List<AccountInfo> suggestAccountFullTextSearch(
+      VisibilityControl visibilityControl) throws OrmException {
+    String str = query.toLowerCase();
+    Map<Account.Id, AccountInfo> accountMap = new LinkedHashMap<>();
+    List<Account> fullNameMatches = new ArrayList<>(fullTextMaxMatches);
+    List<Account> emailMatches = new ArrayList<>(fullTextMaxMatches);
+
+    for (Account a : reviewerSuggestionCache.get()) {
+      if (a.getFullName() != null
+          && a.getFullName().toLowerCase().contains(str)) {
+        fullNameMatches.add(a);
+      } else if (a.getPreferredEmail() != null
+          && emailMatches.size() < fullTextMaxMatches
+          && a.getPreferredEmail().toLowerCase().contains(str)) {
+        emailMatches.add(a);
+      }
+      if (fullNameMatches.size() >= fullTextMaxMatches) {
+        break;
+      }
+    }
+    for (Account a : fullNameMatches) {
+      addSuggestion(accountMap, a.getId(), visibilityControl);
+      if (accountMap.size() >= limit) {
+        break;
+      }
+    }
+    if (accountMap.size() < limit) {
+      for (Account a : emailMatches) {
+        addSuggestion(accountMap, a.getId(), visibilityControl);
+        if (accountMap.size() >= limit) {
+          break;
+        }
+      }
+    }
+    accountLoader.fill();
+    return Lists.newArrayList(accountMap.values());
+  }
+
+  private boolean addSuggestion(Map<Account.Id, AccountInfo> map,
+      Account.Id account, VisibilityControl visibilityControl)
       throws OrmException {
-    if (!map.containsKey(account.getId())
-        && account.isActive()
+    if (!map.containsKey(account)
         // 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);
+        && accountControl.canSee(account)) {
+      map.put(account, accountLoader.get(account));
+      return true;
     }
+    return false;
   }
 
   private boolean suggestGroupAsReviewer(Project project,
@@ -255,7 +352,7 @@
 
       // require that at least one member in the group can see the change
       for (Account account : members) {
-        if (visibilityControl.isVisibleTo(account)) {
+        if (visibilityControl.isVisibleTo(account.getId())) {
           return true;
         }
       }
@@ -267,29 +364,4 @@
 
     return false;
   }
-
-  public static class SuggestedReviewerInfo implements Comparable<SuggestedReviewerInfo> {
-    public AccountInfo account;
-    public 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/change/TestSubmitRule.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
index 25070ab..c17136a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
@@ -14,36 +14,27 @@
 
 package com.google.gerrit.server.change;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.common.base.Function;
-import com.google.common.base.Joiner;
-import com.google.common.base.Objects;
-import com.google.common.base.Throwables;
-import com.google.common.collect.Iterables;
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.rules.RulesCache;
-import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.change.TestSubmitRule.Input;
-import com.google.gerrit.server.project.RuleEvalException;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import com.googlecode.prolog_cafe.lang.Term;
-
 import org.kohsuke.args4j.Option;
 
-import java.io.ByteArrayInputStream;
 import java.util.List;
 import java.util.Map;
 
@@ -61,7 +52,7 @@
   private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
   private final RulesCache rules;
-  private final AccountInfo.Loader.Factory accountInfoFactory;
+  private final AccountLoader.Factory accountInfoFactory;
 
   @Option(name = "--filters", usage = "impact of filters in parent projects")
   private Filters filters = Filters.RUN;
@@ -70,7 +61,7 @@
   TestSubmitRule(Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       RulesCache rules,
-      AccountInfo.Loader.Factory infoFactory) {
+      AccountLoader.Factory infoFactory) {
     this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.rules = rules;
@@ -86,60 +77,27 @@
     if (input.rule != null && !rules.isProjectRulesEnabled()) {
       throw new AuthException("project rules are disabled");
     }
-    input.filters = Objects.firstNonNull(input.filters, filters);
-
+    input.filters = MoreObjects.firstNonNull(input.filters, filters);
     SubmitRuleEvaluator evaluator = new SubmitRuleEvaluator(
-        db.get(),
-        rsrc.getPatchSet(),
-        rsrc.getControl().getProjectControl(),
-        rsrc.getControl(),
-        rsrc.getChange(),
-        changeDataFactory.create(db.get(), rsrc.getChange()),
-        false,
-        "locate_submit_rule", "can_submit",
-        "locate_submit_filter", "filter_submit_results",
-        input.filters == Filters.SKIP,
-        input.rule != null
-          ? new ByteArrayInputStream(input.rule.getBytes(UTF_8))
-          : null);
+        changeDataFactory.create(db.get(), rsrc.getControl()));
 
-    List<Term> results;
-    try {
-      results = eval(evaluator);
-    } catch (RuleEvalException e) {
-      String msg = Joiner.on(": ").skipNulls().join(Iterables.transform(
-          Throwables.getCausalChain(e),
-          new Function<Throwable, String>() {
-            @Override
-            public String apply(Throwable in) {
-              return in.getMessage();
-            }
-          }));
-      throw new BadRequestException("rule failed: " + msg);
-    }
-    if (results.isEmpty()) {
-      throw new BadRequestException(String.format(
-          "rule %s has no solutions",
-          evaluator.getSubmitRule().toString()));
-    }
-
-    List<SubmitRecord> records = rsrc.getControl().resultsToSubmitRecord(
-        evaluator.getSubmitRule(),
-        results);
+    List<SubmitRecord> records = evaluator.setPatchSet(rsrc.getPatchSet())
+          .setLogErrors(false)
+          .setSkipSubmitFilters(input.filters == Filters.SKIP)
+          .setRule(input.rule)
+          .canSubmit();
     List<Record> out = Lists.newArrayListWithCapacity(records.size());
-    AccountInfo.Loader accounts = accountInfoFactory.create(true);
+    AccountLoader accounts = accountInfoFactory.create(true);
     for (SubmitRecord r : records) {
       out.add(new Record(r, accounts));
     }
+    if (!out.isEmpty()) {
+      out.get(0).prologReductionCount = evaluator.getReductionsConsumed();
+    }
     accounts.fill();
     return out;
   }
 
-  private static List<Term> eval(SubmitRuleEvaluator evaluator)
-      throws RuleEvalException {
-    return evaluator.evaluate();
-  }
-
   static class Record {
     SubmitRecord.Status status;
     String errorMessage;
@@ -148,8 +106,9 @@
     Map<String, None> need;
     Map<String, AccountInfo> may;
     Map<String, None> impossible;
+    Integer prologReductionCount;
 
-    Record(SubmitRecord r, AccountInfo.Loader accounts) {
+    Record(SubmitRecord r, AccountLoader accounts) {
       this.status = r.status;
       this.errorMessage = r.errorMessage;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
index 3b7f419..f6016b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
@@ -14,10 +14,9 @@
 
 package com.google.gerrit.server.change;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.common.base.Objects;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.common.base.MoreObjects;
+import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -26,20 +25,14 @@
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.change.TestSubmitRule.Filters;
 import com.google.gerrit.server.change.TestSubmitRule.Input;
-import com.google.gerrit.server.project.RuleEvalException;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import com.googlecode.prolog_cafe.lang.SymbolTerm;
-import com.googlecode.prolog_cafe.lang.Term;
-
 import org.kohsuke.args4j.Option;
 
-import java.io.ByteArrayInputStream;
-import java.util.List;
-
 public class TestSubmitType implements RestModifyView<RevisionResource, Input> {
   private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
@@ -59,60 +52,29 @@
 
   @Override
   public SubmitType apply(RevisionResource rsrc, Input input)
-      throws AuthException, BadRequestException {
+      throws AuthException, BadRequestException, OrmException {
     if (input == null) {
       input = new Input();
     }
     if (input.rule != null && !rules.isProjectRulesEnabled()) {
       throw new AuthException("project rules are disabled");
     }
-    input.filters = Objects.firstNonNull(input.filters, filters);
-
+    input.filters = MoreObjects.firstNonNull(input.filters, filters);
     SubmitRuleEvaluator evaluator = new SubmitRuleEvaluator(
-        db.get(),
-        rsrc.getPatchSet(),
-        rsrc.getControl().getProjectControl(),
-        rsrc.getControl(),
-        rsrc.getChange(),
-        changeDataFactory.create(db.get(), rsrc.getChange()),
-        false,
-        "locate_submit_type", "get_submit_type",
-        "locate_submit_type_filter", "filter_submit_type_results",
-        input.filters == Filters.SKIP,
-        input.rule != null
-          ? new ByteArrayInputStream(input.rule.getBytes(UTF_8))
-          : null);
+          changeDataFactory.create(db.get(), rsrc.getControl()));
 
-    List<Term> results;
-    try {
-      results = evaluator.evaluate();
-    } catch (RuleEvalException e) {
-      throw new BadRequestException(String.format(
-          "rule failed with exception: %s",
-          e.getMessage()));
-    }
-    if (results.isEmpty()) {
-      throw new BadRequestException(String.format(
-          "rule %s has no solution",
-          evaluator.getSubmitRule()));
-    }
-    Term type = results.get(0);
-    if (!type.isSymbol()) {
+    SubmitTypeRecord rec = evaluator.setPatchSet(rsrc.getPatchSet())
+        .setLogErrors(false)
+        .setSkipSubmitFilters(input.filters == Filters.SKIP)
+        .setRule(input.rule)
+        .getSubmitType();
+    if (rec.status != SubmitTypeRecord.Status.OK) {
       throw new BadRequestException(String.format(
           "rule %s produced invalid result: %s",
-          evaluator.getSubmitRule().toString(),
-          type));
+          evaluator.getSubmitRule(), rec));
     }
 
-    String typeName = ((SymbolTerm) type).name();
-    try {
-      return SubmitType.valueOf(typeName.toUpperCase());
-    } catch (IllegalArgumentException e) {
-      throw new BadRequestException(String.format(
-          "rule %s produced invalid result: %s",
-          evaluator.getSubmitRule().toString(),
-          type));
-    }
+    return rec.type;
   }
 
   static class Get implements RestReadView<RevisionResource> {
@@ -125,7 +87,7 @@
 
     @Override
     public SubmitType apply(RevisionResource resource)
-        throws AuthException, BadRequestException {
+        throws AuthException, BadRequestException, OrmException {
       return test.apply(resource, null);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
index 070203d..67e70c1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.changedetail;
 
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -21,20 +22,22 @@
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetAncestor;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeConflictException;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -98,6 +101,7 @@
    * @param change the change to perform the rebase for
    * @param patchSetId the id of the patch set
    * @param uploader the user that creates the rebased patch set
+   * @param newBaseRev the commit that should be the new base
    * @throws NoSuchChangeException thrown if the change to which the patch set
    *         belongs does not exist or is not visible to the user
    * @throws EmailException thrown if sending the e-mail to notify about the new
@@ -106,9 +110,9 @@
    * @throws IOException thrown if rebase is not possible or not needed
    * @throws InvalidChangeOperationException thrown if rebase is not allowed
    */
-  public void rebase(Change change, PatchSet.Id patchSetId, final IdentifiedUser uploader)
-      throws NoSuchChangeException, EmailException, OrmException, IOException,
-      InvalidChangeOperationException {
+  public void rebase(Change change, PatchSet.Id patchSetId, final IdentifiedUser uploader,
+      final String newBaseRev) throws NoSuchChangeException, EmailException, OrmException,
+      IOException, InvalidChangeOperationException {
     final Change.Id changeId = patchSetId.getParentKey();
     final ChangeControl changeControl =
         changeControlFactory.validateFor(change, uploader);
@@ -117,18 +121,20 @@
           "Cannot rebase: New patch sets are not allowed to be added to change: "
               + changeId.toString());
     }
-    Repository git = null;
-    RevWalk rw = null;
-    ObjectInserter inserter = null;
-    try {
-      git = gitManager.openRepository(change.getProject());
-      rw = new RevWalk(git);
-      inserter = git.newObjectInserter();
-
-      final String baseRev = findBaseRevision(patchSetId, db.get(),
-          change.getDest(), git, null, null, null);
-      final RevCommit baseCommit =
-          rw.parseCommit(ObjectId.fromString(baseRev));
+    try (Repository git = gitManager.openRepository(change.getProject());
+        RevWalk rw = new RevWalk(git);
+        ObjectInserter inserter = git.newObjectInserter()) {
+      String baseRev = newBaseRev;
+      if (baseRev == null) {
+          baseRev = findBaseRevision(patchSetId, db.get(),
+              change.getDest(), git, null, null, null);
+      }
+      ObjectId baseObjectId = git.resolve(baseRev);
+      if (baseObjectId == null) {
+        throw new InvalidChangeOperationException(
+          "Cannot rebase: Failed to resolve baseRev: " + baseRev);
+      }
+      final RevCommit baseCommit = rw.parseCommit(baseObjectId);
 
       PersonIdent committerIdent =
           uploader.newCommitterIdent(TimeUtil.nowTs(),
@@ -137,19 +143,9 @@
       rebase(git, rw, inserter, patchSetId, change,
           uploader, baseCommit, mergeUtilFactory.create(
               changeControl.getProjectControl().getProjectState(), true),
-          committerIdent, true, true, ValidatePolicy.GERRIT);
-    } catch (PathConflictException e) {
+          committerIdent, true, ValidatePolicy.GERRIT);
+    } catch (MergeConflictException e) {
       throw new IOException(e.getMessage());
-    } finally {
-      if (inserter != null) {
-        inserter.release();
-      }
-      if (rw != null) {
-        rw.release();
-      }
-      if (git != null) {
-        git.close();
-      }
     }
   }
 
@@ -264,7 +260,6 @@
    * @param baseCommit the commit that should be the new base
    * @param mergeUtil merge utilities for the destination project
    * @param committerIdent the committer's identity
-   * @param sendMail if a mail notification should be sent for the new patch set
    * @param runHooks if hooks should be run for the new patch set
    * @param validate if commit validation should be run for the new patch set
    * @return the new patch set which is based on the given base commit
@@ -278,10 +273,10 @@
       final ObjectInserter inserter, final PatchSet.Id patchSetId,
       final Change change, final IdentifiedUser uploader, final RevCommit baseCommit,
       final MergeUtil mergeUtil, PersonIdent committerIdent,
-      boolean sendMail, boolean runHooks, ValidatePolicy validate)
+      boolean runHooks, ValidatePolicy validate)
           throws NoSuchChangeException,
       OrmException, IOException, InvalidChangeOperationException,
-      PathConflictException {
+      MergeConflictException {
     if (!change.currentPatchSetId().equals(patchSetId)) {
       throw new InvalidChangeOperationException("patch set is not current");
     }
@@ -299,11 +294,10 @@
 
     PatchSetInserter patchSetInserter = patchSetInserterFactory
         .create(git, revWalk, changeControl, rebasedCommit)
-        .setCopyLabels(true)
         .setValidatePolicy(validate)
         .setDraft(originalPatchSet.isDraft())
         .setUploader(uploader.getAccountId())
-        .setSendMail(sendMail)
+        .setSendMail(false)
         .setRunHooks(runHooks);
 
     final PatchSet.Id newPatchSetId = patchSetInserter.getPatchSetId();
@@ -323,54 +317,62 @@
   }
 
   /**
-   * Rebases a commit.
+   * Rebase a commit.
    *
-   * @param git repository to find commits in
-   * @param inserter inserter to handle new trees and blobs
-   * @param original The commit to rebase
-   * @param base Base to rebase against
-   * @param mergeUtil merge utilities for the destination project
-   * @param committerIdent committer identity
-   * @return the id of the rebased commit
-   * @throws IOException Merged failed
-   * @throws PathConflictException the rebase failed due to a path conflict
+   * @param git repository to find commits in.
+   * @param inserter inserter to handle new trees and blobs.
+   * @param original the commit to rebase.
+   * @param base base to rebase against.
+   * @param mergeUtil merge utilities for the destination project.
+   * @param committerIdent committer identity.
+   * @return the id of the rebased commit.
+   * @throws MergeConflictException the rebase failed due to a merge conflict.
+   * @throws IOException the merge failed for another reason.
    */
-  private ObjectId rebaseCommit(final Repository git,
-      final ObjectInserter inserter, final RevCommit original,
-      final RevCommit base, final MergeUtil mergeUtil,
-      final PersonIdent committerIdent) throws IOException,
-      PathConflictException {
-
-    final RevCommit parentCommit = original.getParent(0);
+  private ObjectId rebaseCommit(Repository git, ObjectInserter inserter,
+      RevCommit original, RevCommit base, MergeUtil mergeUtil,
+      PersonIdent committerIdent) throws MergeConflictException, IOException {
+    RevCommit parentCommit = original.getParent(0);
 
     if (base.equals(parentCommit)) {
       throw new IOException("Change is already up to date.");
     }
 
-    final ThreeWayMerger merger = mergeUtil.newThreeWayMerger(git, inserter);
+    ThreeWayMerger merger = mergeUtil.newThreeWayMerger(git, inserter);
     merger.setBase(parentCommit);
     merger.merge(original, base);
 
     if (merger.getResultTreeId() == null) {
-      throw new PathConflictException(
-          "The change could not be rebased due to a path conflict during merge.");
+      throw new MergeConflictException(
+          "The change could not be rebased due to a conflict during merge.");
     }
 
-    final CommitBuilder cb = new CommitBuilder();
+    CommitBuilder cb = new CommitBuilder();
     cb.setTreeId(merger.getResultTreeId());
     cb.setParentId(base);
     cb.setAuthor(original.getAuthorIdent());
     cb.setMessage(original.getFullMessage());
     cb.setCommitter(committerIdent);
-    final ObjectId objectId = inserter.insert(cb);
+    ObjectId objectId = inserter.insert(cb);
     inserter.flush();
     return objectId;
   }
 
+  public boolean canRebase(ChangeResource r) {
+    Change c = r.getChange();
+    return canRebase(c.getProject(), c.currentPatchSetId(), c.getDest());
+  }
+
   public boolean canRebase(RevisionResource r) {
+    return canRebase(r.getChange().getProject(),
+        r.getPatchSet().getId(), r.getChange().getDest());
+  }
+
+  public boolean canRebase(Project.NameKey project,
+      PatchSet.Id patchSetId, Branch.NameKey branch) {
     Repository git;
     try {
-      git = gitManager.openRepository(r.getChange().getProject());
+      git = gitManager.openRepository(project);
     } catch (RepositoryNotFoundException err) {
       return false;
     } catch (IOException err) {
@@ -378,9 +380,9 @@
     }
     try {
       findBaseRevision(
-          r.getPatchSet().getId(),
+          patchSetId,
           db.get(),
-          r.getChange().getDest(),
+          branch,
           git,
           null,
           null,
@@ -394,24 +396,4 @@
       git.close();
     }
   }
-
-  public static boolean canDoRebase(final ReviewDb db,
-      final Change change, final GitRepositoryManager gitManager,
-      List<PatchSetAncestor> patchSetAncestors,
-      List<PatchSet> depPatchSetList, List<Change> depChangeList)
-      throws OrmException, RepositoryNotFoundException, IOException {
-
-    final Repository git = gitManager.openRepository(change.getProject());
-
-    try {
-      // If no exception is thrown, then we can do a rebase.
-      findBaseRevision(change.currentPatchSetId(), db, change.getDest(), git,
-          patchSetAncestors, depPatchSetList, depChangeList);
-      return true;
-    } catch (IOException e) {
-      return false;
-    } finally {
-      git.close();
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AllProjectsNameProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AllProjectsNameProvider.java
index 73074b7..2f25da4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AllProjectsNameProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AllProjectsNameProvider.java
@@ -33,6 +33,7 @@
     name = new AllProjectsName(n);
   }
 
+  @Override
   public AllProjectsName get() {
     return name;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AllUsersNameProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AllUsersNameProvider.java
index e6ec095..f5aa127 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AllUsersNameProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AllUsersNameProvider.java
@@ -33,6 +33,7 @@
     name = new AllUsersName(n);
   }
 
+  @Override
   public AllUsersName get() {
     return name;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
index c2cf95e..d7138b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
@@ -38,6 +38,7 @@
   private final String httpHeader;
   private final String httpDisplaynameHeader;
   private final String httpEmailHeader;
+  private final String httpExternalIdHeader;
   private final String registerPageUrl;
   private final boolean trustContainerAuth;
   private final boolean enableRunAs;
@@ -61,6 +62,7 @@
     httpHeader = cfg.getString("auth", null, "httpheader");
     httpDisplaynameHeader = cfg.getString("auth", null, "httpdisplaynameheader");
     httpEmailHeader = cfg.getString("auth", null, "httpemailheader");
+    httpExternalIdHeader = cfg.getString("auth", null, "httpexternalidheader");
     loginUrl = cfg.getString("auth", null, "loginurl");
     logoutUrl = cfg.getString("auth", null, "logouturl");
     registerPageUrl = cfg.getString("auth", null, "registerPageUrl");
@@ -111,7 +113,7 @@
   }
 
   private static AuthType toType(final Config cfg) {
-    return ConfigUtil.getEnum(cfg, "auth", null, "type", AuthType.OPENID);
+    return cfg.getEnum("auth", null, "type", AuthType.OPENID);
   }
 
   /** Type of user authentication used by this Gerrit server. */
@@ -131,6 +133,10 @@
     return httpEmailHeader;
   }
 
+  public String getHttpExternalIdHeader() {
+    return httpExternalIdHeader;
+  }
+
   public String getLoginUrl() {
     return loginUrl;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java
index 289173b..99a4f52 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java
@@ -24,12 +24,14 @@
 
   public String accessDatabase;
   public String administrateServer;
+  public String batchChangesLimit;
   public String createAccount;
   public String createGroup;
   public String createProject;
   public String emailReviewers;
   public String flushCaches;
   public String killTask;
+  public String modifyAccount;
   public String priority;
   public String queryLimit;
   public String runAs;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
index ab290cb..dd4ba79 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
@@ -17,6 +17,7 @@
 import static org.eclipse.jgit.util.StringUtils.equalsIgnoreCase;
 
 import org.eclipse.jgit.lib.Config;
+
 import java.lang.reflect.InvocationTargetException;
 import java.util.ArrayList;
 import java.util.List;
@@ -25,24 +26,6 @@
 import java.util.regex.Pattern;
 
 public class ConfigUtil {
-  /**
-   * Parse a Java enumeration from the configuration.
-   *
-   * @param <T> type of the enumeration object.
-   * @param config the configuration file to read.
-   * @param section section the key is in.
-   * @param subsection subsection the key is in, or null if not in a subsection.
-   * @param setting name of the setting to read.
-   * @param defaultValue default value to return if the setting was not set.
-   *        Must not be null as the enumeration values are derived from this.
-   * @return the selected enumeration value, or {@code defaultValue}.
-   */
-  public static <T extends Enum<?>> T getEnum(final Config config,
-      final String section, final String subsection, final String setting,
-      final T defaultValue) {
-    final T[] all = allValuesOf(defaultValue);
-    return getEnum(config, section, subsection, setting, all, defaultValue);
-  }
 
   @SuppressWarnings("unchecked")
   private static <T> T[] allValuesOf(final T defaultValue) {
@@ -65,31 +48,6 @@
    * Parse a Java enumeration from the configuration.
    *
    * @param <T> type of the enumeration object.
-   * @param config the configuration file to read.
-   * @param section section the key is in.
-   * @param subsection subsection the key is in, or null if not in a subsection.
-   * @param setting name of the setting to read.
-   * @param all all possible values in the enumeration which should be
-   *        recognized. This should be {@code EnumType.values()}.
-   * @param defaultValue default value to return if the setting was not set.
-   *        This value may be null.
-   * @return the selected enumeration value, or {@code defaultValue}.
-   */
-  public static <T extends Enum<?>> T getEnum(final Config config,
-      final String section, final String subsection, final String setting,
-      final T[] all, final T defaultValue) {
-    final String valueString = config.getString(section, subsection, setting);
-    if (valueString == null) {
-      return defaultValue;
-    }
-
-    return getEnum(section, subsection, setting, valueString, all);
-  }
-
-  /**
-   * Parse a Java enumeration from the configuration.
-   *
-   * @param <T> type of the enumeration object.
    * @param section section the key is in.
    * @param subsection subsection the key is in, or null if not in a subsection.
    * @param setting name of the setting to read.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Current.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/DisableReverseDnsLookup.java
similarity index 79%
rename from gerrit-server/src/main/java/com/google/gerrit/server/schema/Current.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/config/DisableReverseDnsLookup.java
index b16c977..04712f9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Current.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/DisableReverseDnsLookup.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// Copyright (C) 2014 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.server.config;
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
@@ -20,8 +20,7 @@
 
 import java.lang.annotation.Retention;
 
-/** Indicates the {@link SchemaVersion} is the current one. */
 @Retention(RUNTIME)
 @BindingAnnotation
-public @interface Current {
+public @interface DisableReverseDnsLookup {
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/DisableReverseDnsLookupProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/DisableReverseDnsLookupProvider.java
new file mode 100644
index 0000000..8c42714
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/DisableReverseDnsLookupProvider.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.Config;
+
+public class DisableReverseDnsLookupProvider implements Provider<Boolean> {
+  private final boolean disableReverseDnsLookup;
+
+  @Inject
+  DisableReverseDnsLookupProvider(@GerritServerConfig Config config) {
+    disableReverseDnsLookup =
+        config.getBoolean("gerrit", null, "disableReverseDnsLookup", false);
+  }
+
+  @Override
+  public Boolean get() {
+    return disableReverseDnsLookup;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java
index 80031c2..2d9f21a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
-import com.google.gerrit.reviewdb.client.SystemConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -34,8 +33,7 @@
   private final Set<DownloadCommand> downloadCommands;
 
   @Inject
-  DownloadConfig(@GerritServerConfig final Config cfg,
-      final SystemConfig s) {
+  DownloadConfig(@GerritServerConfig final Config cfg) {
     List<DownloadScheme> allSchemes =
         ConfigUtil.getEnumList(cfg, "download", null, "scheme",
             DownloadScheme.DEFAULT_DOWNLOADS);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritConfig.java
new file mode 100644
index 0000000..fff29fa
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritConfig.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.server.securestore.SecureStore;
+
+import org.eclipse.jgit.lib.Config;
+
+class GerritConfig extends Config {
+  private final SecureStore secureStore;
+
+  GerritConfig(Config baseConfig, SecureStore secureStore) {
+    super(baseConfig);
+    this.secureStore = secureStore;
+  }
+
+  @Override
+  public String getString(String section, String subsection, String name) {
+    String secure = secureStore.get(section, subsection, name);
+    if (secure != null) {
+      return secure;
+    }
+    return super.getString(section, subsection, name);
+  }
+
+  @Override
+  public String[] getStringList(String section, String subsection, String name) {
+    String[] secure = secureStore.getList(section, subsection, name);
+    if (secure != null && secure.length > 0) {
+      return secure;
+    }
+    return super.getStringList(section, subsection, name);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 8c686d5..1dff046 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -18,7 +18,7 @@
 
 import com.google.common.cache.Cache;
 import com.google.gerrit.audit.AuditModule;
-import com.google.gerrit.common.ChangeListener;
+import com.google.gerrit.common.EventListener;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.config.DownloadCommand;
 import com.google.gerrit.extensions.config.DownloadScheme;
@@ -32,6 +32,9 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.systemstatus.MessageOfTheDay;
+import com.google.gerrit.extensions.webui.BranchWebLink;
+import com.google.gerrit.extensions.webui.DiffWebLink;
+import com.google.gerrit.extensions.webui.FileWebLink;
 import com.google.gerrit.extensions.webui.PatchSetWebLink;
 import com.google.gerrit.extensions.webui.ProjectWebLink;
 import com.google.gerrit.extensions.webui.TopMenu;
@@ -41,12 +44,8 @@
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.CmdLineParserModule;
-import com.google.gerrit.server.FileTypeRegistry;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.MimeUtilFileTypeRegistry;
 import com.google.gerrit.server.PluginUser;
-import com.google.gerrit.server.WebLinks;
-import com.google.gerrit.server.WebLinksProvider;
 import com.google.gerrit.server.account.AccountByEmailCacheImpl;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.AccountControl;
@@ -71,10 +70,9 @@
 import com.google.gerrit.server.avatar.AvatarProvider;
 import com.google.gerrit.server.cache.CacheRemovalListener;
 import com.google.gerrit.server.change.ChangeKindCacheImpl;
-import com.google.gerrit.server.change.MergeabilityChecker;
+import com.google.gerrit.server.change.MergeabilityCacheImpl;
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.ChangeCache;
 import com.google.gerrit.server.git.ChangeMergeQueue;
 import com.google.gerrit.server.git.GarbageCollection;
 import com.google.gerrit.server.git.GitModule;
@@ -82,6 +80,7 @@
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.NotesBranchUtil;
 import com.google.gerrit.server.git.ReceivePackInitializer;
+import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
@@ -89,9 +88,12 @@
 import com.google.gerrit.server.git.validators.MergeValidationListener;
 import com.google.gerrit.server.git.validators.MergeValidators;
 import com.google.gerrit.server.git.validators.MergeValidators.ProjectConfigValidator;
+import com.google.gerrit.server.git.validators.RefOperationValidationListener;
+import com.google.gerrit.server.git.validators.RefOperationValidators;
 import com.google.gerrit.server.git.validators.UploadValidationListener;
 import com.google.gerrit.server.git.validators.UploadValidators;
 import com.google.gerrit.server.group.GroupModule;
+import com.google.gerrit.server.index.ReindexAfterUpdate;
 import com.google.gerrit.server.mail.AddReviewerSender;
 import com.google.gerrit.server.mail.CreateChangeSender;
 import com.google.gerrit.server.mail.EmailModule;
@@ -102,6 +104,8 @@
 import com.google.gerrit.server.mail.RegisterNewEmailSender;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
 import com.google.gerrit.server.mail.VelocityRuntimeProvider;
+import com.google.gerrit.server.mime.FileTypeRegistry;
+import com.google.gerrit.server.mime.MimeUtilFileTypeRegistry;
 import com.google.gerrit.server.notedb.NoteDbModule;
 import com.google.gerrit.server.patch.PatchListCacheImpl;
 import com.google.gerrit.server.patch.PatchScriptFactory;
@@ -119,13 +123,14 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.SectionSortCache;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ConflictsCacheImpl;
 import com.google.gerrit.server.ssh.SshAddressesModule;
 import com.google.gerrit.server.tools.ToolsCatalog;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.validators.GroupCreationValidationListener;
+import com.google.gerrit.server.validators.HashtagValidationListener;
+import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
 import com.google.gerrit.server.validators.ProjectCreationValidationListener;
 import com.google.inject.Inject;
 import com.google.inject.TypeLiteral;
@@ -158,11 +163,12 @@
     install(authModule);
     install(AccountByEmailCacheImpl.module());
     install(AccountCacheImpl.module());
-    install(ChangeCache.module());
+    install(SearchingChangeCacheImpl.module());
     install(ChangeKindCacheImpl.module());
     install(ConflictsCacheImpl.module());
     install(GroupCacheImpl.module());
     install(GroupIncludeCacheImpl.module());
+    install(MergeabilityCacheImpl.module());
     install(PatchListCacheImpl.module());
     install(ProjectCacheImpl.module());
     install(SectionSortCache.module());
@@ -184,7 +190,6 @@
     factory(AddReviewerSender.Factory.class);
     factory(CapabilityControl.Factory.class);
     factory(ChangeData.Factory.class);
-    factory(ChangeQueryBuilder.Factory.class);
     factory(CreateChangeSender.Factory.class);
     factory(GroupDetailFactory.Factory.class);
     factory(GroupInfoCacheFactory.Factory.class);
@@ -232,7 +237,8 @@
         .in(SINGLETON);
     bind(FromAddressGenerator.class).toProvider(
         FromAddressGeneratorProvider.class).in(SINGLETON);
-    bind(WebLinks.class).toProvider(WebLinksProvider.class).in(SINGLETON);
+    bind(Boolean.class).annotatedWith(DisableReverseDnsLookup.class)
+        .toProvider(DisableReverseDnsLookupProvider.class).in(SINGLETON);
 
     bind(PatchSetInfoFactory.class);
     bind(IdentifiedUser.GenericFactory.class).in(SINGLETON);
@@ -261,15 +267,18 @@
     DynamicSet.setOf(binder(), ProjectDeletedListener.class);
     DynamicSet.setOf(binder(), HeadUpdatedListener.class);
     DynamicSet.setOf(binder(), UsageDataPublishedListener.class);
-    DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ChangeCache.class);
-    DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(MergeabilityChecker.class);
+    DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(SearchingChangeCacheImpl.class);
+    DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ReindexAfterUpdate.class);
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
         .to(ProjectConfigEntry.UpdateChecker.class);
-    DynamicSet.setOf(binder(), ChangeListener.class);
+    DynamicSet.setOf(binder(), EventListener.class);
     DynamicSet.setOf(binder(), CommitValidationListener.class);
+    DynamicSet.setOf(binder(), RefOperationValidationListener.class);
     DynamicSet.setOf(binder(), MergeValidationListener.class);
     DynamicSet.setOf(binder(), ProjectCreationValidationListener.class);
     DynamicSet.setOf(binder(), GroupCreationValidationListener.class);
+    DynamicSet.setOf(binder(), HashtagValidationListener.class);
+    DynamicSet.setOf(binder(), OutgoingEmailValidationListener.class);
     DynamicItem.itemOf(binder(), AvatarProvider.class);
     DynamicSet.setOf(binder(), LifecycleListener.class);
     DynamicSet.setOf(binder(), TopMenu.class);
@@ -278,7 +287,10 @@
     DynamicMap.mapOf(binder(), DownloadCommand.class);
     DynamicMap.mapOf(binder(), ProjectConfigEntry.class);
     DynamicSet.setOf(binder(), PatchSetWebLink.class);
+    DynamicSet.setOf(binder(), FileWebLink.class);
+    DynamicSet.setOf(binder(), DiffWebLink.class);
     DynamicSet.setOf(binder(), ProjectWebLink.class);
+    DynamicSet.setOf(binder(), BranchWebLink.class);
 
     factory(UploadValidators.Factory.class);
     DynamicSet.setOf(binder(), UploadValidationListener.class);
@@ -286,6 +298,7 @@
     bind(AnonymousUser.class);
 
     factory(CommitValidators.Factory.class);
+    factory(RefOperationValidators.Factory.class);
     factory(MergeValidators.Factory.class);
     factory(ProjectConfigValidator.Factory.class);
     factory(NotesBranchUtil.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
index 872c473..cdfad8d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.server.RequestCleanup;
 import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.SubmoduleOp;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.PerRequestProjectControlCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.inject.servlet.RequestScoped;
@@ -34,7 +33,6 @@
     bind(IdentifiedUser.RequestFactory.class).in(SINGLETON);
 
     bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
-    bind(ChangeControl.Factory.class).in(SINGLETON);
     bind(ProjectControl.Factory.class).in(SINGLETON);
 
     factory(SubmoduleOp.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigModule.java
index 18a62d0..89331fd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigModule.java
@@ -16,17 +16,67 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
+import com.google.gerrit.server.securestore.DefaultSecureStore;
+import com.google.gerrit.server.securestore.SecureStore;
+import com.google.gerrit.server.securestore.SecureStoreProvider;
 import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.ProvisionException;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+import java.io.File;
+import java.io.IOException;
 
 /** Creates {@link GerritServerConfig}. */
 public class GerritServerConfigModule extends AbstractModule {
+  public static String getSecureStoreClassName(final File sitePath) {
+    if (sitePath != null) {
+      return getSecureStoreFromGerritConfig(sitePath);
+    }
+
+    String secureStoreProperty = System.getProperty("gerrit.secure_store_class");
+    return nullToDefault(secureStoreProperty);
+  }
+
+  private static String getSecureStoreFromGerritConfig(final File sitePath) {
+    AbstractModule m = new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(File.class).annotatedWith(SitePath.class).toInstance(sitePath);
+        bind(SitePaths.class);
+      }
+    };
+    Injector injector = Guice.createInjector(m);
+    SitePaths site = injector.getInstance(SitePaths.class);
+    FileBasedConfig cfg = new FileBasedConfig(site.gerrit_config, FS.DETECTED);
+    if (!cfg.getFile().exists()) {
+      return DefaultSecureStore.class.getName();
+    }
+
+    try {
+      cfg.load();
+      String className = cfg.getString("gerrit", null, "secureStoreClass");
+      return nullToDefault(className);
+    } catch (IOException | ConfigInvalidException e) {
+      throw new ProvisionException(e.getMessage(), e);
+    }
+  }
+
+  private static String nullToDefault(String className) {
+    return className != null ? className : DefaultSecureStore.class.getName();
+  }
+
   @Override
   protected void configure() {
     bind(SitePaths.class);
     bind(TrackingFooters.class).toProvider(TrackingFootersProvider.class).in(SINGLETON) ;
     bind(Config.class).annotatedWith(GerritServerConfig.class).toProvider(
         GerritServerConfigProvider.class).in(SINGLETON);
+    bind(SecureStore.class).toProvider(SecureStoreProvider.class).in(SINGLETON);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigProvider.java
index 92a2614..aa699c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigProvider.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.gerrit.server.securestore.SecureStore;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
@@ -33,10 +34,12 @@
       LoggerFactory.getLogger(GerritServerConfigProvider.class);
 
   private final SitePaths site;
+  private final SecureStore secureStore;
 
   @Inject
-  GerritServerConfigProvider(final SitePaths site) {
+  GerritServerConfigProvider(SitePaths site, SecureStore secureStore) {
     this.site = site;
+    this.secureStore = secureStore;
   }
 
   @Override
@@ -46,7 +49,7 @@
     if (!cfg.getFile().exists()) {
       log.info("No " + site.gerrit_config.getAbsolutePath()
           + "; assuming defaults");
-      return cfg;
+      return new GerritConfig(cfg, secureStore);
     }
 
     try {
@@ -57,17 +60,6 @@
       throw new ProvisionException(e.getMessage(), e);
     }
 
-    if (site.secure_config.exists()) {
-      cfg = new FileBasedConfig(cfg, site.secure_config, FS.DETECTED);
-      try {
-        cfg.load();
-      } catch (IOException e) {
-        throw new ProvisionException(e.getMessage(), e);
-      } catch (ConfigInvalidException e) {
-        throw new ProvisionException(e.getMessage(), e);
-      }
-    }
-
-    return cfg;
+    return new GerritConfig(cfg, secureStore);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java
index 27fcd72..0600712 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.server.git.ProjectConfig;
@@ -86,7 +86,8 @@
     if (defaultValue == null) {
       return cfg.getString(PLUGIN, pluginName, name);
     } else {
-      return Objects.firstNonNull(cfg.getString(PLUGIN, pluginName, name), defaultValue);
+      return MoreObjects.firstNonNull(cfg.getString(PLUGIN, pluginName, name),
+          defaultValue);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java
index 385570b..5943801 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectConfigEntry.java
@@ -185,23 +185,59 @@
     return permittedValues;
   }
 
+  /**
+   * @param project project state.
+   * @return whether the project is editable.
+   */
   public boolean isEditable(ProjectState project) {
     return true;
   }
 
+  /**
+   * @param project project state.
+   * @return any warning associated with the project.
+   */
   public String getWarning(ProjectState project) {
     return null;
   }
 
+  /**
+   * Called after a project config is updated.
+   *
+   * @param project project name.
+   * @param oldValue old entry value.
+   * @param newValue new entry value.
+   */
   public void onUpdate(Project.NameKey project, String oldValue, String newValue) {
   }
 
+  /**
+   * Called after a project config is updated.
+   *
+   * @param project project name.
+   * @param oldValue old entry value.
+   * @param newValue new entry value.
+   */
   public void onUpdate(Project.NameKey project, Boolean oldValue, Boolean newValue) {
   }
 
+  /**
+   * Called after a project config is updated.
+   *
+   * @param project project name.
+   * @param oldValue old entry value.
+   * @param newValue new entry value.
+   */
   public void onUpdate(Project.NameKey project, Integer oldValue, Integer newValue) {
   }
 
+  /**
+   * Called after a project config is updated.
+   *
+   * @param project project name.
+   * @param oldValue old entry value.
+   * @param newValue new entry value.
+   */
   public void onUpdate(Project.NameKey project, Long oldValue, Long newValue) {
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ScheduleConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ScheduleConfig.java
index 66f0171..d19f063 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ScheduleConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ScheduleConfig.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.common.annotations.VisibleForTesting;
+
 import org.eclipse.jgit.lib.Config;
 import org.joda.time.DateTime;
 import org.joda.time.LocalDateTime;
@@ -52,12 +54,13 @@
     this(rc, section, subsection, keyInterval, keyStartTime, DateTime.now());
   }
 
-  /* For testing we need to be able to pass now */
+  @VisibleForTesting
   ScheduleConfig(Config rc, String section, String subsection, DateTime now) {
     this(rc, section, subsection, KEY_INTERVAL, KEY_STARTTIME, now);
   }
 
-  private ScheduleConfig(Config rc, String section, String subsection,
+  @VisibleForTesting
+  ScheduleConfig(Config rc, String section, String subsection,
       String keyInterval, String keyStartTime, DateTime now) {
     this.interval = interval(rc, section, subsection, keyInterval);
     if (interval > 0) {
@@ -68,10 +71,18 @@
     }
   }
 
+  /**
+   * Milliseconds between constructor invocation and first event time.
+   * <p>
+   * If there is any lag between the constructor invocation and queuing the
+   * object into an executor the event will run later, as there is no method
+   * to adjust for the scheduling delay.
+   */
   public long getInitialDelay() {
     return initialDelay;
   }
 
+  /** Number of milliseconds between events. */
   public long getInterval() {
     return interval;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
index e8b2dc5..d97499c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
@@ -49,13 +49,11 @@
         || i.useFlashClipboard != null || i.downloadScheme != null
         || i.downloadCommand != null || i.copySelfOnEmail != null
         || i.dateFormat != null || i.timeFormat != null
-        || i.reversePatchSetOrder != null
         || i.relativeDateInChangeTable != null
         || i.sizeBarInChangeTable != null
         || i.legacycidInChangeTable != null
-        || i.reviewCategoryStrategy != null
-        || i.commentVisibilityStrategy != null || i.diffView != null
-        || i.changeScreen != null) {
+        || i.muteCommonPathPrefixes != null
+        || i.reviewCategoryStrategy != null) {
       throw new BadRequestException("unsupported option");
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
index 9d7c54a..fbff7c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
@@ -100,12 +100,13 @@
 
     if (site_path.exists()) {
       final String[] contents = site_path.list();
-      if (contents != null)
+      if (contents != null) {
         isNew = contents.length == 0;
-      else if (site_path.isDirectory())
+      } else if (site_path.isDirectory()) {
         throw new FileNotFoundException("Cannot access " + site_path);
-      else
+      } else {
         throw new FileNotFoundException("Not a directory: " + site_path);
+      }
     } else {
       isNew = true;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFootersProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFootersProvider.java
index a76b010..4c1bde9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFootersProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFootersProvider.java
@@ -24,7 +24,11 @@
 import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.regex.PatternSyntaxException;
 
 /** Provides a list of all configured {@link TrackingFooter}s. */
@@ -43,8 +47,11 @@
     for (String name : cfg.getSubsections(TRACKING_ID_TAG)) {
       boolean configValid = true;
 
-      String footer = cfg.getString(TRACKING_ID_TAG, name, FOOTER_TAG);
-      if (footer == null || footer.isEmpty()) {
+      Set<String> footers = new HashSet<>(
+          Arrays.asList(cfg.getStringList(TRACKING_ID_TAG, name, FOOTER_TAG)));
+      footers.removeAll(Collections.singleton(null));
+
+      if (footers.isEmpty()) {
         configValid = false;
         log.error("Missing " + TRACKING_ID_TAG + "." + name + "." + FOOTER_TAG
             + " in gerrit.config");
@@ -71,7 +78,9 @@
 
       if (configValid) {
         try {
-          trackingFooters.add(new TrackingFooter(footer, match, system));
+          for (String footer : footers) {
+            trackingFooters.add(new TrackingFooter(footer, match, system));
+          }
         } catch (PatternSyntaxException e) {
           log.error("Invalid pattern \"" + match + "\" in gerrit.config "
               + TRACKING_ID_TAG + "." + name + "." + REGEX_TAG + ": "
@@ -81,6 +90,7 @@
     }
   }
 
+  @Override
   public TrackingFooters get() {
     return new TrackingFooters(trackingFooters);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreModule.java
similarity index 85%
rename from gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreProvider.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreModule.java
index 835dc40..6b195de 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreModule.java
@@ -14,12 +14,13 @@
 
 package com.google.gerrit.server.contact;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
 import com.google.inject.ProvisionException;
 
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
@@ -35,24 +36,16 @@
 import java.security.Security;
 
 /** Creates the {@link ContactStore} based on the configuration. */
-public class ContactStoreProvider implements Provider<ContactStore> {
-  private final Config config;
-  private final SitePaths site;
-  private final SchemaFactory<ReviewDb> schema;
-  private final ContactStoreConnection.Factory connFactory;
-
-  @Inject
-  ContactStoreProvider(@GerritServerConfig final Config config,
-      final SitePaths site, final SchemaFactory<ReviewDb> schema,
-      final ContactStoreConnection.Factory connFactory) {
-    this.config = config;
-    this.site = site;
-    this.schema = schema;
-    this.connFactory = connFactory;
+public class ContactStoreModule extends AbstractModule {
+  @Override
+  protected void configure() {
   }
 
-  @Override
-  public ContactStore get() {
+  @Nullable
+  @Provides
+  public ContactStore provideContactStore(@GerritServerConfig final Config config,
+      final SitePaths site, final SchemaFactory<ReviewDb> schema,
+      final ContactStoreConnection.Factory connFactory) {
     final String url = config.getString("contactstore", null, "url");
     if (StringUtils.isEmptyOrNull(url)) {
       return new NoContactStore();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java
index f1f374c..4048748 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.contact;
 
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.errors.ContactInformationStoreException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.ContactInformation;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.UrlEncoded;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.ProvisionException;
@@ -110,9 +110,7 @@
     try (InputStream fin = new FileInputStream(pub);
         InputStream in = PGPUtil.getDecoderStream(fin)) {
         return new BcPGPPublicKeyRingCollection(in);
-    } catch (IOException e) {
-      throw new ProvisionException("Cannot read " + pub, e);
-    } catch (PGPException e) {
+    } catch (IOException | PGPException e) {
       throw new ProvisionException("Cannot read " + pub, e);
     }
   }
@@ -130,6 +128,7 @@
     return null;
   }
 
+  @Override
   public void store(final Account account, final ContactInformation info)
       throws ContactInformationStoreException {
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/NoContactStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/NoContactStore.java
index a625c0c..98001bb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/contact/NoContactStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/contact/NoContactStore.java
@@ -18,7 +18,7 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.ContactInformation;
 
-class NoContactStore implements ContactStore {
+public class NoContactStore implements ContactStore {
   @Override
   public boolean isEnabled() {
     return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
index 5f5fd33..8c18514 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
@@ -31,7 +31,6 @@
 
     public Long createdOn;
     public Long lastUpdated;
-    public String sortKey;
     public Boolean open;
     public Change.Status status;
     public List<MessageAttribute> comments;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/data/QueryStatsAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/QueryStatsAttribute.java
index 9897065..4674037 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/data/QueryStatsAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/QueryStatsAttribute.java
@@ -18,5 +18,5 @@
   public final String type = "stats";
   public int rowCount;
   public long runTimeMilliseconds;
-  public String resumeSortKey;
+  public boolean moreChanges;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
index af08d1e..03441e7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
@@ -16,9 +16,11 @@
 
 import static org.pegdown.Extensions.ALL;
 import static org.pegdown.Extensions.HARDWRAPS;
+import static org.pegdown.Extensions.SUPPRESS_ALL_HTML;
 
 import com.google.common.base.Strings;
 
+import org.apache.commons.lang.StringEscapeUtils;
 import org.eclipse.jgit.util.RawParseUtils;
 import org.eclipse.jgit.util.TemporaryBuffer;
 import org.pegdown.LinkRenderer;
@@ -43,7 +45,7 @@
   private static final Logger log =
       LoggerFactory.getLogger(MarkdownFormatter.class);
 
-  private static final String css;
+  private static final String defaultCss;
 
   static {
     AtomicBoolean file = new AtomicBoolean();
@@ -54,12 +56,12 @@
       log.warn("Cannot load pegdown.css", err);
       src = "";
     }
-    css = file.get() ? null : src;
+    defaultCss = file.get() ? null : src;
   }
 
   private static String readCSS() {
-    if (css != null) {
-      return css;
+    if (defaultCss != null) {
+      return defaultCss;
     }
     try {
       return readPegdownCss(new AtomicBoolean());
@@ -69,6 +71,19 @@
     }
   }
 
+  private boolean suppressHtml;
+  private String css;
+
+  public MarkdownFormatter suppressHtml() {
+    suppressHtml = true;
+    return this;
+  }
+
+  public MarkdownFormatter setCss(String css) {
+    this.css = StringEscapeUtils.escapeHtml(css);
+    return this;
+  }
+
   public byte[] markdownToDocHtml(String md, String charEnc)
       throws UnsupportedEncodingException {
     RootNode root = parseMarkdown(md);
@@ -80,9 +95,13 @@
     if (!Strings.isNullOrEmpty(title)) {
       html.append("<title>").append(title).append("</title>");
     }
-    html.append("<style type=\"text/css\">\n")
-        .append(readCSS())
-        .append("\n</style>");
+    html.append("<style type=\"text/css\">\n");
+    if (css != null) {
+      html.append(css);
+    } else {
+      html.append(readCSS());
+    }
+    html.append("\n</style>");
     html.append("</head>");
     html.append("<body>\n");
     html.append(new ToHtmlSerializer(new LinkRenderer()).toHtml(root));
@@ -121,7 +140,11 @@
   }
 
   private RootNode parseMarkdown(String md) {
-    return new PegDownProcessor(ALL & ~(HARDWRAPS))
+    int options = ALL & ~(HARDWRAPS);
+    if (suppressHtml) {
+      options |= SUPPRESS_ALL_HTML;
+    }
+    return new PegDownProcessor(options)
         .parseMarkdown(md.toCharArray());
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
index b734007..188e95b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/QueryDocumentationExecutor.java
@@ -31,7 +31,6 @@
 import org.apache.lucene.store.Directory;
 import org.apache.lucene.store.IndexOutput;
 import org.apache.lucene.store.RAMDirectory;
-import org.apache.lucene.util.Version;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -46,8 +45,6 @@
   private static final Logger log =
       LoggerFactory.getLogger(QueryDocumentationExecutor.class);
 
-  private static final Version LUCENE_VERSION = Version.LUCENE_48;
-
   private IndexSearcher searcher;
   private QueryParser parser;
 
@@ -68,8 +65,7 @@
       }
       IndexReader reader = DirectoryReader.open(dir);
       searcher = new IndexSearcher(reader);
-      StandardAnalyzer analyzer = new StandardAnalyzer(LUCENE_VERSION);
-      parser = new QueryParser(LUCENE_VERSION, Constants.DOC_FIELD, analyzer);
+      parser = new QueryParser(Constants.DOC_FIELD, new StandardAnalyzer());
     } catch (IOException e) {
       log.error("Cannot initialize documentation full text index", e);
       searcher = null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEdit.java
new file mode 100644
index 0000000..b7bb360
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEdit.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.edit;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.IdentifiedUser;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+/**
+ * A single user's edit for a change.
+ * <p>
+ * There is max. one edit per user per change. Edits are stored on refs:
+ * refs/users/UU/UUUU/edit-CCCC/P where UU/UUUU is sharded representation
+ * of user account, CCCC is change number and P is the patch set number it
+ * is based on.
+ */
+public class ChangeEdit {
+  private final IdentifiedUser user;
+  private final Change change;
+  private final Ref ref;
+  private final RevCommit editCommit;
+  private final PatchSet basePatchSet;
+
+  public ChangeEdit(IdentifiedUser user, Change change, Ref ref,
+      RevCommit editCommit, PatchSet basePatchSet) {
+    checkNotNull(user);
+    checkNotNull(change);
+    checkNotNull(ref);
+    checkNotNull(editCommit);
+    checkNotNull(basePatchSet);
+    this.user = user;
+    this.change = change;
+    this.ref = ref;
+    this.editCommit = editCommit;
+    this.basePatchSet = basePatchSet;
+  }
+
+  public Change getChange() {
+    return change;
+  }
+
+  public IdentifiedUser getUser() {
+    return user;
+  }
+
+  public Ref getRef() {
+    return ref;
+  }
+
+  public RevId getRevision() {
+    return new RevId(ObjectId.toString(ref.getObjectId()));
+  }
+
+  public String getRefName() {
+    return RefNames.refsEdit(user.getAccountId(), change.getId(),
+        basePatchSet.getId());
+  }
+
+  public RevCommit getEditCommit() {
+    return editCommit;
+  }
+
+  public PatchSet getBasePatchSet() {
+    return basePatchSet;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditJson.java
new file mode 100644
index 0000000..738d309
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditJson.java
@@ -0,0 +1,107 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.edit;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.common.EditInfo;
+import com.google.gerrit.extensions.common.FetchInfo;
+import com.google.gerrit.extensions.config.DownloadCommand;
+import com.google.gerrit.extensions.config.DownloadScheme;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.server.CommonConverters;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.change.ChangeJson;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import java.util.ArrayList;
+import java.util.Map;
+
+@Singleton
+public class ChangeEditJson {
+  private final DynamicMap<DownloadCommand> downloadCommands;
+  private final DynamicMap<DownloadScheme> downloadSchemes;
+  private final Provider<CurrentUser> userProvider;
+
+  @Inject
+  ChangeEditJson(DynamicMap<DownloadCommand> downloadCommand,
+      DynamicMap<DownloadScheme> downloadSchemes,
+      Provider<CurrentUser> userProvider) {
+    this.downloadCommands = downloadCommand;
+    this.downloadSchemes = downloadSchemes;
+    this.userProvider = userProvider;
+  }
+
+  public EditInfo toEditInfo(ChangeEdit edit, boolean downloadCommands) {
+    EditInfo out = new EditInfo();
+    out.commit = fillCommit(edit.getEditCommit());
+    out.baseRevision = edit.getBasePatchSet().getRevision().get();
+    if (downloadCommands) {
+      out.fetch = fillFetchMap(edit);
+    }
+    return out;
+  }
+
+  private static CommitInfo fillCommit(RevCommit editCommit) {
+    CommitInfo commit = new CommitInfo();
+    commit.commit = editCommit.toObjectId().getName();
+    commit.author = CommonConverters.toGitPerson(editCommit.getAuthorIdent());
+    commit.committer = CommonConverters.toGitPerson(
+        editCommit.getCommitterIdent());
+    commit.subject = editCommit.getShortMessage();
+    commit.message = editCommit.getFullMessage();
+
+    commit.parents = new ArrayList<>(editCommit.getParentCount());
+    for (RevCommit p : editCommit.getParents()) {
+      CommitInfo i = new CommitInfo();
+      i.commit = p.name();
+      commit.parents.add(i);
+    }
+
+    return commit;
+  }
+
+  private Map<String, FetchInfo> fillFetchMap(ChangeEdit edit) {
+    Map<String, FetchInfo> r = Maps.newLinkedHashMap();
+    for (DynamicMap.Entry<DownloadScheme> e : downloadSchemes) {
+      String schemeName = e.getExportName();
+      DownloadScheme scheme = e.getProvider().get();
+      if (!scheme.isEnabled()
+          || (scheme.isAuthRequired()
+              && !userProvider.get().isIdentifiedUser())) {
+        continue;
+      }
+
+      // No fluff, just stuff
+      if (!scheme.isAuthSupported()) {
+        continue;
+      }
+
+      String projectName = edit.getChange().getProject().get();
+      String refName = edit.getRefName();
+      FetchInfo fetchInfo = new FetchInfo(scheme.getUrl(projectName), refName);
+      r.put(schemeName, fetchInfo);
+
+      ChangeJson.populateFetchMap(scheme, downloadCommands, projectName,
+          refName, fetchInfo);
+    }
+
+    return r;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
new file mode 100644
index 0000000..67de914
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -0,0 +1,473 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.edit;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.common.base.Strings;
+import com.google.common.io.ByteStreams;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RawInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEditor;
+import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
+import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.MergeStrategy;
+import org.eclipse.jgit.merge.ThreeWayMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.treewalk.TreeWalk;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.TimeZone;
+
+/**
+ * Utility functions to manipulate change edits.
+ * <p>
+ * This class contains methods to modify edit's content.
+ * For retrieving, publishing and deleting edit see
+ * {@link ChangeEditUtil}.
+ * <p>
+ */
+@Singleton
+public class ChangeEditModifier {
+
+  private static enum TreeOperation {
+    CHANGE_ENTRY,
+    DELETE_ENTRY,
+    RENAME_ENTRY,
+    RESTORE_ENTRY
+  }
+  private final TimeZone tz;
+  private final GitRepositoryManager gitManager;
+  private final Provider<CurrentUser> currentUser;
+
+  @Inject
+  ChangeEditModifier(@GerritPersonIdent PersonIdent gerritIdent,
+      GitRepositoryManager gitManager,
+      Provider<CurrentUser> currentUser) {
+    this.gitManager = gitManager;
+    this.currentUser = currentUser;
+    this.tz = gerritIdent.getTimeZone();
+  }
+
+  /**
+   * Create new change edit.
+   *
+   * @param change to create change edit for
+   * @param ps patch set to create change edit on
+   * @return result
+   * @throws AuthException
+   * @throws IOException
+   * @throws ResourceConflictException When change edit already
+   * exists for the change
+   */
+  public RefUpdate.Result createEdit(Change change, PatchSet ps)
+      throws AuthException, IOException, ResourceConflictException {
+    if (!currentUser.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+
+    IdentifiedUser me = (IdentifiedUser) currentUser.get();
+    String refPrefix = RefNames.refsEditPrefix(me.getAccountId(), change.getId());
+
+    try (Repository repo = gitManager.openRepository(change.getProject())) {
+      Map<String, Ref> refs = repo.getRefDatabase().getRefs(refPrefix);
+      if (!refs.isEmpty()) {
+        throw new ResourceConflictException("edit already exists");
+      }
+
+      try (RevWalk rw = new RevWalk(repo)) {
+        ObjectId revision = ObjectId.fromString(ps.getRevision().get());
+        String editRefName = RefNames.refsEdit(me.getAccountId(), change.getId(),
+            ps.getId());
+        return update(repo, me, editRefName, rw, ObjectId.zeroId(), revision);
+      }
+    }
+  }
+
+  /**
+   * Rebase change edit on latest patch set
+   *
+   * @param edit change edit that contains edit to rebase
+   * @param current patch set to rebase the edit on
+   * @throws AuthException
+   * @throws ResourceConflictException thrown if rebase fails due to merge conflicts
+   * @throws InvalidChangeOperationException
+   * @throws IOException
+   */
+  public void rebaseEdit(ChangeEdit edit, PatchSet current)
+      throws AuthException, ResourceConflictException,
+      InvalidChangeOperationException, IOException {
+    if (!currentUser.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+
+    Change change = edit.getChange();
+    IdentifiedUser me = (IdentifiedUser) currentUser.get();
+    String refName = RefNames.refsEdit(me.getAccountId(), change.getId(),
+        current.getId());
+    try (Repository repo = gitManager.openRepository(change.getProject());
+        RevWalk rw = new RevWalk(repo);
+        ObjectInserter inserter = repo.newObjectInserter()) {
+      BatchRefUpdate ru = repo.getRefDatabase().newBatchUpdate();
+      RevCommit editCommit = edit.getEditCommit();
+      if (editCommit.getParentCount() == 0) {
+        throw new InvalidChangeOperationException(
+            "Rebase edit against root commit not supported");
+      }
+      RevCommit tip = rw.parseCommit(ObjectId.fromString(
+          current.getRevision().get()));
+      ThreeWayMerger m = MergeStrategy.RESOLVE.newMerger(repo, true);
+      m.setObjectInserter(inserter);
+      m.setBase(ObjectId.fromString(
+          edit.getBasePatchSet().getRevision().get()));
+
+      if (m.merge(tip, editCommit)) {
+        ObjectId tree = m.getResultTreeId();
+
+        CommitBuilder commit = new CommitBuilder();
+        commit.setTreeId(tree);
+        for (int i = 0; i < tip.getParentCount(); i++) {
+          commit.addParentId(tip.getParent(i));
+        }
+        commit.setAuthor(editCommit.getAuthorIdent());
+        commit.setCommitter(new PersonIdent(
+            editCommit.getCommitterIdent(), TimeUtil.nowTs()));
+        commit.setMessage(editCommit.getFullMessage());
+        ObjectId newEdit = inserter.insert(commit);
+        inserter.flush();
+
+        ru.addCommand(new ReceiveCommand(ObjectId.zeroId(), newEdit,
+            refName));
+        ru.addCommand(new ReceiveCommand(edit.getRef().getObjectId(),
+            ObjectId.zeroId(), edit.getRefName()));
+        ru.execute(rw, NullProgressMonitor.INSTANCE);
+        for (ReceiveCommand cmd : ru.getCommands()) {
+          if (cmd.getResult() != ReceiveCommand.Result.OK) {
+            throw new IOException("failed: " + cmd);
+          }
+        }
+      } else {
+        // TODO(davido): Allow to resolve conflicts inline
+        throw new ResourceConflictException("merge conflict");
+      }
+    }
+  }
+
+  /**
+   * Modify commit message in existing change edit.
+   *
+   * @param edit change edit
+   * @param msg new commit message
+   * @return result
+   * @throws AuthException
+   * @throws InvalidChangeOperationException
+   * @throws IOException
+   * @throws UnchangedCommitMessageException
+   */
+  public RefUpdate.Result modifyMessage(ChangeEdit edit, String msg)
+      throws AuthException, InvalidChangeOperationException, IOException,
+      UnchangedCommitMessageException {
+    checkState(!Strings.isNullOrEmpty(msg), "message cannot be null");
+    if (!currentUser.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+
+    RevCommit prevEdit = edit.getEditCommit();
+    if (prevEdit.getFullMessage().equals(msg)) {
+      throw new UnchangedCommitMessageException();
+    }
+
+    IdentifiedUser me = (IdentifiedUser) currentUser.get();
+    Project.NameKey project = edit.getChange().getProject();
+    try (Repository repo = gitManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo);
+        ObjectInserter inserter = repo.newObjectInserter()) {
+      String refName = edit.getRefName();
+      ObjectId commit = createCommit(me, inserter, prevEdit,
+          prevEdit.getTree(),
+          msg);
+      inserter.flush();
+      return update(repo, me, refName, rw, prevEdit, commit);
+    }
+  }
+
+  /**
+   * Modify file in existing change edit from its base commit.
+   *
+   * @param edit change edit
+   * @param file path to modify
+   * @param content new content
+   * @return result
+   * @throws AuthException
+   * @throws InvalidChangeOperationException
+   * @throws IOException
+   */
+  public RefUpdate.Result modifyFile(ChangeEdit edit,
+      String file, RawInput content) throws AuthException,
+      InvalidChangeOperationException, IOException {
+    return modify(TreeOperation.CHANGE_ENTRY, edit, file, null, content);
+  }
+
+  /**
+   * Delete file in existing change edit.
+   *
+   * @param edit change edit
+   * @param file path to delete
+   * @return result
+   * @throws AuthException
+   * @throws InvalidChangeOperationException
+   * @throws IOException
+   */
+  public RefUpdate.Result deleteFile(ChangeEdit edit,
+      String file) throws AuthException, InvalidChangeOperationException,
+      IOException {
+    return modify(TreeOperation.DELETE_ENTRY, edit, file, null, null);
+  }
+
+  /**
+   * Rename file in existing change edit.
+   *
+   * @param edit change edit
+   * @param file path to rename
+   * @param newFile path to rename the file to
+   * @return result
+   * @throws AuthException
+   * @throws InvalidChangeOperationException
+   * @throws IOException
+   */
+  public RefUpdate.Result renameFile(ChangeEdit edit, String file,
+      String newFile) throws AuthException, InvalidChangeOperationException,
+      IOException {
+    return modify(TreeOperation.RENAME_ENTRY, edit, file, newFile, null);
+  }
+
+  /**
+   * Restore file in existing change edit.
+   *
+   * @param edit change edit
+   * @param file path to restore
+   * @return result
+   * @throws AuthException
+   * @throws InvalidChangeOperationException
+   * @throws IOException
+   */
+  public RefUpdate.Result restoreFile(ChangeEdit edit,
+      String file) throws AuthException, InvalidChangeOperationException,
+      IOException {
+    return modify(TreeOperation.RESTORE_ENTRY, edit, file, null, null);
+  }
+
+  private RefUpdate.Result modify(TreeOperation op, ChangeEdit edit,
+      String file, @Nullable String newFile, @Nullable RawInput content)
+      throws AuthException, IOException, InvalidChangeOperationException {
+    if (!currentUser.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+    IdentifiedUser me = (IdentifiedUser) currentUser.get();
+    Project.NameKey project = edit.getChange().getProject();
+    try (Repository repo = gitManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo);
+        ObjectInserter inserter = repo.newObjectInserter();
+        ObjectReader reader = repo.newObjectReader()) {
+      String refName = edit.getRefName();
+      RevCommit prevEdit = edit.getEditCommit();
+      ObjectId newTree = writeNewTree(op,
+          rw,
+          inserter,
+          prevEdit,
+          reader,
+          file,
+          newFile,
+          toBlob(inserter, content));
+      if (ObjectId.equals(newTree, prevEdit.getTree())) {
+        throw new InvalidChangeOperationException("no changes were made");
+      }
+
+      ObjectId commit = createCommit(me, inserter, prevEdit, newTree);
+      inserter.flush();
+      return update(repo, me, refName, rw, prevEdit, commit);
+    }
+  }
+
+  private static ObjectId toBlob(ObjectInserter ins, @Nullable RawInput content)
+      throws IOException {
+    if (content == null) {
+      return null;
+    }
+
+    long len = content.getContentLength();
+    InputStream in = content.getInputStream();
+    if (len < 0) {
+      return ins.insert(OBJ_BLOB, ByteStreams.toByteArray(in));
+    }
+    return ins.insert(OBJ_BLOB, len, in);
+  }
+
+  private ObjectId createCommit(IdentifiedUser me, ObjectInserter inserter,
+      RevCommit revision, ObjectId tree) throws IOException {
+    return createCommit(me, inserter, revision, tree,
+        revision.getFullMessage());
+  }
+
+  private ObjectId createCommit(IdentifiedUser me, ObjectInserter inserter,
+      RevCommit revision, ObjectId tree, String msg)
+      throws IOException {
+    CommitBuilder builder = new CommitBuilder();
+    builder.setTreeId(tree);
+    builder.setParentIds(revision.getParents());
+    builder.setAuthor(revision.getAuthorIdent());
+    builder.setCommitter(getCommitterIdent(me));
+    builder.setMessage(msg);
+    return inserter.insert(builder);
+  }
+
+  private RefUpdate.Result update(Repository repo, IdentifiedUser me,
+      String refName, RevWalk rw, ObjectId oldObjectId, ObjectId newEdit)
+      throws IOException {
+    RefUpdate ru = repo.updateRef(refName);
+    ru.setExpectedOldObjectId(oldObjectId);
+    ru.setNewObjectId(newEdit);
+    ru.setRefLogIdent(getRefLogIdent(me));
+    ru.setRefLogMessage("inline edit (amend)", false);
+    ru.setForceUpdate(true);
+    RefUpdate.Result res = ru.update(rw);
+    if (res != RefUpdate.Result.NEW &&
+        res != RefUpdate.Result.FORCED) {
+      throw new IOException("update failed: " + ru);
+    }
+    return res;
+  }
+
+  private static ObjectId writeNewTree(TreeOperation op, RevWalk rw,
+      ObjectInserter ins, RevCommit prevEdit, ObjectReader reader,
+      String fileName, @Nullable String newFile,
+      final @Nullable ObjectId content) throws IOException {
+    DirCache newTree = readTree(reader, prevEdit);
+    DirCacheEditor dce = newTree.editor();
+    switch (op) {
+      case DELETE_ENTRY:
+        dce.add(new DeletePath(fileName));
+        break;
+
+      case RENAME_ENTRY:
+        rw.parseHeaders(prevEdit);
+        TreeWalk tw =
+            TreeWalk.forPath(rw.getObjectReader(), fileName, prevEdit.getTree());
+        if (tw != null) {
+          dce.add(new DeletePath(fileName));
+          addFileToCommit(newFile, dce, tw);
+        }
+        break;
+
+      case CHANGE_ENTRY:
+        checkNotNull(content, "new content required");
+        dce.add(new PathEdit(fileName) {
+          @Override
+          public void apply(DirCacheEntry ent) {
+            if (ent.getRawMode() == 0) {
+              ent.setFileMode(FileMode.REGULAR_FILE);
+            }
+            ent.setObjectId(content);
+          }
+        });
+        break;
+
+      case RESTORE_ENTRY:
+        if (prevEdit.getParentCount() == 0) {
+          dce.add(new DeletePath(fileName));
+          break;
+        }
+
+        RevCommit base = prevEdit.getParent(0);
+        rw.parseHeaders(base);
+        tw = TreeWalk.forPath(rw.getObjectReader(), fileName, base.getTree());
+        if (tw == null) {
+          dce.add(new DeletePath(fileName));
+          break;
+        }
+
+        addFileToCommit(fileName, dce, tw);
+        break;
+    }
+    dce.finish();
+    return newTree.writeTree(ins);
+  }
+
+  private static void addFileToCommit(String newFile, DirCacheEditor dce,
+      TreeWalk tw) {
+    final FileMode mode = tw.getFileMode(0);
+    final ObjectId oid = tw.getObjectId(0);
+    dce.add(new PathEdit(newFile) {
+      @Override
+      public void apply(DirCacheEntry ent) {
+        ent.setFileMode(mode);
+        ent.setObjectId(oid);
+      }
+    });
+  }
+
+  private static DirCache readTree(ObjectReader reader, RevCommit prevEdit)
+      throws IOException {
+    DirCache dc = DirCache.newInCore();
+    DirCacheBuilder b = dc.builder();
+    b.addTree(new byte[0], DirCacheEntry.STAGE_0, reader, prevEdit.getTree());
+    b.finish();
+    return dc;
+  }
+
+  private PersonIdent getCommitterIdent(IdentifiedUser user) {
+    return user.newCommitterIdent(TimeUtil.nowTs(), tz);
+  }
+
+  private PersonIdent getRefLogIdent(IdentifiedUser user) {
+    return user.newRefLogIdent(TimeUtil.nowTs(), tz);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
new file mode 100644
index 0000000..0a65527
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -0,0 +1,267 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.edit;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * Utility functions to manipulate change edits.
+ * <p>
+ * This class contains methods to retrieve, publish and delete edits.
+ * For changing edits see {@link ChangeEditModifier}.
+ */
+@Singleton
+public class ChangeEditUtil {
+  private final GitRepositoryManager gitManager;
+  private final PatchSetInserter.Factory patchSetInserterFactory;
+  private final ChangeControl.GenericFactory changeControlFactory;
+  private final Provider<ReviewDb> db;
+  private final Provider<CurrentUser> user;
+
+  @Inject
+  ChangeEditUtil(GitRepositoryManager gitManager,
+      PatchSetInserter.Factory patchSetInserterFactory,
+      ChangeControl.GenericFactory changeControlFactory,
+      Provider<ReviewDb> db,
+      Provider<CurrentUser> user) {
+    this.gitManager = gitManager;
+    this.patchSetInserterFactory = patchSetInserterFactory;
+    this.changeControlFactory = changeControlFactory;
+    this.db = db;
+    this.user = user;
+  }
+
+  /**
+   * Retrieve edits for a change and user. Max. one change edit can
+   * exist per user and change.
+   *
+   * @param change
+   * @return edit for this change for this user, if present.
+   * @throws AuthException
+   * @throws IOException
+   */
+  public Optional<ChangeEdit> byChange(Change change)
+      throws AuthException, IOException {
+    CurrentUser currentUser = user.get();
+    if (!currentUser.isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+    return byChange(change, (IdentifiedUser)currentUser);
+  }
+
+  /**
+   * Retrieve edits for a change and user. Max. one change edit can
+   * exist per user and change.
+   *
+   * @param change
+   * @param user to retrieve change edits for
+   * @return edit for this change for this user, if present.
+   * @throws IOException
+   */
+  public Optional<ChangeEdit> byChange(Change change, IdentifiedUser user)
+      throws IOException {
+    try (Repository repo = gitManager.openRepository(change.getProject())) {
+      String editRefPrefix = RefNames.refsEditPrefix(user.getAccountId(), change.getId());
+      Map<String, Ref> refs = repo.getRefDatabase().getRefs(editRefPrefix);
+      if (refs.isEmpty()) {
+        return Optional.absent();
+      }
+
+      // TODO(davido): Rather than failing when we encounter the corrupt state
+      // where there is more than one ref, we could silently delete all but the
+      // current one.
+      Ref ref = Iterables.getOnlyElement(refs.values());
+      try (RevWalk rw = new RevWalk(repo)) {
+        RevCommit commit = rw.parseCommit(ref.getObjectId());
+        PatchSet basePs = getBasePatchSet(change, ref);
+        return Optional.of(new ChangeEdit(user, change, ref, commit, basePs));
+      }
+    }
+  }
+
+  /**
+   * Promote change edit to patch set, by squashing the edit into
+   * its parent.
+   *
+   * @param edit change edit to publish
+   * @throws AuthException
+   * @throws NoSuchChangeException
+   * @throws IOException
+   * @throws InvalidChangeOperationException
+   * @throws OrmException
+   * @throws ResourceConflictException
+   */
+  public void publish(ChangeEdit edit) throws AuthException,
+      NoSuchChangeException, IOException, InvalidChangeOperationException,
+      OrmException, ResourceConflictException {
+    Change change = edit.getChange();
+    try (Repository repo = gitManager.openRepository(change.getProject());
+        RevWalk rw = new RevWalk(repo);
+        ObjectInserter inserter = repo.newObjectInserter()) {
+      PatchSet basePatchSet = edit.getBasePatchSet();
+      if (!basePatchSet.getId().equals(change.currentPatchSetId())) {
+        throw new ResourceConflictException(
+            "only edit for current patch set can be published");
+      }
+
+      insertPatchSet(edit, change, repo, rw, basePatchSet,
+          squashEdit(rw, inserter, edit.getEditCommit(), basePatchSet));
+      // TODO(davido): This should happen in the same BatchRefUpdate.
+      deleteRef(repo, edit);
+    }
+  }
+
+  /**
+   * Delete change edit.
+   *
+   * @param edit change edit to delete
+   * @throws IOException
+   */
+  public void delete(ChangeEdit edit)
+      throws IOException {
+    Change change = edit.getChange();
+    Repository repo = gitManager.openRepository(change.getProject());
+    try {
+      deleteRef(repo, edit);
+    } finally {
+      repo.close();
+    }
+  }
+
+  private PatchSet getBasePatchSet(Change change, Ref ref)
+      throws IOException {
+    try {
+      int pos = ref.getName().lastIndexOf("/");
+      checkArgument(pos > 0, "invalid edit ref: %s", ref.getName());
+      String psId = ref.getName().substring(pos + 1);
+      return db.get().patchSets().get(new PatchSet.Id(
+          change.getId(), Integer.valueOf(psId)));
+    } catch (OrmException e) {
+      throw new IOException(e);
+    }
+  }
+
+  private RevCommit squashEdit(RevWalk rw, ObjectInserter inserter,
+      RevCommit edit, PatchSet basePatchSet)
+      throws IOException, ResourceConflictException {
+    RevCommit parent = rw.parseCommit(ObjectId.fromString(
+        basePatchSet.getRevision().get()));
+    if (parent.getTree().equals(edit.getTree())
+        && edit.getFullMessage().equals(parent.getFullMessage())) {
+      throw new ResourceConflictException("identical tree and message");
+    }
+    return writeSquashedCommit(rw, inserter, parent, edit);
+  }
+
+  private void insertPatchSet(ChangeEdit edit, Change change,
+      Repository repo, RevWalk rw, PatchSet basePatchSet, RevCommit squashed)
+      throws NoSuchChangeException, InvalidChangeOperationException,
+      OrmException, IOException {
+    PatchSet ps = new PatchSet(
+        ChangeUtil.nextPatchSetId(change.currentPatchSetId()));
+    ps.setRevision(new RevId(ObjectId.toString(squashed)));
+    ps.setUploader(edit.getUser().getAccountId());
+    ps.setCreatedOn(TimeUtil.nowTs());
+
+    PatchSetInserter insr =
+        patchSetInserterFactory.create(repo, rw,
+            changeControlFactory.controlFor(change, edit.getUser()),
+            squashed);
+    insr.setPatchSet(ps)
+        .setDraft(change.getStatus() == Status.DRAFT ||
+            basePatchSet.isDraft())
+        .setMessage(
+            String.format("Patch Set %d: Published edit on patch set %d",
+                ps.getPatchSetId(),
+                basePatchSet.getPatchSetId()))
+        .insert();
+  }
+
+  private static void deleteRef(Repository repo, ChangeEdit edit)
+      throws IOException {
+    String refName = edit.getRefName();
+    RefUpdate ru = repo.updateRef(refName, true);
+    ru.setExpectedOldObjectId(edit.getRef().getObjectId());
+    ru.setForceUpdate(true);
+    RefUpdate.Result result = ru.delete();
+    switch (result) {
+      case FORCED:
+      case NEW:
+      case NO_CHANGE:
+        break;
+      default:
+        throw new IOException(String.format("Failed to delete ref %s: %s",
+            refName, result));
+    }
+  }
+
+  private static RevCommit writeSquashedCommit(RevWalk rw,
+      ObjectInserter inserter, RevCommit parent, RevCommit edit)
+      throws IOException {
+    CommitBuilder mergeCommit = new CommitBuilder();
+    for (int i = 0; i < parent.getParentCount(); i++) {
+      mergeCommit.addParentId(parent.getParent(i));
+    }
+    mergeCommit.setAuthor(parent.getAuthorIdent());
+    mergeCommit.setMessage(edit.getFullMessage());
+    mergeCommit.setCommitter(edit.getCommitterIdent());
+    mergeCommit.setTreeId(edit.getTree());
+
+    return rw.parseCommit(commit(inserter, mergeCommit));
+  }
+
+  private static ObjectId commit(ObjectInserter inserter,
+      CommitBuilder mergeCommit) throws IOException {
+    ObjectId id = inserter.insert(mergeCommit);
+    inserter.flush();
+    return id;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PathConflictException.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/UnchangedCommitMessageException.java
similarity index 68%
rename from gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PathConflictException.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/edit/UnchangedCommitMessageException.java
index 7e2f2d7..f405f8b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PathConflictException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/UnchangedCommitMessageException.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2014 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,13 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.changedetail;
+package com.google.gerrit.server.edit;
 
-/** Indicates a path conflict during rebase or merge */
-public class PathConflictException extends Exception {
+public class UnchangedCommitMessageException extends Exception {
   private static final long serialVersionUID = 1L;
 
-  public PathConflictException(String msg) {
-    super(msg);
+  public UnchangedCommitMessageException() {
+    super("New commit message cannot be same as existing commit message");
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java
index b0eb9c6..c285a82 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAbandonedEvent.java
@@ -15,13 +15,12 @@
 package com.google.gerrit.server.events;
 
 import com.google.gerrit.server.data.AccountAttribute;
-import com.google.gerrit.server.data.ChangeAttribute;
-import com.google.gerrit.server.data.PatchSetAttribute;
 
-public class ChangeAbandonedEvent extends ChangeEvent {
-    public final String type = "change-abandoned";
-    public ChangeAttribute change;
-    public PatchSetAttribute patchSet;
-    public AccountAttribute abandoner;
-    public String reason;
+public class ChangeAbandonedEvent extends PatchSetEvent {
+  public AccountAttribute abandoner;
+  public String reason;
+
+  public ChangeAbandonedEvent() {
+    super("change-abandoned");
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeEvent.java
index 904a0a0..9a5ad82 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeEvent.java
@@ -14,5 +14,30 @@
 
 package com.google.gerrit.server.events;
 
-public abstract class ChangeEvent {
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.data.ChangeAttribute;
+
+public abstract class ChangeEvent extends RefEvent {
+  public ChangeAttribute change;
+
+  protected ChangeEvent(String type) {
+    super(type);
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
+
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.java
index 38996a5..41a95cb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeMergedEvent.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.server.events;
 
 import com.google.gerrit.server.data.AccountAttribute;
-import com.google.gerrit.server.data.ChangeAttribute;
-import com.google.gerrit.server.data.PatchSetAttribute;
 
-public class ChangeMergedEvent extends ChangeEvent {
-    public final String type = "change-merged";
-    public ChangeAttribute change;
-    public PatchSetAttribute patchSet;
-    public AccountAttribute submitter;
+public class ChangeMergedEvent extends PatchSetEvent {
+  public AccountAttribute submitter;
+  public String newRev;
+
+  public ChangeMergedEvent() {
+    super("change-merged");
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
index e761190..a575a42 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
@@ -15,13 +15,12 @@
 package com.google.gerrit.server.events;
 
 import com.google.gerrit.server.data.AccountAttribute;
-import com.google.gerrit.server.data.ChangeAttribute;
-import com.google.gerrit.server.data.PatchSetAttribute;
 
-public class ChangeRestoredEvent extends ChangeEvent {
-    public final String type = "change-restored";
-    public ChangeAttribute change;
-    public PatchSetAttribute patchSet;
-    public AccountAttribute restorer;
-    public String reason;
+public class ChangeRestoredEvent extends PatchSetEvent {
+  public AccountAttribute restorer;
+  public String reason;
+
+  public ChangeRestoredEvent () {
+    super("change-restored");
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java
index 52d7409..4391dfb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommentAddedEvent.java
@@ -16,14 +16,13 @@
 
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.ApprovalAttribute;
-import com.google.gerrit.server.data.ChangeAttribute;
-import com.google.gerrit.server.data.PatchSetAttribute;
 
-public class CommentAddedEvent extends ChangeEvent {
-    public final String type = "comment-added";
-    public ChangeAttribute change;
-    public PatchSetAttribute patchSet;
-    public AccountAttribute author;
-    public ApprovalAttribute[] approvals;
-    public String comment;
+public class CommentAddedEvent extends PatchSetEvent {
+  public AccountAttribute author;
+  public ApprovalAttribute[] approvals;
+  public String comment;
+
+  public CommentAddedEvent() {
+    super("comment-added");
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/CommitReceivedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommitReceivedEvent.java
index 8dd1084..8843dbb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/CommitReceivedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommitReceivedEvent.java
@@ -20,19 +20,34 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.ReceiveCommand;
 
-public class CommitReceivedEvent extends ChangeEvent {
-  public final ReceiveCommand command;
-  public final Project project;
-  public final String refName;
-  public final RevCommit commit;
-  public final IdentifiedUser user;
+public class CommitReceivedEvent extends RefEvent {
+  public ReceiveCommand command;
+  public Project project;
+  public String refName;
+  public RevCommit commit;
+  public IdentifiedUser user;
+
+  public CommitReceivedEvent() {
+    super("commit-received");
+  }
 
   public CommitReceivedEvent(ReceiveCommand command, Project project,
       String refName, RevCommit commit, IdentifiedUser user) {
+    this();
     this.command = command;
     this.project = project;
     this.refName = refName;
     this.commit = commit;
     this.user = user;
   }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return project.getNameKey();
+  }
+
+  @Override
+  public String getRefName() {
+    return refName;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java
index 7fd033a..5db628f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java
@@ -15,12 +15,11 @@
 package com.google.gerrit.server.events;
 
 import com.google.gerrit.server.data.AccountAttribute;
-import com.google.gerrit.server.data.ChangeAttribute;
-import com.google.gerrit.server.data.PatchSetAttribute;
 
-public class DraftPublishedEvent extends ChangeEvent {
-    public final String type = "draft-published";
-    public ChangeAttribute change;
-    public PatchSetAttribute patchSet;
-    public AccountAttribute uploader;
+public class DraftPublishedEvent extends PatchSetEvent {
+  public AccountAttribute uploader;
+
+  public DraftPublishedEvent() {
+    super("draft-published");
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/Event.java
similarity index 65%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/events/Event.java
index 407b7c7..20fbe2f 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/Event.java
@@ -12,10 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.server.events;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+import com.google.gerrit.common.TimeUtil;
+
+public abstract class Event {
+  public final String type;
+  public long eventCreatedOn = TimeUtil.nowMs() / 1000L;
+
+  protected Event(String type) {
+    this.type = type;
+  }
+
+  public String getType() {
+    return type;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventDeserializer.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventDeserializer.java
new file mode 100644
index 0000000..3508acf
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventDeserializer.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.events;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+
+import java.lang.reflect.Type;
+
+/**
+ * JSON deserializer for {@link Event}s.
+ * <p>
+ * Deserialized objects are of an appropriate subclass based on the value of the
+ * top-level "type" element.
+ */
+public class EventDeserializer implements JsonDeserializer<Event> {
+  @Override
+  public Event deserialize(JsonElement json, Type typeOfT,
+      JsonDeserializationContext context) throws JsonParseException {
+    if (!json.isJsonObject()) {
+      throw new JsonParseException("Not an object");
+    }
+    JsonElement typeJson = json.getAsJsonObject().get("type");
+    if (typeJson == null || !typeJson.isJsonPrimitive()
+        || !typeJson.getAsJsonPrimitive().isString()) {
+      throw new JsonParseException("Type is not a string: " + typeJson);
+    }
+    String type = typeJson.getAsJsonPrimitive().getAsString();
+    Class<?> cls = EventTypes.getClass(type);
+    if (cls == null) {
+      throw new JsonParseException("Unknown event type: " + type);
+    }
+    return context.deserialize(json, cls);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index e72d465..f0c0bc1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -164,7 +164,6 @@
   public void extend(ChangeAttribute a, Change change) {
     a.createdOn = change.getCreatedOn().getTime() / 1000L;
     a.lastUpdated = change.getLastUpdatedOn().getTime() / 1000L;
-    a.sortKey = change.getSortKey();
     a.open = change.getStatus().isOpen();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java
new file mode 100644
index 0000000..908fd0a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventTypes.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.events;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Class for registering event types */
+public class EventTypes {
+  private static final Map<String, Class<?>> typesByString = new HashMap<>();
+
+  static {
+    registerClass(new ChangeAbandonedEvent());
+    registerClass(new ChangeMergedEvent());
+    registerClass(new ChangeRestoredEvent());
+    registerClass(new CommentAddedEvent());
+    registerClass(new CommitReceivedEvent());
+    registerClass(new DraftPublishedEvent());
+    registerClass(new HashtagsChangedEvent());
+    registerClass(new MergeFailedEvent());
+    registerClass(new RefUpdatedEvent());
+    registerClass(new RefReceivedEvent());
+    registerClass(new ReviewerAddedEvent());
+    registerClass(new PatchSetCreatedEvent());
+    registerClass(new TopicChangedEvent());
+  }
+
+  /** Register an event.
+   *
+   *  @param event The event to register.
+   *  registered.
+   **/
+  public static void registerClass(Event event) {
+    String type = event.getType();
+    typesByString.put(type, event.getClass());
+  }
+
+  /** Get the class for an event type.
+   *
+   * @param type The type.
+   * @return The event class, or null if no class is registered with the
+   * given type
+   **/
+  public static Class<?> getClass(String type) {
+    return typesByString.get(type);
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/HashtagsChangedEvent.java
similarity index 63%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/events/HashtagsChangedEvent.java
index 407b7c7..c5919e5 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/HashtagsChangedEvent.java
@@ -12,10 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.server.events;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
+import com.google.gerrit.server.data.AccountAttribute;
+
+public class HashtagsChangedEvent extends ChangeEvent {
+  public AccountAttribute editor;
+  public String[] added;
+  public String[] removed;
+  public String[] hashtags;
+
+  public HashtagsChangedEvent () {
+    super("hashtags-changed");
+  }
 }
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
index 599fe60..75cbcb0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
@@ -15,13 +15,12 @@
 package com.google.gerrit.server.events;
 
 import com.google.gerrit.server.data.AccountAttribute;
-import com.google.gerrit.server.data.ChangeAttribute;
-import com.google.gerrit.server.data.PatchSetAttribute;
 
-public class MergeFailedEvent extends ChangeEvent {
-    public final String type = "merge-failed";
-    public ChangeAttribute change;
-    public PatchSetAttribute patchSet;
-    public AccountAttribute submitter;
-    public String reason;
+public class MergeFailedEvent extends PatchSetEvent {
+  public AccountAttribute submitter;
+  public String reason;
+
+  public MergeFailedEvent() {
+    super("merge-failed");
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java
index fbaf4ef..e468593 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCreatedEvent.java
@@ -15,12 +15,11 @@
 package com.google.gerrit.server.events;
 
 import com.google.gerrit.server.data.AccountAttribute;
-import com.google.gerrit.server.data.ChangeAttribute;
-import com.google.gerrit.server.data.PatchSetAttribute;
 
-public class PatchSetCreatedEvent extends ChangeEvent {
-    public final String type = "patchset-created";
-    public ChangeAttribute change;
-    public PatchSetAttribute patchSet;
-    public AccountAttribute uploader;
+public class PatchSetCreatedEvent extends PatchSetEvent {
+  public AccountAttribute uploader;
+
+  public PatchSetCreatedEvent() {
+    super("patchset-created");
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetEvent.java
similarity index 70%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetEvent.java
index 407b7c7..cdaf601 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetEvent.java
@@ -12,10 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.server.events;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+import com.google.gerrit.server.data.PatchSetAttribute;
+
+public class PatchSetEvent extends ChangeEvent {
+  public PatchSetAttribute patchSet;
+
+  protected PatchSetEvent(String type) {
+    super(type);
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ProjectEvent.java
similarity index 69%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/events/ProjectEvent.java
index 407b7c7..cba8e90 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ProjectEvent.java
@@ -12,10 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.server.events;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+import com.google.gerrit.reviewdb.client.Project;
+
+public abstract class ProjectEvent extends Event {
+  protected ProjectEvent(String type) {
+    super(type);
+  }
+
+  public abstract Project.NameKey getProjectNameKey();
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefEvent.java
similarity index 75%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/events/RefEvent.java
index 407b7c7..646bb96 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefEvent.java
@@ -12,10 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.server.events;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+public abstract class RefEvent extends ProjectEvent {
+  protected RefEvent(String type) {
+    super(type);
+  }
+
+  public abstract String getRefName();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefReceivedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefReceivedEvent.java
new file mode 100644
index 0000000..38f4442
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefReceivedEvent.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.server.events;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.IdentifiedUser;
+
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+public class RefReceivedEvent extends RefEvent {
+  public ReceiveCommand command;
+  public Project project;
+  public IdentifiedUser user;
+
+  public RefReceivedEvent() {
+    super("ref-received");
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return project.getNameKey();
+  }
+
+  @Override
+  public String getRefName() {
+    return command.getRefName();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java
index 944c9ad..e5039ff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java
@@ -14,11 +14,25 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.RefUpdateAttribute;
 
-public class RefUpdatedEvent extends ChangeEvent {
-  public final String type = "ref-updated";
+public class RefUpdatedEvent extends RefEvent {
   public AccountAttribute submitter;
   public RefUpdateAttribute refUpdate;
+
+  public RefUpdatedEvent() {
+    super("ref-updated");
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(refUpdate.project);
+  }
+
+  @Override
+  public String getRefName() {
+    return refUpdate.refName;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
index e00cc60..b016bd9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
@@ -15,12 +15,11 @@
 package com.google.gerrit.server.events;
 
 import com.google.gerrit.server.data.AccountAttribute;
-import com.google.gerrit.server.data.ChangeAttribute;
-import com.google.gerrit.server.data.PatchSetAttribute;
 
-public class ReviewerAddedEvent extends ChangeEvent {
-    public final String type = "reviewer-added";
-    public ChangeAttribute change;
-    public PatchSetAttribute patchSet;
-    public AccountAttribute reviewer;
+public class ReviewerAddedEvent extends PatchSetEvent {
+  public AccountAttribute reviewer;
+
+  public ReviewerAddedEvent() {
+    super("reviewer-added");
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/TopicChangedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/TopicChangedEvent.java
index e725eac..7bb334f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/TopicChangedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/TopicChangedEvent.java
@@ -15,11 +15,12 @@
 package com.google.gerrit.server.events;
 
 import com.google.gerrit.server.data.AccountAttribute;
-import com.google.gerrit.server.data.ChangeAttribute;
 
 public class TopicChangedEvent extends ChangeEvent {
-  public final String type = "topic-changed";
-  public ChangeAttribute change;
   public AccountAttribute changer;
   public String oldTopic;
-}
\ No newline at end of file
+
+  public TopicChangedEvent() {
+    super("topic-changed");
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
index 9e48e81..5327448 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
@@ -19,8 +19,10 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.transport.ReceiveCommand;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -63,6 +65,14 @@
     }
   }
 
+  public void fire(Project.NameKey project, BatchRefUpdate batchRefUpdate) {
+    for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
+      if (cmd.getResult() == ReceiveCommand.Result.OK) {
+        fire(project, cmd.getRefName(), cmd.getOldId(), cmd.getNewId());
+      }
+    }
+  }
+
   private static class Event implements GitReferenceUpdatedListener.Event {
     private final String projectName;
     private final String ref;
@@ -96,5 +106,11 @@
     public String getNewObjectId() {
       return newObjectId;
     }
+
+    @Override
+    public String toString() {
+      return String.format("%s[%s,%s: %s -> %s]", getClass().getSimpleName(),
+          projectName, ref, oldObjectId, newObjectId);
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java
index a9f161e..5952568 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java
@@ -20,13 +20,13 @@
 import com.google.gerrit.server.git.WorkQueue.Executor;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.FactoryModuleBuilder;
 import com.google.inject.Inject;
-import com.google.inject.name.Named;
 import com.google.inject.PrivateModule;
 import com.google.inject.Provides;
 import com.google.inject.Singleton;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+import com.google.inject.name.Named;
 
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
@@ -166,7 +166,7 @@
       log.warn(String.format(
           "Error in ReceiveCommits while processing changes for project %s",
               rc.getProject().getName()), e);
-      rc.addError("internal error while processing changes " + e.getMessage());
+      rc.addError("internal error while processing changes");
       // ReceiveCommits has tried its best to catch errors, so anything at this
       // point is very bad.
       for (final ReceiveCommand c : commands) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
index 7ff91ef..d8e56b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
@@ -48,15 +48,15 @@
 
 @Singleton
 public class BanCommit {
-
   /**
    * Loads a list of commits to reject from {@code refs/meta/reject-commits}.
    *
    * @param repo repository from which the rejected commits should be loaded
+   * @param walk open revwalk on repo.
    * @return NoteMap of commits to be rejected, null if there are none.
    * @throws IOException the map cannot be loaded.
    */
-  public static NoteMap loadRejectCommitsMap(Repository repo)
+  public static NoteMap loadRejectCommitsMap(Repository repo, RevWalk walk)
       throws IOException {
     try {
       Ref ref = repo.getRef(RefNames.REFS_REJECT_COMMITS);
@@ -64,13 +64,8 @@
         return NoteMap.newEmptyMap();
       }
 
-      RevWalk rw = new RevWalk(repo);
-      try {
-        RevCommit map = rw.parseCommit(ref.getObjectId());
-        return NoteMap.read(rw.getObjectReader(), map);
-      } finally {
-        rw.release();
-      }
+      RevCommit map = walk.parseCommit(ref.getObjectId());
+      return NoteMap.read(walk.getObjectReader(), map);
     } catch (IOException badMap) {
       throw new IOException("Cannot load " + RefNames.REFS_REJECT_COMMITS,
           badMap);
@@ -79,7 +74,7 @@
 
   private final Provider<IdentifiedUser> currentUser;
   private final GitRepositoryManager repoManager;
-  private final PersonIdent gerritIdent;
+  private final TimeZone tz;
   private NotesBranchUtil.Factory notesBranchUtilFactory;
 
   @Inject
@@ -89,60 +84,55 @@
       final NotesBranchUtil.Factory notesBranchUtilFactory) {
     this.currentUser = currentUser;
     this.repoManager = repoManager;
-    this.gerritIdent = gerritIdent;
     this.notesBranchUtilFactory = notesBranchUtilFactory;
+    this.tz = gerritIdent.getTimeZone();
   }
 
   public BanCommitResult ban(final ProjectControl projectControl,
       final List<ObjectId> commitsToBan, final String reason)
-      throws PermissionDeniedException, IOException, InterruptedException,
-      MergeException, ConcurrentRefUpdateException {
+      throws PermissionDeniedException, IOException,
+      ConcurrentRefUpdateException {
     if (!projectControl.isOwner()) {
       throw new PermissionDeniedException(
-          "No project owner: not permitted to ban commits");
+          "Not project owner: not permitted to ban commits");
     }
 
     final BanCommitResult result = new BanCommitResult();
     NoteMap banCommitNotes = NoteMap.newEmptyMap();
-    // add a note for each banned commit to notes
+    // Add a note for each banned commit to notes.
     final Project.NameKey project = projectControl.getProject().getNameKey();
-    final Repository repo = repoManager.openRepository(project);
-    try {
-      final RevWalk revWalk = new RevWalk(repo);
-      final ObjectInserter inserter = repo.newObjectInserter();
-      try {
-        for (final ObjectId commitToBan : commitsToBan) {
-          try {
-            revWalk.parseCommit(commitToBan);
-          } catch (MissingObjectException e) {
-            // ignore exception, also not existing commits can be banned
-          } catch (IncorrectObjectTypeException e) {
-            result.notACommit(commitToBan, e.getMessage());
-            continue;
-          }
-          banCommitNotes.set(commitToBan, createNoteContent(reason, inserter));
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk revWalk = new RevWalk(repo);
+        ObjectInserter inserter = repo.newObjectInserter()) {
+      ObjectId noteId = null;
+      for (final ObjectId commitToBan : commitsToBan) {
+        try {
+          revWalk.parseCommit(commitToBan);
+        } catch (MissingObjectException e) {
+          // Ignore exception, non-existing commits can be banned.
+        } catch (IncorrectObjectTypeException e) {
+          result.notACommit(commitToBan);
+          continue;
         }
-        inserter.flush();
-        NotesBranchUtil notesBranchUtil =
-            notesBranchUtilFactory.create(project, repo, inserter);
-        NoteMap newlyCreated =
-            notesBranchUtil.commitNewNotes(banCommitNotes, REFS_REJECT_COMMITS,
-                createPersonIdent(), buildCommitMessage(commitsToBan, reason));
-
-        for (Note n : banCommitNotes) {
-          if (newlyCreated.contains(n)) {
-            result.commitBanned(n);
-          } else {
-            result.commitAlreadyBanned(n);
-          }
+        if (noteId == null) {
+          noteId = createNoteContent(reason, inserter);
         }
-        return result;
-      } finally {
-        revWalk.release();
-        inserter.release();
+        banCommitNotes.set(commitToBan, noteId);
       }
-    } finally {
-      repo.close();
+      NotesBranchUtil notesBranchUtil =
+          notesBranchUtilFactory.create(project, repo, inserter);
+      NoteMap newlyCreated =
+          notesBranchUtil.commitNewNotes(banCommitNotes, REFS_REJECT_COMMITS,
+              createPersonIdent(), buildCommitMessage(commitsToBan, reason));
+
+      for (Note n : banCommitNotes) {
+        if (newlyCreated.contains(n)) {
+          result.commitBanned(n);
+        } else {
+          result.commitAlreadyBanned(n);
+        }
+      }
+      return result;
     }
   }
 
@@ -157,7 +147,6 @@
 
   private PersonIdent createPersonIdent() {
     Date now = new Date();
-    TimeZone tz = gerritIdent.getTimeZone();
     return currentUser.get().newCommitterIdent(now, tz);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java
index baae629..92c3b6c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java
@@ -16,17 +16,13 @@
 
 import org.eclipse.jgit.lib.ObjectId;
 
-import java.util.LinkedList;
+import java.util.ArrayList;
 import java.util.List;
 
 public class BanCommitResult {
-
-  private final List<ObjectId> newlyBannedCommits = new LinkedList<>();
-  private final List<ObjectId> alreadyBannedCommits = new LinkedList<>();
-  private final List<ObjectId> ignoredObjectIds = new LinkedList<>();
-
-  public BanCommitResult() {
-  }
+  private final List<ObjectId> newlyBannedCommits = new ArrayList<>(4);
+  private final List<ObjectId> alreadyBannedCommits = new ArrayList<>(4);
+  private final List<ObjectId> ignoredObjectIds = new ArrayList<>(4);
 
   public void commitBanned(final ObjectId commitId) {
     newlyBannedCommits.add(commitId);
@@ -36,7 +32,7 @@
     alreadyBannedCommits.add(commitId);
   }
 
-  public void notACommit(final ObjectId id, final String message) {
+  public void notACommit(final ObjectId id) {
     ignoredObjectIds.add(id);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCache.java
index eb4acfc..391ccd0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCache.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2015 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,86 +14,11 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
-import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.cache.CacheModule;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.Module;
-import com.google.inject.Singleton;
-import com.google.inject.TypeLiteral;
-import com.google.inject.name.Named;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.Collections;
 import java.util.List;
-import java.util.concurrent.ExecutionException;
 
-@Singleton
-public class ChangeCache implements GitReferenceUpdatedListener {
-  private static final Logger log =
-      LoggerFactory.getLogger(ChangeCache.class);
-  private static final String ID_CACHE = "changes";
-
-  public static Module module() {
-    return new CacheModule() {
-      @Override
-      protected void configure() {
-        cache(ID_CACHE,
-            Project.NameKey.class,
-            new TypeLiteral<List<Change>>() {})
-          .maximumWeight(0)
-          .loader(Loader.class);
-      }
-    };
-  }
-
-  private final LoadingCache<Project.NameKey, List<Change>> cache;
-
-  @Inject
-  ChangeCache(@Named(ID_CACHE) LoadingCache<Project.NameKey, List<Change>> cache) {
-    this.cache = cache;
-  }
-
-  List<Change> get(Project.NameKey name) {
-    try {
-      return cache.get(name);
-    } catch (ExecutionException e) {
-      log.warn("Cannot fetch changes for " + name, e);
-      return Collections.emptyList();
-    }
-  }
-
-  @Override
-  public void onGitReferenceUpdated(GitReferenceUpdatedListener.Event event) {
-    if (event.getRefName().startsWith(RefNames.REFS_CHANGES)) {
-      cache.invalidate(new Project.NameKey(event.getProjectName()));
-    }
-  }
-
-  static class Loader extends CacheLoader<Project.NameKey, List<Change>> {
-    private final SchemaFactory<ReviewDb> schema;
-
-    @Inject
-    Loader(SchemaFactory<ReviewDb> schema) {
-      this.schema = schema;
-    }
-
-    @Override
-    public List<Change> load(Project.NameKey key) throws Exception {
-      final ReviewDb db = schema.open();
-      try {
-        return Collections.unmodifiableList(db.changes().byProject(key).toList());
-      } finally {
-        db.close();
-      }
-    }
-  }
+public interface ChangeCache {
+  public List<Change> get(Project.NameKey name);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
index e386bc5..be3902c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
@@ -17,6 +17,7 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -27,7 +28,6 @@
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
index 99009cd..1ff37b0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.common.base.Function;
+import com.google.common.collect.Ordering;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -21,12 +23,41 @@
 
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.util.List;
 
 /** Extended commit entity with code review specific metadata. */
 public class CodeReviewCommit extends RevCommit {
+  /**
+   * Default ordering when merging multiple topologically-equivalent commits.
+   * <p>
+   * Operates only on these commits and does not take ancestry into account.
+   * <p>
+   * Use this in preference to the default order, which comes from {@link
+   * AnyObjectId} and only orders on SHA-1.
+   */
+  public static final Ordering<CodeReviewCommit> ORDER = Ordering.natural()
+      .onResultOf(new Function<CodeReviewCommit, Integer>() {
+        @Override
+        public Integer apply(CodeReviewCommit in) {
+          return in.getPatchsetId() != null
+              ? in.getPatchsetId().getParentKey().get()
+              : null;
+        }
+      }).nullsFirst();
+
+  public static RevWalk newRevWalk(Repository repo) {
+    return new CodeReviewRevWalk(repo);
+  }
+
+  public static RevWalk newRevWalk(ObjectReader reader) {
+    return new CodeReviewRevWalk(reader);
+  }
+
   static CodeReviewCommit revisionGone(ChangeControl ctl) {
     return error(ctl, CommitMergeStatus.REVISION_GONE);
   }
@@ -43,7 +74,7 @@
    * #setStatusCode(CommitMergeStatus)}, enumerated in the methods above.
    *
    * @param ctl control for change that caused this error
-   * @param CommitMergeStatus status
+   * @param s status
    * @return new commit instance
    */
   private static CodeReviewCommit error(ChangeControl ctl,
@@ -54,6 +85,21 @@
     return r;
   }
 
+  private static class CodeReviewRevWalk extends RevWalk {
+    private CodeReviewRevWalk(Repository repo) {
+      super(repo);
+    }
+
+    private CodeReviewRevWalk(ObjectReader reader) {
+      super(reader);
+    }
+
+    @Override
+    protected RevCommit createCommit(AnyObjectId id) {
+      return new CodeReviewCommit(id);
+    }
+  }
+
   /**
    * Unique key of the PatchSet entity from the code review system.
    * <p>
@@ -66,13 +112,6 @@
   private ChangeControl control;
 
   /**
-   * Ordinal position of this commit within the submit queue.
-   * <p>
-   * Only valid if {@link #patchsetId} is not null.
-   */
-  int originalOrder;
-
-  /**
    * The result status for this commit.
    * <p>
    * Only valid if {@link #patchsetId} is not null.
@@ -109,7 +148,6 @@
   public void copyFrom(final CodeReviewCommit src) {
     control = src.control;
     patchsetId = src.patchsetId;
-    originalOrder = src.originalOrder;
     statusCode = src.statusCode;
     missing = src.missing;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
index f421dcb..3d3d9b1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
@@ -28,11 +28,17 @@
   ALREADY_MERGED(""),
 
   /** */
-  PATH_CONFLICT("The change could not be merged due to a path conflict.\n"
+  PATH_CONFLICT("Change could not be merged due to a path conflict.\n"
                   + "\n"
                   + "Please rebase the change locally and upload the rebased commit for review."),
 
   /** */
+  REBASE_MERGE_CONFLICT(
+      "Change could not be merged due to a conflict.\n"
+          + "\n"
+          + "Please rebase the change locally and upload the rebased commit for review."),
+
+  /** */
   MISSING_DEPENDENCY(""),
 
   /** */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
index ffb91ce..dee2df0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
@@ -46,6 +46,9 @@
 
   /**
    * Create (and open) a repository by name.
+   * <p>
+   * If the implementation supports separate metadata repositories, this method
+   * must also create the metadata repository, but does not open it.
    *
    * @param name the repository name, relative to the base directory.
    * @return the cached Repository instance. Caller must call {@code close()}
@@ -59,6 +62,23 @@
       throws RepositoryCaseMismatchException, RepositoryNotFoundException,
       IOException;
 
+  /**
+   * Open the repository storing metadata for the given project.
+   * <p>
+   * This includes any project-specific metadata <em>except</em> what is stored
+   * in {@code refs/meta/config}. Implementations may choose to store all
+   * metadata in the original project.
+   *
+   * @param name the base project name name.
+   * @return the cached metadata Repository instance. Caller must call
+   *         {@code close()} when done to decrement the resource handle.
+   * @throws RepositoryNotFoundException the name does not denote an existing
+   *         repository.
+   * @throws IOException the name cannot be read as a repository.
+   */
+  public abstract Repository openMetadataRepository(Project.NameKey name)
+      throws RepositoryNotFoundException, IOException;
+
   /** @return set of all known projects, sorted by natural NameKey order. */
   public abstract SortedSet<Project.NameKey> list();
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java
new file mode 100644
index 0000000..29948c2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java
@@ -0,0 +1,142 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+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 GroupList {
+  public static final String FILE_NAME = "groups";
+  private final Map<AccountGroup.UUID, GroupReference> byUUID;
+
+  private GroupList(Map<AccountGroup.UUID, GroupReference> byUUID) {
+        this.byUUID = byUUID;
+  }
+
+  public static GroupList parse(String text, ValidationError.Sink errors) throws IOException {
+    Map<AccountGroup.UUID, GroupReference> groupsByUUID = new HashMap<>();
+
+    BufferedReader br = new BufferedReader(new StringReader(text));
+    String s;
+    for (int lineNumber = 1; (s = br.readLine()) != null; lineNumber++) {
+      if (s.isEmpty() || s.startsWith("#")) {
+        continue;
+      }
+
+      int tab = s.indexOf('\t');
+      if (tab < 0) {
+        errors.error(new ValidationError(FILE_NAME, lineNumber, "missing tab delimiter"));
+        continue;
+      }
+
+      AccountGroup.UUID uuid = new AccountGroup.UUID(s.substring(0, tab).trim());
+      String name = s.substring(tab + 1).trim();
+      GroupReference ref = new GroupReference(uuid, name);
+
+      groupsByUUID.put(uuid, ref);
+    }
+
+    return new GroupList(groupsByUUID);
+  }
+
+  public GroupReference byUUID(AccountGroup.UUID uuid) {
+    return byUUID.get(uuid);
+  }
+
+  public GroupReference resolve(GroupReference group) {
+    if (group != null) {
+      GroupReference ref = byUUID.get(group.getUUID());
+      if (ref != null) {
+        return ref;
+      }
+      byUUID.put(group.getUUID(), group);
+    }
+    return group;
+  }
+
+  public Collection<GroupReference> references() {
+    return byUUID.values();
+  }
+
+  public Set<AccountGroup.UUID> uuids() {
+    return byUUID.keySet();
+  }
+
+  public void put(UUID uuid, GroupReference reference) {
+    byUUID.put(uuid, reference);
+  }
+
+  private static String pad(int len, String src) {
+    if (len <= src.length()) {
+      return src;
+    }
+
+    StringBuilder r = new StringBuilder(len);
+    r.append(src);
+    while (r.length() < len) {
+      r.append(' ');
+    }
+    return r.toString();
+  }
+
+  private static <T extends Comparable<? super T>> List<T> sort(Collection<T> m) {
+    ArrayList<T> r = new ArrayList<>(m);
+    Collections.sort(r);
+    return r;
+  }
+
+  public String asText() {
+    if (byUUID.isEmpty()) {
+      return null;
+    }
+
+    final int uuidLen = 40;
+    StringBuilder buf = new StringBuilder();
+    buf.append(pad(uuidLen, "# UUID"));
+    buf.append('\t');
+    buf.append("Group Name");
+    buf.append('\n');
+
+    buf.append('#');
+    buf.append('\n');
+
+    for (GroupReference g : sort(byUUID.values())) {
+      if (g.getUUID() != null && g.getName() != null) {
+        buf.append(pad(uuidLen, g.getUUID().get()));
+        buf.append('\t');
+        buf.append(g.getName());
+        buf.append('\n');
+      }
+    }
+    return buf.toString();
+  }
+
+  public void retainUUIDs(Collection<AccountGroup.UUID> toBeRetained) {
+    byUUID.keySet().retainAll(toBeRetained);
+  }
+
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
index 12efd56..0dc7aa9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
@@ -16,8 +16,8 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 
+import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Objects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
@@ -49,60 +49,25 @@
  */
 @Singleton
 public class LabelNormalizer {
-  public static class Result {
-    private final ImmutableList<PatchSetApproval> unchanged;
-    private final ImmutableList<PatchSetApproval> updated;
-    private final ImmutableList<PatchSetApproval> deleted;
-
+  @AutoValue
+  public abstract static class Result {
     @VisibleForTesting
-    Result(
+    static Result create(
         List<PatchSetApproval> unchanged,
         List<PatchSetApproval> updated,
         List<PatchSetApproval> deleted) {
-      this.unchanged = ImmutableList.copyOf(unchanged);
-      this.updated = ImmutableList.copyOf(updated);
-      this.deleted = ImmutableList.copyOf(deleted);
+      return new AutoValue_LabelNormalizer_Result(
+          ImmutableList.copyOf(unchanged),
+          ImmutableList.copyOf(updated),
+          ImmutableList.copyOf(deleted));
     }
 
-    public ImmutableList<PatchSetApproval> getUnchanged() {
-      return unchanged;
-    }
-
-    public ImmutableList<PatchSetApproval> getUpdated() {
-      return updated;
-    }
-
-    public ImmutableList<PatchSetApproval> getDeleted() {
-      return deleted;
-    }
+    public abstract ImmutableList<PatchSetApproval> unchanged();
+    public abstract ImmutableList<PatchSetApproval> updated();
+    public abstract ImmutableList<PatchSetApproval> deleted();
 
     public Iterable<PatchSetApproval> getNormalized() {
-      return Iterables.concat(unchanged, updated);
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (o instanceof Result) {
-        Result r = (Result) o;
-        return Objects.equal(unchanged, r.unchanged)
-            && Objects.equal(updated, r.updated)
-            && Objects.equal(deleted, r.deleted);
-      }
-      return false;
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hashCode(unchanged, updated, deleted);
-    }
-
-    @Override
-    public String toString() {
-      return Objects.toStringHelper(this)
-          .add("unchanged", unchanged)
-          .add("updated", updated)
-          .add("deleted", deleted)
-          .toString();
+      return Iterables.concat(unchanged(), updated());
     }
   }
 
@@ -173,7 +138,7 @@
         unchanged.add(psa);
       }
     }
-    return new Result(unchanged, updated, deleted);
+    return Result.create(unchanged, updated, deleted);
   }
 
   /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index 196d3e9..633c3bb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -14,11 +14,16 @@
 
 package com.google.gerrit.server.git;
 
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.MoreObjects;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -124,16 +129,25 @@
   }
 
   private final File basePath;
+  private final File noteDbPath;
   private final Lock namesUpdateLock;
   private volatile SortedSet<Project.NameKey> names;
 
   @Inject
-  LocalDiskRepositoryManager(final SitePaths site,
-      @GerritServerConfig final Config cfg) {
+  LocalDiskRepositoryManager(SitePaths site,
+      @GerritServerConfig Config cfg,
+      NotesMigration notesMigration) {
     basePath = site.resolve(cfg.getString("gerrit", null, "basePath"));
     if (basePath == null) {
       throw new IllegalStateException("gerrit.basePath must be configured");
     }
+
+    if (notesMigration.enabled()) {
+      noteDbPath = site.resolve(MoreObjects.firstNonNull(
+          cfg.getString("gerrit", null, "noteDbPath"), "notedb"));
+    } else {
+      noteDbPath = null;
+    }
     namesUpdateLock = new ReentrantLock(true /* fair */);
     names = list();
   }
@@ -143,28 +157,31 @@
     return basePath;
   }
 
-  private File gitDirOf(Project.NameKey name) {
-    return new File(getBasePath(), name.get());
+  @Override
+  public Repository openRepository(Project.NameKey name)
+      throws RepositoryNotFoundException {
+    return openRepository(basePath, name);
   }
 
-  public Repository openRepository(Project.NameKey name)
+  private Repository openRepository(File path, Project.NameKey name)
       throws RepositoryNotFoundException {
     if (isUnreasonableName(name)) {
       throw new RepositoryNotFoundException("Invalid name: " + name);
     }
+    File gitDir = new File(path, name.get());
     if (!names.contains(name)) {
       // The this.names list does not hold the project-name but it can still exist
       // on disk; for instance when the project has been created directly on the
       // file-system through replication.
       //
       if (!name.get().endsWith(Constants.DOT_GIT_EXT)) {
-        if (FileKey.resolve(gitDirOf(name), FS.DETECTED) != null) {
+        if (FileKey.resolve(gitDir, FS.DETECTED) != null) {
           onCreateProject(name);
         } else {
-          throw new RepositoryNotFoundException(gitDirOf(name));
+          throw new RepositoryNotFoundException(gitDir);
         }
       } else {
-        final File directory = gitDirOf(name);
+        final File directory = gitDir;
         if (FileKey.isGitRepository(new File(directory, Constants.DOT_GIT),
             FS.DETECTED)) {
           onCreateProject(name);
@@ -172,11 +189,11 @@
             directory.getName() + Constants.DOT_GIT_EXT), FS.DETECTED)) {
           onCreateProject(name);
         } else {
-          throw new RepositoryNotFoundException(gitDirOf(name));
+          throw new RepositoryNotFoundException(gitDir);
         }
       }
     }
-    final FileKey loc = FileKey.lenient(gitDirOf(name), FS.DETECTED);
+    final FileKey loc = FileKey.lenient(gitDir, FS.DETECTED);
     try {
       return RepositoryCache.open(loc);
     } catch (IOException e1) {
@@ -187,13 +204,23 @@
     }
   }
 
-  public Repository createRepository(final Project.NameKey name)
+  @Override
+  public Repository createRepository(Project.NameKey name)
+      throws RepositoryNotFoundException, RepositoryCaseMismatchException {
+    Repository repo = createRepository(basePath, name);
+    if (noteDbPath != null) {
+      createRepository(noteDbPath, name);
+    }
+    return repo;
+  }
+
+  private Repository createRepository(File path, Project.NameKey name)
       throws RepositoryNotFoundException, RepositoryCaseMismatchException {
     if (isUnreasonableName(name)) {
       throw new RepositoryNotFoundException("Invalid name: " + name);
     }
 
-    File dir = FileKey.resolve(gitDirOf(name), FS.DETECTED);
+    File dir = FileKey.resolve(new File(path, name.get()), FS.DETECTED);
     FileKey loc;
     if (dir != null) {
       // Already exists on disk, use the repository we found.
@@ -208,7 +235,7 @@
       // of the repository name, so prefer the standard bare name.
       //
       String n = name.get() + Constants.DOT_GIT_EXT;
-      loc = FileKey.exact(new File(basePath, n), FS.DETECTED);
+      loc = FileKey.exact(new File(path, n), FS.DETECTED);
     }
 
     try {
@@ -220,6 +247,18 @@
         null, ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, true);
       config.save();
 
+      // JGit only writes to the reflog for refs/meta/config if the log file
+      // already exists.
+      //
+      File metaConfigLog =
+          new File(db.getDirectory(), "logs/" + RefNames.REFS_CONFIG);
+      if (!metaConfigLog.getParentFile().mkdirs()
+          || !metaConfigLog.createNewFile()) {
+        log.error(String.format(
+            "Failed to create ref log for %s in repository %s",
+            RefNames.REFS_CONFIG, name));
+      }
+
       onCreateProject(name);
 
       return db;
@@ -231,6 +270,17 @@
     }
   }
 
+  @Override
+  public Repository openMetadataRepository(Project.NameKey name)
+      throws RepositoryNotFoundException, IOException {
+    checkState(noteDbPath != null, "notedb disabled");
+    try {
+      return openRepository(noteDbPath, name);
+    } catch (RepositoryNotFoundException e) {
+      return createRepository(noteDbPath, name);
+    }
+  }
+
   private void onCreateProject(final Project.NameKey newProjectName) {
     namesUpdateLock.lock();
     try {
@@ -242,6 +292,7 @@
     }
   }
 
+  @Override
   public String getProjectDescription(final Project.NameKey name)
       throws RepositoryNotFoundException, IOException {
     final Repository e = openRepository(name);
@@ -274,6 +325,7 @@
     return description;
   }
 
+  @Override
   public void setProjectDescription(final Project.NameKey name,
       final String description) {
     // Update git's description file, in case gitweb is being used
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PathConflictException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeConflictException.java
similarity index 68%
copy from gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PathConflictException.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/git/MergeConflictException.java
index 7e2f2d7..02bc8dc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PathConflictException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeConflictException.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2014 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,13 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.changedetail;
+package com.google.gerrit.server.git;
 
-/** Indicates a path conflict during rebase or merge */
-public class PathConflictException extends Exception {
+/** Indicates that the commit cannot be merged without conflicts. */
+public class MergeConflictException extends Exception {
   private static final long serialVersionUID = 1L;
-
-  public PathConflictException(String msg) {
-    super(msg);
+  public MergeConflictException(String msg) {
+    super(msg, null);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
index 54cd329..d3ebb95 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
@@ -18,11 +18,15 @@
 public class MergeException extends Exception {
   private static final long serialVersionUID = 1L;
 
-  public MergeException(final String msg) {
-    super(msg, null);
+  public MergeException(String msg) {
+    super(msg);
   }
 
-  public MergeException(final String msg, final Throwable why) {
+  public MergeException(Throwable why) {
+    super(why);
+  }
+
+  public MergeException(String msg, Throwable why) {
     super(msg, why);
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java
similarity index 66%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java
index 407b7c7..109fa76 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java
@@ -12,10 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.server.git;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+/** Indicates that the commit is already contained in destination banch. */
+public class MergeIdenticalTreeException extends Exception {
+  private static final long serialVersionUID = 1L;
+  public MergeIdenticalTreeException(String msg) {
+    super(msg, null);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 3f0a70d..05e864b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -20,7 +20,6 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.eclipse.jgit.lib.RefDatabase.ALL;
 
-import com.google.common.base.Objects;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
@@ -28,9 +27,10 @@
 import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.common.data.SubmitTypeRecord;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -63,17 +63,19 @@
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -85,6 +87,7 @@
 import org.eclipse.jgit.revwalk.RevFlag;
 import org.eclipse.jgit.revwalk.RevSort;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.joda.time.format.ISODateTimeFormat;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -95,6 +98,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -128,89 +132,103 @@
   private static final long MAX_SUBMIT_WINDOW =
       MILLISECONDS.convert(12, HOURS);
 
-  private final GitRepositoryManager repoManager;
-  private final SchemaFactory<ReviewDb> schemaFactory;
+  private final AccountCache accountCache;
+  private final ApprovalsUtil approvalsUtil;
+  private final ChangeControl.GenericFactory changeControlFactory;
+  private final ChangeData.Factory changeDataFactory;
+  private final ChangeHooks hooks;
+  private final ChangeIndexer indexer;
+  private final ChangeMessagesUtil cmUtil;
   private final ChangeNotes.Factory notesFactory;
-  private final ProjectCache projectCache;
+  private final ChangeUpdate.Factory updateFactory;
   private final GitReferenceUpdated gitRefUpdated;
+  private final GitRepositoryManager repoManager;
+  private final IdentifiedUser.GenericFactory identifiedUserFactory;
   private final MergedSender.Factory mergedSenderFactory;
   private final MergeFailSender.Factory mergeFailSenderFactory;
-  private final PatchSetInfoFactory patchSetInfoFactory;
-  private final IdentifiedUser.GenericFactory identifiedUserFactory;
-  private final ChangeControl.GenericFactory changeControlFactory;
-  private final ChangeUpdate.Factory updateFactory;
   private final MergeQueue mergeQueue;
   private final MergeValidators.Factory mergeValidatorsFactory;
-  private final ApprovalsUtil approvalsUtil;
-  private final ChangeMessagesUtil cmUtil;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final ProjectCache projectCache;
+  private final Provider<InternalChangeQuery> queryProvider;
+  private final RequestScopePropagator requestScopePropagator;
+  private final SchemaFactory<ReviewDb> schemaFactory;
+  private final SubmitStrategyFactory submitStrategyFactory;
+  private final SubmoduleOp.Factory subOpFactory;
+  private final TagCache tagCache;
+  private final WorkQueue workQueue;
 
+  private final String logPrefix;
   private final Branch.NameKey destBranch;
-  private ProjectState destProject;
   private final ListMultimap<SubmitType, CodeReviewCommit> toMerge;
   private final List<CodeReviewCommit> potentiallyStillSubmittable;
   private final Map<Change.Id, CodeReviewCommit> commits;
   private final List<Change> toUpdate;
+
+  private ProjectState destProject;
   private ReviewDb db;
   private Repository repo;
   private RevWalk rw;
   private RevFlag canMergeFlag;
   private CodeReviewCommit branchTip;
-  private CodeReviewCommit mergeTip;
+  private MergeTip mergeTip;
   private ObjectInserter inserter;
   private PersonIdent refLogIdent;
 
-  private final ChangeHooks hooks;
-  private final AccountCache accountCache;
-  private final TagCache tagCache;
-  private final SubmitStrategyFactory submitStrategyFactory;
-  private final SubmoduleOp.Factory subOpFactory;
-  private final WorkQueue workQueue;
-  private final RequestScopePropagator requestScopePropagator;
-  private final ChangeIndexer indexer;
-
   @Inject
-  MergeOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> sf,
-      final ChangeNotes.Factory nf,
-      final ProjectCache pc,
-      final GitReferenceUpdated gru, final MergedSender.Factory msf,
-      final MergeFailSender.Factory mfsf,
-      final PatchSetInfoFactory psif, final IdentifiedUser.GenericFactory iuf,
-      final ChangeControl.GenericFactory changeControlFactory,
-      final ChangeUpdate.Factory updateFactory,
-      final MergeQueue mergeQueue, @Assisted final Branch.NameKey branch,
-      final ChangeHooks hooks, final AccountCache accountCache,
-      final TagCache tagCache,
-      final SubmitStrategyFactory submitStrategyFactory,
-      final SubmoduleOp.Factory subOpFactory,
-      final WorkQueue workQueue,
-      final RequestScopePropagator requestScopePropagator,
-      final ChangeIndexer indexer,
-      final MergeValidators.Factory mergeValidatorsFactory,
-      final ApprovalsUtil approvalsUtil,
-      final ChangeMessagesUtil cmUtil) {
-    repoManager = grm;
-    schemaFactory = sf;
-    notesFactory = nf;
-    projectCache = pc;
-    gitRefUpdated = gru;
-    mergedSenderFactory = msf;
-    mergeFailSenderFactory = mfsf;
-    patchSetInfoFactory = psif;
-    identifiedUserFactory = iuf;
-    this.changeControlFactory = changeControlFactory;
-    this.updateFactory = updateFactory;
-    this.mergeQueue = mergeQueue;
-    this.hooks = hooks;
+  MergeOp(AccountCache accountCache,
+      ApprovalsUtil approvalsUtil,
+      ChangeControl.GenericFactory changeControlFactory,
+      ChangeData.Factory changeDataFactory,
+      ChangeHooks hooks,
+      ChangeIndexer indexer,
+      ChangeMessagesUtil cmUtil,
+      ChangeNotes.Factory notesFactory,
+      ChangeUpdate.Factory updateFactory,
+      GitReferenceUpdated gitRefUpdated,
+      GitRepositoryManager repoManager,
+      IdentifiedUser.GenericFactory identifiedUserFactory,
+      MergedSender.Factory mergedSenderFactory,
+      MergeFailSender.Factory mergeFailSenderFactory,
+      MergeQueue mergeQueue,
+      MergeValidators.Factory mergeValidatorsFactory,
+      PatchSetInfoFactory patchSetInfoFactory,
+      ProjectCache projectCache,
+      Provider<InternalChangeQuery> queryProvider,
+      RequestScopePropagator requestScopePropagator,
+      SchemaFactory<ReviewDb> schemaFactory,
+      SubmitStrategyFactory submitStrategyFactory,
+      SubmoduleOp.Factory subOpFactory,
+      TagCache tagCache,
+      WorkQueue workQueue,
+      @Assisted Branch.NameKey branch) {
     this.accountCache = accountCache;
-    this.tagCache = tagCache;
+    this.approvalsUtil = approvalsUtil;
+    this.changeControlFactory = changeControlFactory;
+    this.changeDataFactory = changeDataFactory;
+    this.hooks = hooks;
+    this.indexer = indexer;
+    this.cmUtil = cmUtil;
+    this.notesFactory = notesFactory;
+    this.updateFactory = updateFactory;
+    this.gitRefUpdated = gitRefUpdated;
+    this.repoManager = repoManager;
+    this.identifiedUserFactory = identifiedUserFactory;
+    this.mergedSenderFactory = mergedSenderFactory;
+    this.mergeFailSenderFactory = mergeFailSenderFactory;
+    this.mergeQueue = mergeQueue;
+    this.mergeValidatorsFactory = mergeValidatorsFactory;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.projectCache = projectCache;
+    this.queryProvider = queryProvider;
+    this.requestScopePropagator = requestScopePropagator;
+    this.schemaFactory = schemaFactory;
     this.submitStrategyFactory = submitStrategyFactory;
     this.subOpFactory = subOpFactory;
+    this.tagCache = tagCache;
     this.workQueue = workQueue;
-    this.requestScopePropagator = requestScopePropagator;
-    this.indexer = indexer;
-    this.mergeValidatorsFactory = mergeValidatorsFactory;
-    this.approvalsUtil = approvalsUtil;
-    this.cmUtil = cmUtil;
+    logPrefix = String.format("[%s@%s]: ", branch.toString(),
+        ISODateTimeFormat.hourMinuteSecond().print(TimeUtil.nowMs()));
     destBranch = branch;
     toMerge = ArrayListMultimap.create();
     potentiallyStillSubmittable = new ArrayList<>();
@@ -231,7 +249,9 @@
     }
   }
 
-  public void merge() throws MergeException, NoSuchChangeException, IOException {
+  public void merge()
+      throws MergeException, NoSuchChangeException, IOException {
+    logDebug("Beginning merge attempt on {}", destBranch);
     setDestProject();
     try {
       openSchema();
@@ -240,74 +260,82 @@
       RefUpdate branchUpdate = openBranch();
       boolean reopen = false;
 
-      final ListMultimap<SubmitType, Change> toSubmit =
-          validateChangeList(db.changes().submitted(destBranch).toList());
-      final ListMultimap<SubmitType, CodeReviewCommit> toMergeNextTurn =
+      ListMultimap<SubmitType, Change> toSubmit =
+          validateChangeList(queryProvider.get().submitted(destBranch));
+      ListMultimap<SubmitType, CodeReviewCommit> toMergeNextTurn =
           ArrayListMultimap.create();
-      final List<CodeReviewCommit> potentiallyStillSubmittableOnNextRun =
+      List<CodeReviewCommit> potentiallyStillSubmittableOnNextRun =
           new ArrayList<>();
       while (!toMerge.isEmpty()) {
+        logDebug("Beginning merge iteration with {} left to merge",
+            toMerge.size());
         toMergeNextTurn.clear();
-        final Set<SubmitType> submitTypes =
-            new HashSet<>(toMerge.keySet());
-        for (final SubmitType submitType : submitTypes) {
+        Set<SubmitType> submitTypes = new HashSet<>(toMerge.keySet());
+        for (SubmitType submitType : submitTypes) {
           if (reopen) {
+            logDebug("Reopening branch");
             branchUpdate = openBranch();
           }
-          final SubmitStrategy strategy = createStrategy(submitType);
-          preMerge(strategy, toMerge.get(submitType));
+          SubmitStrategy strategy = createStrategy(submitType);
+          MergeTip mergeTip = preMerge(strategy, toMerge.get(submitType));
           RefUpdate update = updateBranch(strategy, branchUpdate);
           reopen = true;
 
-          updateChangeStatus(toSubmit.get(submitType));
+          updateChangeStatus(toSubmit.get(submitType), mergeTip);
           updateSubscriptions(toSubmit.get(submitType));
           if (update != null) {
             fireRefUpdated(update);
           }
 
-          for (final Iterator<CodeReviewCommit> it =
+          for (Iterator<CodeReviewCommit> it =
               potentiallyStillSubmittable.iterator(); it.hasNext();) {
-            final CodeReviewCommit commit = it.next();
+            CodeReviewCommit commit = it.next();
             if (containsMissingCommits(toMerge, commit)
                 || containsMissingCommits(toMergeNextTurn, commit)) {
               // change has missing dependencies, but all commits which are
               // missing are still attempted to be merged with another submit
               // strategy, retry to merge this commit in the next turn
+              logDebug("Revision {} of patch set {} has missing dependencies"
+                  + " with different submit types, reconsidering on next run",
+                  commit.name(), commit.getPatchsetId());
               it.remove();
               commit.setStatusCode(null);
               commit.missing = null;
               toMergeNextTurn.put(submitType, commit);
             }
           }
-          potentiallyStillSubmittableOnNextRun.addAll(potentiallyStillSubmittable);
+          logDebug("Adding {} changes potentially submittable on next run",
+              potentiallyStillSubmittable.size());
+          potentiallyStillSubmittableOnNextRun.addAll(
+              potentiallyStillSubmittable);
           potentiallyStillSubmittable.clear();
         }
         toMerge.clear();
         toMerge.putAll(toMergeNextTurn);
+        logDebug("Adding {} changes to merge on next run", toMerge.size());
       }
 
-      updateChangeStatus(toUpdate);
+      updateChangeStatus(toUpdate, mergeTip);
 
-      for (final CodeReviewCommit commit : potentiallyStillSubmittableOnNextRun) {
-        final Capable capable = isSubmitStillPossible(commit);
+      for (CodeReviewCommit commit : potentiallyStillSubmittableOnNextRun) {
+        Capable capable = isSubmitStillPossible(commit);
         if (capable != Capable.OK) {
           sendMergeFail(commit.notes(),
               message(commit.change(), capable.getMessage()), false);
         }
       }
     } catch (NoSuchProjectException noProject) {
-      log.warn(String.format(
-          "Project %s no longer exists, abandoning open changes",
-          destBranch.getParentKey().get()));
+      logWarn("Project " + destBranch.getParentKey() + " no longer exists,"
+          + " abandoning open changes");
       abandonAllOpenChanges();
     } catch (OrmException e) {
       throw new MergeException("Cannot query the database", e);
     } finally {
       if (inserter != null) {
-        inserter.release();
+        inserter.close();
       }
       if (rw != null) {
-        rw.release();
+        rw.close();
       }
       if (repo != null) {
         repo.close();
@@ -319,13 +347,12 @@
   }
 
   private boolean containsMissingCommits(
-      final ListMultimap<SubmitType, CodeReviewCommit> map,
-      final CodeReviewCommit commit) {
+      ListMultimap<SubmitType, CodeReviewCommit> map, CodeReviewCommit commit) {
     if (!isSubmitForMissingCommitsStillPossible(commit)) {
       return false;
     }
 
-    for (final CodeReviewCommit missingCommit : commit.missing) {
+    for (CodeReviewCommit missingCommit : commit.missing) {
       if (!map.containsValue(missingCommit)) {
         return false;
       }
@@ -333,8 +360,12 @@
     return true;
   }
 
-  private boolean isSubmitForMissingCommitsStillPossible(final CodeReviewCommit commit) {
+  private boolean isSubmitForMissingCommitsStillPossible(
+      CodeReviewCommit commit) {
+    PatchSet.Id psId = commit.getPatchsetId();
     if (commit.missing == null || commit.missing.isEmpty()) {
+      logDebug("Patch set {} is not submittable: no list of missing commits",
+          psId);
       return false;
     }
 
@@ -342,7 +373,7 @@
       try {
         loadChangeInfo(missingCommit);
       } catch (NoSuchChangeException | OrmException e) {
-        log.error("Cannot check if missing commits can be submitted", e);
+        logError("Cannot check if missing commits can be submitted", e);
         return false;
       }
 
@@ -350,14 +381,21 @@
         // The commit doesn't have a patch set, so it cannot be
         // submitted to the branch.
         //
+        logDebug("Patch set {} is not submittable: dependency {} has no"
+            + " associated patch set", psId, missingCommit.name());
         return false;
       }
 
       if (!missingCommit.change().currentPatchSetId().equals(
           missingCommit.getPatchsetId())) {
+        PatchSet.Id missingId = missingCommit.getPatchsetId();
         // If the missing commit is not the current patch set,
         // the change must be rebased to use the proper parent.
         //
+        logDebug("Patch set {} is not submittable: depends on patch set {} of"
+            + " change {}, but current patch set is {}", psId, missingId,
+            missingId.getParentKey(),
+            missingCommit.change().currentPatchSetId());
         return false;
       }
     }
@@ -365,36 +403,35 @@
     return true;
   }
 
-  private void preMerge(final SubmitStrategy strategy,
-      final List<CodeReviewCommit> toMerge) throws MergeException {
+  private MergeTip preMerge(SubmitStrategy strategy,
+      List<CodeReviewCommit> toMerge) throws MergeException {
+    logDebug("Running submit strategy {} for {} commits",
+        strategy.getClass().getSimpleName(), toMerge.size());
     mergeTip = strategy.run(branchTip, toMerge);
     refLogIdent = strategy.getRefLogIdent();
+    logDebug("Produced {} new commits", strategy.getNewCommits().size());
     commits.putAll(strategy.getNewCommits());
+    return mergeTip;
   }
 
-  private SubmitStrategy createStrategy(final SubmitType submitType)
+  private SubmitStrategy createStrategy(SubmitType submitType)
       throws MergeException, NoSuchProjectException {
     return submitStrategyFactory.create(submitType, db, repo, rw, inserter,
         canMergeFlag, getAlreadyAccepted(branchTip), destBranch);
   }
 
   private void openRepository() throws MergeException, NoSuchProjectException {
-    final Project.NameKey name = destBranch.getParentKey();
+    Project.NameKey name = destBranch.getParentKey();
     try {
       repo = repoManager.openRepository(name);
     } catch (RepositoryNotFoundException notFound) {
       throw new NoSuchProjectException(name, notFound);
     } catch (IOException err) {
-      final String m = "Error opening repository \"" + name.get() + '"';
+      String m = "Error opening repository \"" + name.get() + '"';
       throw new MergeException(m, err);
     }
 
-    rw = new RevWalk(repo) {
-      @Override
-      protected RevCommit createCommit(final AnyObjectId id) {
-        return new CodeReviewCommit(id);
-      }
-    };
+    rw = CodeReviewCommit.newRevWalk(repo);
     rw.sort(RevSort.TOPO);
     rw.sort(RevSort.COMMIT_TIME_DESC, true);
     canMergeFlag = rw.newFlag("CAN_MERGE");
@@ -402,9 +439,10 @@
     inserter = repo.newObjectInserter();
   }
 
-  private RefUpdate openBranch() throws MergeException, OrmException, NoSuchChangeException {
+  private RefUpdate openBranch()
+      throws MergeException, OrmException, NoSuchChangeException {
     try {
-      final RefUpdate branchUpdate = repo.updateRef(destBranch.get());
+      RefUpdate branchUpdate = repo.updateRef(destBranch.get());
       if (branchUpdate.getOldObjectId() != null) {
         branchTip =
             (CodeReviewCommit) rw.parseCommit(branchUpdate.getOldObjectId());
@@ -412,93 +450,107 @@
         branchTip = null;
         branchUpdate.setExpectedOldObjectId(ObjectId.zeroId());
       } else {
-        for (final Change c : db.changes().submitted(destBranch).toList()) {
-          setNew(c, message(c, "Your change could not be merged, "
-              + "because the destination branch does not exist anymore."));
+        for (ChangeData cd : queryProvider.get().submitted(destBranch)) {
+          try {
+            Change c = cd.change();
+            setNew(c, message(c, "Change could not be merged, "
+                + "because the destination branch does not exist anymore."));
+          } catch (OrmException e) {
+            log.error("Error setting change new", e);
+          }
         }
       }
+      logDebug("Opened branch {}: {}", destBranch.get(), branchTip);
       return branchUpdate;
     } catch (IOException e) {
       throw new MergeException("Cannot open branch", e);
     }
   }
 
-  private Set<RevCommit> getAlreadyAccepted(final CodeReviewCommit branchTip)
+  private Set<RevCommit> getAlreadyAccepted(CodeReviewCommit branchTip)
       throws MergeException {
-    final Set<RevCommit> alreadyAccepted = new HashSet<>();
+    Set<RevCommit> alreadyAccepted = new HashSet<>();
 
     if (branchTip != null) {
       alreadyAccepted.add(branchTip);
     }
 
     try {
-      for (final Ref r : repo.getRefDatabase().getRefs(ALL).values()) {
-        if (r.getName().startsWith(Constants.R_HEADS)) {
-          try {
-            alreadyAccepted.add(rw.parseCommit(r.getObjectId()));
-          } catch (IncorrectObjectTypeException iote) {
-            // Not a commit? Skip over it.
-          }
+      for (Ref r : repo.getRefDatabase().getRefs(Constants.R_HEADS).values()) {
+        try {
+          alreadyAccepted.add(rw.parseCommit(r.getObjectId()));
+        } catch (IncorrectObjectTypeException iote) {
+          // Not a commit? Skip over it.
         }
       }
     } catch (IOException e) {
-      throw new MergeException("Failed to determine already accepted commits.", e);
+      throw new MergeException(
+          "Failed to determine already accepted commits.", e);
     }
 
+    logDebug("Found {} existing heads", alreadyAccepted.size());
     return alreadyAccepted;
   }
 
   private ListMultimap<SubmitType, Change> validateChangeList(
-      final List<Change> submitted) throws MergeException, NoSuchChangeException {
-    final ListMultimap<SubmitType, Change> toSubmit =
-        ArrayListMultimap.create();
+      List<ChangeData> submitted) throws MergeException {
+    logDebug("Validating {} changes", submitted.size());
+    ListMultimap<SubmitType, Change> toSubmit = ArrayListMultimap.create();
 
-    final Map<String, Ref> allRefs;
+    Map<String, Ref> allRefs;
     try {
       allRefs = repo.getRefDatabase().getRefs(ALL);
     } catch (IOException e) {
       throw new MergeException(e.getMessage(), e);
     }
 
-    final Set<ObjectId> tips = new HashSet<>();
-    for (final Ref r : allRefs.values()) {
+    Set<ObjectId> tips = new HashSet<>();
+    for (Ref r : allRefs.values()) {
       tips.add(r.getObjectId());
     }
 
-    int commitOrder = 0;
-    for (final Change chg : submitted) {
+    for (ChangeData cd : submitted) {
       ChangeControl ctl;
+      Change chg;
       try {
-        ctl = changeControlFactory.controlFor(chg,
-            identifiedUserFactory.create(chg.getOwner()));
-      } catch (NoSuchChangeException e) {
+        ctl = cd.changeControl();
+        // Reload change in case index was stale.
+        chg = cd.reloadChange();
+      } catch (OrmException e) {
         throw new MergeException("Failed to validate changes", e);
       }
-      final Change.Id changeId = chg.getId();
+      Change.Id changeId = cd.getId();
+      if (chg.getStatus() != Change.Status.SUBMITTED) {
+        logDebug("Change {} is not submitted: {}", changeId, chg.getStatus());
+        continue;
+      }
       if (chg.currentPatchSetId() == null) {
+        logError("Missing current patch set on change " + changeId);
         commits.put(changeId, CodeReviewCommit.noPatchSet(ctl));
         toUpdate.add(chg);
         continue;
       }
 
-      final PatchSet ps;
+      PatchSet ps;
       try {
-        ps = db.patchSets().get(chg.currentPatchSetId());
+        ps = cd.currentPatchSet();
       } catch (OrmException e) {
         throw new MergeException("Cannot query the database", e);
       }
       if (ps == null || ps.getRevision() == null
           || ps.getRevision().get() == null) {
+        logError("Missing patch set or revision on change " + changeId);
         commits.put(changeId, CodeReviewCommit.noPatchSet(ctl));
         toUpdate.add(chg);
         continue;
       }
 
-      final String idstr = ps.getRevision().get();
-      final ObjectId id;
+      String idstr = ps.getRevision().get();
+      ObjectId id;
       try {
         id = ObjectId.fromString(idstr);
       } catch (IllegalArgumentException iae) {
+        logError("Invalid revision on patch set " + ps.getId());
         commits.put(changeId, CodeReviewCommit.noPatchSet(ctl));
         toUpdate.add(chg);
         continue;
@@ -514,30 +566,36 @@
         // want to merge the issue. We can't safely do that if the
         // tip is not reachable.
         //
+        logError("Revision " + idstr + " of patch set " + ps.getId()
+            + " is not contained in any ref");
         commits.put(changeId, CodeReviewCommit.revisionGone(ctl));
         toUpdate.add(chg);
         continue;
       }
 
-      final CodeReviewCommit commit;
+      CodeReviewCommit commit;
       try {
         commit = (CodeReviewCommit) rw.parseCommit(id);
       } catch (IOException e) {
-        log.error("Invalid commit " + id.name() + " on " + chg.getKey(), e);
+        logError(
+            "Invalid commit " + idstr + " on patch set " + ps.getId(), e);
         commits.put(changeId, CodeReviewCommit.revisionGone(ctl));
         toUpdate.add(chg);
         continue;
       }
 
+      // TODO(dborowitz): Consider putting ChangeData in CodeReviewCommit.
       commit.setControl(ctl);
       commit.setPatchsetId(ps.getId());
-      commit.originalOrder = commitOrder++;
       commits.put(changeId, commit);
 
       MergeValidators mergeValidators = mergeValidatorsFactory.create();
       try {
-        mergeValidators.validatePreMerge(repo, commit, destProject, destBranch, ps.getId());
+        mergeValidators.validatePreMerge(
+            repo, commit, destProject, destBranch, ps.getId());
       } catch (MergeValidationException mve) {
+        logDebug("Revision {} of patch set {} failed validation: {}",
+            idstr, ps.getId(), mve.getStatus());
         commit.setStatusCode(mve.getStatus());
         toUpdate.add(chg);
         continue;
@@ -550,11 +608,13 @@
         //
         try {
           if (rw.isMergedInto(commit, branchTip)) {
+            logDebug("Revision {} of patch set {} is already merged",
+                idstr, ps.getId());
             commit.setStatusCode(CommitMergeStatus.ALREADY_MERGED);
             try {
-              setMerged(chg, null);
+              setMerged(chg, null, commit);
             } catch (OrmException e) {
-              log.error("Cannot mark change " + chg.getId() + " merged", e);
+              logError("Cannot mark change " + chg.getId() + " merged", e);
             }
             continue;
           }
@@ -563,8 +623,11 @@
         }
       }
 
-      SubmitType submitType = getSubmitType(commit.getControl(), ps);
+      SubmitType submitType;
+      submitType = getSubmitType(commit.getControl(), ps);
       if (submitType == null) {
+        logError("No submit type for revision " + idstr + " of patch set "
+            + ps.getId());
         commit.setStatusCode(CommitMergeStatus.NO_SUBMIT_TYPE);
         toUpdate.add(chg);
         continue;
@@ -574,58 +637,77 @@
       toMerge.put(submitType, commit);
       toSubmit.put(submitType, chg);
     }
+    logDebug("Submitting on this run: {}", toSubmit);
     return toSubmit;
   }
 
   private SubmitType getSubmitType(ChangeControl ctl, PatchSet ps) {
-    SubmitTypeRecord r = ctl.getSubmitTypeRecord(db, ps);
-    if (r.status != SubmitTypeRecord.Status.OK) {
-      log.error("Failed to get submit type for " + ctl.getChange().getKey());
+    try {
+      ChangeData cd = changeDataFactory.create(db, ctl);
+      SubmitTypeRecord r = new SubmitRuleEvaluator(cd).setPatchSet(ps)
+          .getSubmitType();
+      if (r.status != SubmitTypeRecord.Status.OK) {
+        logError("Failed to get submit type for " + ctl.getChange().getKey());
+        return null;
+      }
+      return r.type;
+    } catch (OrmException e) {
+      logError("Failed to get submit type for " + ctl.getChange().getKey(), e);
       return null;
     }
-    return r.type;
   }
 
-  private RefUpdate updateBranch(final SubmitStrategy strategy,
-      final RefUpdate branchUpdate) throws MergeException {
-    if (branchTip == mergeTip || mergeTip == null) {
-      // nothing to do
+  private RefUpdate updateBranch(SubmitStrategy strategy,
+      RefUpdate branchUpdate) throws MergeException {
+    CodeReviewCommit currentTip =
+        mergeTip != null ? mergeTip.getCurrentTip() : null;
+    if (Objects.equals(branchTip, currentTip)) {
+      logDebug("Branch already at merge tip {}, no update to perform",
+          currentTip.name());
+      return null;
+    } else if (currentTip == null) {
+      logDebug("No merge tip, no update to perform");
       return null;
     }
 
     if (RefNames.REFS_CONFIG.equals(branchUpdate.getName())) {
+      logDebug("Loading new configuration from {}", RefNames.REFS_CONFIG);
       try {
         ProjectConfig cfg =
             new ProjectConfig(destProject.getProject().getNameKey());
-        cfg.load(repo, mergeTip);
+        cfg.load(repo, currentTip);
       } catch (Exception e) {
         throw new MergeException("Submit would store invalid"
-            + " project configuration " + mergeTip.name() + " for "
+            + " project configuration " + currentTip.name() + " for "
             + destProject.getProject().getName(), e);
       }
     }
 
     branchUpdate.setRefLogIdent(refLogIdent);
     branchUpdate.setForceUpdate(false);
-    branchUpdate.setNewObjectId(mergeTip);
+    branchUpdate.setNewObjectId(currentTip);
     branchUpdate.setRefLogMessage("merged", true);
     try {
-      switch (branchUpdate.update(rw)) {
+      RefUpdate.Result result = branchUpdate.update(rw);
+      logDebug("Update of {}: {}..{} returned status {}",
+          branchUpdate.getName(), branchUpdate.getOldObjectId(),
+          branchUpdate.getNewObjectId(), result);
+      switch (result) {
         case NEW:
         case FAST_FORWARD:
           if (branchUpdate.getResult() == RefUpdate.Result.FAST_FORWARD) {
             tagCache.updateFastForward(destBranch.getParentKey(),
                 branchUpdate.getName(),
                 branchUpdate.getOldObjectId(),
-                mergeTip);
+                currentTip);
           }
 
           if (RefNames.REFS_CONFIG.equals(branchUpdate.getName())) {
-            projectCache.evict(destProject.getProject());
-            destProject = projectCache.get(destProject.getProject().getNameKey());
+            Project p = destProject.getProject();
+            projectCache.evict(p);
+            destProject = projectCache.get(p.getNameKey());
             repoManager.setProjectDescription(
-                destProject.getProject().getNameKey(),
-                destProject.getProject().getDescription());
+                p.getNameKey(), p.getDescription());
           }
 
           return branchUpdate;
@@ -639,9 +721,12 @@
           } else {
             msg = "will not retry";
           }
-          throw new IOException(branchUpdate.getResult().name() + ", " + msg);
+          // TODO(dborowitz): Implement RefUpdate.toString().
+          throw new IOException(branchUpdate.getResult().name() + ", " + msg
+              + '\n' + branchUpdate);
         default:
-          throw new IOException(branchUpdate.getResult().name());
+          throw new IOException(branchUpdate.getResult().name()
+              + '\n' + branchUpdate);
       }
     } catch (IOException e) {
       throw new MergeException("Cannot update " + branchUpdate.getName(), e);
@@ -649,8 +734,10 @@
   }
 
   private void fireRefUpdated(RefUpdate branchUpdate) {
+    logDebug("Firing ref updated hooks for {}", branchUpdate.getName());
     gitRefUpdated.fire(destBranch.getParentKey(), branchUpdate);
-    hooks.doRefUpdatedHook(destBranch, branchUpdate, getAccount(mergeTip));
+    hooks.doRefUpdatedHook(destBranch, branchUpdate,
+        getAccount(mergeTip.getCurrentTip()));
   }
 
   private Account getAccount(CodeReviewCommit codeReviewCommit) {
@@ -671,33 +758,42 @@
     return "";
   }
 
-  private void updateChangeStatus(final List<Change> submitted) throws NoSuchChangeException {
-    for (final Change c : submitted) {
-      final CodeReviewCommit commit = commits.get(c.getId());
-      final CommitMergeStatus s = commit != null ? commit.getStatusCode() : null;
+  private void updateChangeStatus(List<Change> submitted, MergeTip mergeTip)
+      throws NoSuchChangeException {
+    logDebug("Updating change status for {} changes", submitted.size());
+    for (Change c : submitted) {
+      CodeReviewCommit commit = commits.get(c.getId());
+      CommitMergeStatus s = commit != null ? commit.getStatusCode() : null;
       if (s == null) {
         // Shouldn't ever happen, but leave the change alone. We'll pick
         // it up on the next pass.
         //
+        logDebug("Submitted change {} did not appear in set of new commits"
+            + " produced by merge strategy", c.getId());
         continue;
       }
 
-      final String txt = s.getMessage();
-
+      String txt = s.getMessage();
+      logDebug("Status of change {} ({}) on {}: {}", c.getId(), commit.name(),
+          c.getDest(), s);
+      // If mergeTip is null merge failed and mergeResultRev will not be read.
+      ObjectId mergeResultRev =
+          mergeTip != null ? mergeTip.getMergeResults().get(commit) : null;
       try {
         switch (s) {
           case CLEAN_MERGE:
-            setMerged(c, message(c, txt + getByAccountName(commit)));
+            setMerged(c, message(c, txt + getByAccountName(commit)),
+                mergeResultRev);
             break;
 
           case CLEAN_REBASE:
           case CLEAN_PICK:
             setMerged(c, message(c, txt + " as " + commit.name()
-                + getByAccountName(commit)));
+                + getByAccountName(commit)), mergeResultRev);
             break;
 
           case ALREADY_MERGED:
-            setMerged(c, null);
+            setMerged(c, null, mergeResultRev);
             break;
 
           case PATH_CONFLICT:
@@ -714,54 +810,64 @@
             break;
 
           case MISSING_DEPENDENCY:
+            logDebug("Change {} is missing dependency", c.getId());
             potentiallyStillSubmittable.add(commit);
             break;
 
           default:
-            setNew(commit, message(c, "Unspecified merge failure: " + s.name()));
+            setNew(commit,
+                message(c, "Unspecified merge failure: " + s.name()));
             break;
         }
       } catch (OrmException err) {
-        log.warn("Error updating change status for " + c.getId(), err);
+        logWarn("Error updating change status for " + c.getId(), err);
       } catch (IOException err) {
-        log.warn("Error updating change status for " + c.getId(), err);
+        logWarn("Error updating change status for " + c.getId(), err);
       }
     }
   }
 
-  private void updateSubscriptions(final List<Change> submitted) {
-    if (mergeTip != null && (branchTip == null || branchTip != mergeTip)) {
+  private void updateSubscriptions(List<Change> submitted) {
+    if (mergeTip != null
+        && (branchTip == null || branchTip != mergeTip.getCurrentTip())) {
+      logDebug("Updating submodule subscriptions for {} changes",
+          submitted.size());
       SubmoduleOp subOp =
-          subOpFactory.create(destBranch, mergeTip, rw, repo,
+          subOpFactory.create(destBranch, mergeTip.getCurrentTip(), rw, repo,
               destProject.getProject(), submitted, commits,
-              getAccount(mergeTip));
+              getAccount(mergeTip.getCurrentTip()));
       try {
         subOp.update();
       } catch (SubmoduleException e) {
-        log
-            .error("The gitLinks were not updated according to the subscriptions "
-                + e.getMessage());
+        logError(
+            "The gitLinks were not updated according to the subscriptions" , e);
       }
     }
   }
 
-  private Capable isSubmitStillPossible(final CodeReviewCommit commit) {
-    final Capable capable;
-    final Change c = commit.change();
-    final boolean submitStillPossible = isSubmitForMissingCommitsStillPossible(commit);
-    final long now = TimeUtil.nowMs();
-    final long waitUntil = c.getLastUpdatedOn().getTime() + DEPENDENCY_DELAY;
+  private Capable isSubmitStillPossible(CodeReviewCommit commit) {
+    Capable capable;
+    Change c = commit.change();
+    boolean submitStillPossible =
+        isSubmitForMissingCommitsStillPossible(commit);
+    long now = TimeUtil.nowMs();
+    long waitUntil = c.getLastUpdatedOn().getTime() + DEPENDENCY_DELAY;
     if (submitStillPossible && now < waitUntil) {
+      long recheckIn = waitUntil - now;
+      logDebug("Submit for {} is still possible; rechecking in {}ms",
+          c.getId(), recheckIn);
       // If we waited a short while we might still be able to get
       // this change submitted. Reschedule an attempt in a bit.
       //
-      mergeQueue.recheckAfter(destBranch, waitUntil - now, MILLISECONDS);
+      mergeQueue.recheckAfter(destBranch, recheckIn, MILLISECONDS);
       capable = Capable.OK;
     } else if (submitStillPossible) {
       // It would be possible to submit the change if the missing
       // dependencies are also submitted. Perhaps the user just
       // forgot to submit those.
       //
+      logDebug("Submit for {} is still possible after missing dependencies",
+          c.getId());
       StringBuilder m = new StringBuilder();
       m.append("Change could not be merged because of a missing dependency.");
       m.append("\n");
@@ -781,20 +887,23 @@
       // needs to rebase it in order to work around the missing
       // dependencies.
       //
+      logDebug("Submit for {} is not possible", c.getId());
       StringBuilder m = new StringBuilder();
       m.append("Change cannot be merged due to unsatisfiable dependencies.\n");
       m.append("\n");
       m.append("The following dependency errors were found:\n");
       m.append("\n");
       for (CodeReviewCommit missingCommit : commit.missing) {
-        if (missingCommit.getPatchsetId() != null) {
+        PatchSet.Id missingPsId = missingCommit.getPatchsetId();
+        if (missingPsId != null) {
           m.append("* Depends on patch set ");
-          m.append(missingCommit.getPatchsetId().get());
+          m.append(missingPsId.get());
           m.append(" of ");
           m.append(missingCommit.change().getKey().abbreviate());
-          if (missingCommit.getPatchsetId().get() != missingCommit.change().currentPatchSetId().get()) {
+          PatchSet.Id currPsId = missingCommit.change().currentPatchSetId();
+          if (!missingPsId.equals(currPsId)) {
             m.append(", however the current patch set is ");
-            m.append(missingCommit.change().currentPatchSetId().get());
+            m.append(currPsId.get());
           }
           m.append(".\n");
 
@@ -812,21 +921,21 @@
     return capable;
   }
 
-  private void loadChangeInfo(final CodeReviewCommit commit)
+  private void loadChangeInfo(CodeReviewCommit commit)
       throws NoSuchChangeException, OrmException {
     if (commit.getControl() == null) {
       List<PatchSet> matches =
           db.patchSets().byRevision(new RevId(commit.name())).toList();
       if (matches.size() == 1) {
-        PatchSet ps = matches.get(0);
-        commit.setPatchsetId(ps.getId());
-        commit.setControl(changeControl(db.changes().get(ps.getId().getParentKey())));
+        PatchSet.Id psId = matches.get(0).getId();
+        commit.setPatchsetId(psId);
+        commit.setControl(changeControl(db.changes().get(psId.getParentKey())));
       }
     }
   }
 
-  private ChangeMessage message(final Change c, final String body) {
-    final String uuid;
+  private ChangeMessage message(Change c, String body) {
+    String uuid;
     try {
       uuid = ChangeUtil.messageUUID(db);
     } catch (OrmException e) {
@@ -838,19 +947,22 @@
     return m;
   }
 
-  private void setMerged(Change c, ChangeMessage msg)
-      throws OrmException, IOException, NoSuchChangeException {
+  private void setMerged(Change c, ChangeMessage msg, ObjectId mergeResultRev)
+      throws OrmException, IOException {
+    logDebug("Setting change {} merged", c.getId());
     ChangeUpdate update = null;
+    PatchSetApproval submitter;
+    PatchSet merged;
     try {
       db.changes().beginTransaction(c.getId());
 
       // We must pull the patchset out of commits, because the patchset ID is
       // modified when using the cherry-pick merge strategy.
       CodeReviewCommit commit = commits.get(c.getId());
-      PatchSet.Id merged = commit.change().currentPatchSetId();
-      c = setMergedPatchSet(c.getId(), merged);
-      PatchSetApproval submitter =
-          approvalsUtil.getSubmitter(db, commit.notes(), merged);
+      PatchSet.Id mergedId = commit.change().currentPatchSetId();
+      merged = db.patchSets().get(mergedId);
+      c = setMergedPatchSet(c.getId(), mergedId);
+      submitter = approvalsUtil.getSubmitter(db, commit.notes(), mergedId);
       ChangeControl control = commit.getControl();
       update = updateFactory.create(control, c.getLastUpdatedOn());
 
@@ -862,22 +974,21 @@
       }
       db.commit();
 
-      sendMergedEmail(c, submitter);
-      indexer.index(db, c);
-      if (submitter != null) {
-        try {
-          hooks.doChangeMergedHook(c,
-              accountCache.get(submitter.getAccountId()).getAccount(),
-              db.patchSets().get(merged), db);
-        } catch (OrmException ex) {
-          log.error("Cannot run hook for submitted patch set " + c.getId(), ex);
-        }
-      }
     } finally {
       db.rollback();
     }
-    indexer.index(db, c);
     update.commit();
+    sendMergedEmail(c, submitter);
+    indexer.index(db, c);
+    if (submitter != null && mergeResultRev != null) {
+      try {
+        hooks.doChangeMergedHook(c,
+            accountCache.get(submitter.getAccountId()).getAccount(),
+            merged, db, mergeResultRev.name());
+      } catch (OrmException ex) {
+        logError("Cannot run hook for submitted patch set " + c.getId(), ex);
+      }
+    }
   }
 
   private Change setMergedPatchSet(Change.Id changeId, final PatchSet.Id merged)
@@ -886,10 +997,6 @@
       @Override
       public Change update(Change c) {
         c.setStatus(Change.Status.MERGED);
-        // It could be possible that the change being merged
-        // has never had its mergeability tested. So we insure
-        // merged changes has mergeable field true.
-        c.setMergeable(true);
         if (!merged.equals(c.currentPatchSetId())) {
           // Uncool; the patch set changed after we merged it.
           // Go back to the patch set that was actually merged.
@@ -897,7 +1004,7 @@
           try {
             c.setCurrentPatchSet(patchSetInfoFactory.get(db, merged));
           } catch (PatchSetInfoNotAvailableException e1) {
-            log.error("Cannot read merged patch set " + merged, e1);
+            logError("Cannot read merged patch set " + merged, e1);
           }
         }
         ChangeUtil.updated(c);
@@ -920,7 +1027,7 @@
             reviewDb.close();
           }
         } catch (Exception e) {
-          log.error("Cannot send email for submitted patch set " + c.getId(), e);
+          logError("Cannot send email for submitted patch set " + c.getId(), e);
           return;
         }
 
@@ -932,7 +1039,7 @@
           cm.setPatchSet(patchSet);
           cm.send();
         } catch (Exception e) {
-          log.error("Cannot send email for submitted patch set " + c.getId(), e);
+          logError("Cannot send email for submitted patch set " + c.getId(), e);
         }
       }
 
@@ -948,11 +1055,13 @@
         c, identifiedUserFactory.create(c.getOwner()));
   }
 
-  private void setNew(CodeReviewCommit c, ChangeMessage msg) throws NoSuchChangeException, IOException {
+  private void setNew(CodeReviewCommit c, ChangeMessage msg)
+      throws NoSuchChangeException, IOException {
     sendMergeFail(c.notes(), msg, true);
   }
 
-  private void setNew(Change c, ChangeMessage msg) throws OrmException, NoSuchChangeException, IOException {
+  private void setNew(Change c, ChangeMessage msg)
+      throws NoSuchChangeException, IOException {
     sendMergeFail(notesFactory.create(c), msg, true);
   }
 
@@ -964,39 +1073,57 @@
       @Nullable PatchSetApproval submitter,
       ChangeMessage msg,
       ChangeNotes notes) {
-    if (submitter != null
-        && TimeUtil.nowMs() - submitter.getGranted().getTime()
-          > MAX_SUBMIT_WINDOW) {
-      return RetryStatus.UNSUBMIT;
+    Change.Id id = notes.getChangeId();
+    if (submitter != null) {
+      long sinceMs = TimeUtil.nowMs() - submitter.getGranted().getTime();
+      if (sinceMs > MAX_SUBMIT_WINDOW) {
+        logDebug("Change {} submitted {}ms ago, unsubmitting", id, sinceMs);
+        return RetryStatus.UNSUBMIT;
+      } else {
+        logDebug("Change {} submitted {}ms ago, within window", id, sinceMs);
+      }
+    } else {
+      logDebug("No submitter for change {}", id);
     }
 
     try {
       ChangeMessage last = Iterables.getLast(cmUtil.byChange(db, notes));
       if (last != null) {
-        if (Objects.equal(last.getAuthor(), msg.getAuthor())
-            && Objects.equal(last.getMessage(), msg.getMessage())) {
+        if (Objects.equals(last.getAuthor(), msg.getAuthor())
+            && Objects.equals(last.getMessage(), msg.getMessage())) {
           long lastMs = last.getWrittenOn().getTime();
           long msgMs = msg.getWrittenOn().getTime();
-          return msgMs - lastMs > MAX_SUBMIT_WINDOW
-              ? RetryStatus.UNSUBMIT
-              : RetryStatus.RETRY_NO_MESSAGE;
+          long sinceMs = msgMs - lastMs;
+          if (sinceMs > MAX_SUBMIT_WINDOW) {
+            logDebug("Last message for change {} was {}ms ago, unsubmitting",
+                id, sinceMs);
+            return RetryStatus.UNSUBMIT;
+          } else {
+            logDebug("Last message for change {} was {}ms ago, within window",
+                id, sinceMs);
+            return RetryStatus.RETRY_NO_MESSAGE;
+          }
+        } else {
+          logDebug("Last message for change {} differed, adding message", id);
         }
       }
       return RetryStatus.RETRY_ADD_MESSAGE;
     } catch (OrmException err) {
-      log.warn("Cannot check previous merge failure, unsubmitting", err);
+      logWarn("Cannot check previous merge failure, unsubmitting", err);
       return RetryStatus.UNSUBMIT;
     }
   }
 
   private void sendMergeFail(ChangeNotes notes, final ChangeMessage msg,
       boolean makeNew) throws NoSuchChangeException, IOException {
+    logDebug("Possibly sending merge failure notification for {}",
+        notes.getChangeId());
     PatchSetApproval submitter = null;
     try {
       submitter = approvalsUtil.getSubmitter(
           db, notes, notes.getChange().currentPatchSetId());
     } catch (Exception e) {
-      log.error("Cannot get submitter", e);
+      logError("Cannot get submitter for change " + notes.getChangeId(), e);
     }
 
     if (!makeNew) {
@@ -1041,7 +1168,7 @@
         db.rollback();
       }
     } catch (OrmException err) {
-      log.warn("Cannot record merge failure message", err);
+      logWarn("Cannot record merge failure message", err);
     }
     if (update != null) {
       update.commit();
@@ -1067,12 +1194,12 @@
             reviewDb.close();
           }
         } catch (Exception e) {
-          log.error("Cannot send email notifications about merge failure", e);
+          logError("Cannot send email notifications about merge failure", e);
           return;
         }
 
         try {
-          final MergeFailSender cm = mergeFailSenderFactory.create(c);
+          MergeFailSender cm = mergeFailSenderFactory.create(c);
           if (from != null) {
             cm.setFrom(from.getAccountId());
           }
@@ -1080,7 +1207,7 @@
           cm.setChangeMessage(msg);
           cm.send();
         } catch (Exception e) {
-          log.error("Cannot send email notifications about merge failure", e);
+          logError("Cannot send email notifications about merge failure", e);
         }
       }
 
@@ -1094,7 +1221,7 @@
       try {
         indexFuture.checkedGet();
       } catch (IOException e) {
-        log.error("Failed to index new change message", e);
+        logError("Failed to index new change message", e);
       }
     }
 
@@ -1104,7 +1231,7 @@
             accountCache.get(submitter.getAccountId()).getAccount(),
             db.patchSets().get(c.currentPatchSetId()), msg.getMessage(), db);
       } catch (OrmException ex) {
-        log.error("Cannot run hook for merge failed " + c.getId(), ex);
+        logError("Cannot run hook for merge failed " + c.getId(), ex);
       }
     }
   }
@@ -1113,8 +1240,9 @@
     Exception err = null;
     try {
       openSchema();
-      for (Change c : db.changes().byProjectOpenAll(destBranch.getParentKey())) {
-        abandonOneChange(c);
+      for (ChangeData cd
+          : queryProvider.get().byProjectOpen(destBranch.getParentKey())) {
+        abandonOneChange(cd.change());
       }
       db.close();
       db = null;
@@ -1124,9 +1252,8 @@
       err = e;
     }
     if (err != null) {
-      log.warn(String.format(
-          "Cannot abandon changes for deleted project %s",
-          destBranch.getParentKey().get()), err);
+      logWarn("Cannot abandon changes for deleted project "
+          + destBranch.getParentKey().get(), err);
     }
   }
 
@@ -1172,4 +1299,34 @@
     }
     update.commit();
   }
+
+  private void logDebug(String msg, Object... args) {
+    if (log.isDebugEnabled()) {
+      log.debug(logPrefix + msg, args);
+    }
+  }
+
+  private void logWarn(String msg, Throwable t) {
+    if (log.isWarnEnabled()) {
+      log.warn(logPrefix + msg, t);
+    }
+  }
+
+  private void logWarn(String msg) {
+    if (log.isWarnEnabled()) {
+      log.warn(logPrefix + msg);
+    }
+  }
+
+  private void logError(String msg, Throwable t) {
+    if (log.isErrorEnabled()) {
+      log.error(logPrefix + msg, t);
+    }
+  }
+
+  private void logError(String msg) {
+    if (log.isErrorEnabled()) {
+      log.error(logPrefix + msg);
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeTip.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeTip.java
new file mode 100644
index 0000000..ba92651
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeTip.java
@@ -0,0 +1,87 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.Nullable;
+
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Class describing a merge tip during merge operation.
+ * <p>
+ * The current tip of a {@link MergeTip} may be null if the merge operation is
+ * against an unborn branch, and has not yet been attempted. This is distinct
+ * from a null {@link MergeTip} instance, which may be used to indicate that a
+ * merge failed or another error state.
+ */
+public class MergeTip {
+  private CodeReviewCommit branchTip;
+  private Map<ObjectId, ObjectId> mergeResults;
+
+  /**
+   * @param initial Tip before the merge operation; may be null, indicating an
+   *     unborn branch.
+   * @param toMerge List of CodeReview commits to be merged in merge operation;
+   *     may not be null or empty.
+   */
+  public MergeTip(@Nullable CodeReviewCommit initial,
+      Collection<CodeReviewCommit> toMerge) {
+    checkArgument(toMerge != null && !toMerge.isEmpty(),
+        "toMerge may not be null or empty: %s", toMerge);
+    this.mergeResults = Maps.newHashMap();
+    this.branchTip = initial;
+    // Assume fast-forward merge until opposite is proven.
+    for (CodeReviewCommit commit : toMerge) {
+      mergeResults.put(commit.copy(), commit.copy());
+    }
+  }
+
+  /**
+   * Moves this MergeTip to newTip and appends mergeResult.
+   *
+   * @param newTip The new tip; may not be null.
+   * @param mergedFrom The result of the merge of {@code newTip}.
+   */
+  public void moveTipTo(CodeReviewCommit newTip, ObjectId mergedFrom) {
+    checkArgument(newTip != null);
+    branchTip = newTip;
+    mergeResults.put(mergedFrom, newTip.copy());
+  }
+
+  /**
+   * The merge results of all the merges of this merge operation.
+   *
+   * @return The merge results of the merge operation as a map of SHA-1 to be
+   *     merged to SHA-1 of the merge result.
+   */
+  public Map<ObjectId, ObjectId> getMergeResults() {
+    return mergeResults;
+  }
+
+  /**
+   * @return The current tip of the current merge operation; may be null,
+   *     indicating an unborn branch.
+   */
+  @Nullable
+  public CodeReviewCommit getCurrentTip() {
+    return branchTip;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index 709a8ac..295ad52 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -20,12 +20,13 @@
 import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
+import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
@@ -71,7 +72,6 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
@@ -80,8 +80,6 @@
 import java.util.TimeZone;
 
 public class MergeUtil {
-  public static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
-  private static final FooterKey REVIEWED_ON = new FooterKey("Reviewed-on");
   private static final Logger log = LoggerFactory.getLogger(MergeUtil.class);
   private static final String R_HEADS_MASTER =
       Constants.R_HEADS + Constants.MASTER;
@@ -154,23 +152,16 @@
     return mergeTip;
   }
 
-  public void reduceToMinimalMerge(final MergeSorter mergeSorter,
-      final List<CodeReviewCommit> toSort) throws MergeException {
-    final Collection<CodeReviewCommit> heads;
+  public List<CodeReviewCommit> reduceToMinimalMerge(MergeSorter mergeSorter,
+      Collection<CodeReviewCommit> toSort) throws MergeException {
+    List<CodeReviewCommit> result = new ArrayList<>();
     try {
-      heads = mergeSorter.sort(toSort);
+      result.addAll(mergeSorter.sort(toSort));
     } catch (IOException e) {
       throw new MergeException("Branch head sorting failed", e);
     }
-
-    toSort.clear();
-    toSort.addAll(heads);
-    Collections.sort(toSort, new Comparator<CodeReviewCommit>() {
-      @Override
-      public int compare(final CodeReviewCommit a, final CodeReviewCommit b) {
-        return a.originalOrder - b.originalOrder;
-      }
-    });
+    Collections.sort(result, CodeReviewCommit.ORDER);
+    return result;
   }
 
   public PatchSetApproval getSubmitter(CodeReviewCommit c) {
@@ -180,7 +171,8 @@
   public RevCommit createCherryPickFromCommit(Repository repo,
       ObjectInserter inserter, RevCommit mergeTip, RevCommit originalCommit,
       PersonIdent cherryPickCommitterIdent, String commitMsg, RevWalk rw)
-      throws MissingObjectException, IncorrectObjectTypeException, IOException {
+      throws MissingObjectException, IncorrectObjectTypeException, IOException,
+      MergeIdenticalTreeException, MergeConflictException {
 
     final ThreeWayMerger m = newThreeWayMerger(repo, inserter);
 
@@ -188,7 +180,7 @@
     if (m.merge(mergeTip, originalCommit)) {
       ObjectId tree = m.getResultTreeId();
       if (tree.equals(mergeTip.getTree())) {
-        return null;
+        throw new MergeIdenticalTreeException("identical tree");
       }
 
       CommitBuilder mergeCommit = new CommitBuilder();
@@ -199,7 +191,7 @@
       mergeCommit.setMessage(commitMsg);
       return rw.parseCommit(commit(inserter, mergeCommit));
     } else {
-      return null;
+      throw new MergeConflictException("merge conflict");
     }
   }
 
@@ -223,8 +215,8 @@
       msgbuf.append('\n');
     }
 
-    if (!contains(footers, CHANGE_ID, n.change().getKey().get())) {
-      msgbuf.append(CHANGE_ID.getName());
+    if (!contains(footers, FooterConstants.CHANGE_ID, n.change().getKey().get())) {
+      msgbuf.append(FooterConstants.CHANGE_ID.getName());
       msgbuf.append(": ");
       msgbuf.append(n.change().getKey().get());
       msgbuf.append('\n');
@@ -233,8 +225,8 @@
     final String siteUrl = urlProvider.get();
     if (siteUrl != null) {
       final String url = siteUrl + n.getPatchsetId().getParentKey().get();
-      if (!contains(footers, REVIEWED_ON, url)) {
-        msgbuf.append(REVIEWED_ON.getName());
+      if (!contains(footers, FooterConstants.REVIEWED_ON, url)) {
+        msgbuf.append(FooterConstants.REVIEWED_ON.getName());
         msgbuf.append(": ");
         msgbuf.append(url);
         msgbuf.append('\n');
@@ -379,8 +371,7 @@
 
       final Timestamp dt = submitter.getGranted();
       final TimeZone tz = myIdent.getTimeZone();
-      if (emails.size() == 1
-          && who.getEmailAddresses().contains(emails.iterator().next())) {
+      if (emails.size() == 1 && who.hasEmailAddress(emails.iterator().next())) {
         authorIdent =
             new PersonIdent(codeReviewCommits.get(0).getAuthorIdent(), dt, tz);
       } else {
@@ -612,9 +603,11 @@
     }
 
     if (topics.size() == 1) {
-      return String.format("Merge topic '%s'", Iterables.getFirst(topics, null));
+      return String.format("Merge changes from topic '%s'",
+          Iterables.getFirst(topics, null));
     } else if (topics.size() > 1) {
-      return String.format("Merge topics '%s'", Joiner.on("', '").join(topics));
+      return String.format("Merge changes from topics '%s'",
+          Joiner.on("', '").join(topics));
     } else {
       return String.format("Merge changes %s%s",
           Joiner.on(',').join(Iterables.transform(
@@ -631,8 +624,11 @@
 
   public ThreeWayMerger newThreeWayMerger(final Repository repo,
       final ObjectInserter inserter) {
-    return newThreeWayMerger(repo, inserter,
-        mergeStrategyName(useContentMerge, useRecursiveMerge));
+    return newThreeWayMerger(repo, inserter, mergeStrategyName());
+  }
+
+  public String mergeStrategyName() {
+    return mergeStrategyName(useContentMerge, useRecursiveMerge);
   }
 
   public static String mergeStrategyName(boolean useContentMerge,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
index b48028e..c71c94f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
@@ -21,8 +22,10 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -59,7 +62,42 @@
 
     public MetaDataUpdate create(Project.NameKey name, IdentifiedUser user)
         throws RepositoryNotFoundException, IOException {
-      MetaDataUpdate md = factory.create(name, mgr.openRepository(name));
+      return create(name, user, null);
+    }
+
+  /**
+   * Create an update using an existing batch ref update.
+   * <p>
+   * This allows batching together updates to multiple metadata refs. For making
+   * multiple commits to a single metadata ref, see
+   * {@link VersionedMetaData#openUpdate(MetaDataUpdate)}.
+   *
+   * @param name project name.
+   * @param user user for the update.
+   * @param batch batch update to use; the caller is responsible for committing
+   *     the update.
+   */
+    public MetaDataUpdate create(Project.NameKey name, IdentifiedUser user,
+        BatchRefUpdate batch) throws RepositoryNotFoundException, IOException {
+      return create(name, mgr.openRepository(name), user, batch);
+    }
+
+    /**
+     * Create an update using an existing batch ref update.
+     * <p>
+     * This allows batching together updates to multiple metadata refs. For making
+     * multiple commits to a single metadata ref, see
+     * {@link VersionedMetaData#openUpdate(MetaDataUpdate)}.
+     *
+     * @param name project name.
+     * @param repository GIT respository
+     * @param user user for the update.
+     * @param batch batch update to use; the caller is responsible for committing
+     *     the update.
+     */
+    public MetaDataUpdate create(Project.NameKey name, Repository repository,
+        IdentifiedUser user, BatchRefUpdate batch) {
+      MetaDataUpdate md = factory.create(name, repository, batch);
       md.getCommitBuilder().setAuthor(createPersonIdent(user));
       md.getCommitBuilder().setCommitter(serverIdent);
       return md;
@@ -86,7 +124,13 @@
 
     public MetaDataUpdate create(Project.NameKey name)
         throws RepositoryNotFoundException, IOException {
-      MetaDataUpdate md = factory.create(name, mgr.openRepository(name));
+      return create(name, null);
+    }
+
+    /** @see User#create(Project.NameKey, IdentifiedUser, BatchRefUpdate) */
+    public MetaDataUpdate create(Project.NameKey name, BatchRefUpdate batch)
+        throws RepositoryNotFoundException, IOException {
+      MetaDataUpdate md = factory.create(name, mgr.openRepository(name), batch);
       md.getCommitBuilder().setAuthor(serverIdent);
       md.getCommitBuilder().setCommitter(serverIdent);
       return md;
@@ -95,24 +139,32 @@
 
   interface InternalFactory {
     MetaDataUpdate create(@Assisted Project.NameKey projectName,
-        @Assisted Repository db);
+        @Assisted Repository db, @Assisted @Nullable BatchRefUpdate batch);
   }
 
   private final GitReferenceUpdated gitRefUpdated;
   private final Project.NameKey projectName;
   private final Repository db;
+  private final BatchRefUpdate batch;
   private final CommitBuilder commit;
   private boolean allowEmpty;
 
-  @Inject
+  @AssistedInject
   public MetaDataUpdate(GitReferenceUpdated gitRefUpdated,
-      @Assisted Project.NameKey projectName, @Assisted Repository db) {
+      @Assisted Project.NameKey projectName, @Assisted Repository db,
+      @Assisted @Nullable BatchRefUpdate batch) {
     this.gitRefUpdated = gitRefUpdated;
     this.projectName = projectName;
     this.db = db;
+    this.batch = batch;
     this.commit = new CommitBuilder();
   }
 
+  public MetaDataUpdate(GitReferenceUpdated gitRefUpdated,
+      Project.NameKey projectName, Repository db) {
+    this(gitRefUpdated, projectName, db, null);
+  }
+
   /** Set the commit message used when committing the update. */
   public void setMessage(String message) {
     getCommitBuilder().setMessage(message);
@@ -128,6 +180,11 @@
     this.allowEmpty = allowEmpty;
   }
 
+  /** @return batch in which to run the update, or {@code null} for no batch. */
+  BatchRefUpdate getBatch() {
+    return batch;
+  }
+
   /** Close the cached Repository handle. */
   public void close() {
     getRepository().close();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index 4279b31..d081fe6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -25,14 +25,13 @@
 
 import java.io.IOException;
 import java.io.OutputStream;
-
+import java.util.List;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
-import java.util.List;
 
 /**
  * Progress reporting interface that multiplexes multiple sub-tasks.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
index f1b0a78..fb96a6b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
@@ -153,8 +153,8 @@
       }
       updateRef(notesBranch);
     } finally {
-      revWalk.release();
-      reader.release();
+      revWalk.close();
+      reader.close();
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index a905385..020c94a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -19,7 +19,7 @@
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Joiner;
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
@@ -39,9 +39,9 @@
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
 import com.google.gerrit.common.data.RefConfigSection;
-import com.google.gerrit.extensions.api.projects.ProjectState;
-import com.google.gerrit.extensions.common.InheritableBoolean;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.Project;
@@ -58,9 +58,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.util.StringUtils;
 
-import java.io.BufferedReader;
 import java.io.IOException;
-import java.io.StringReader;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -75,7 +73,7 @@
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
 
-public class ProjectConfig extends VersionedMetaData {
+public class ProjectConfig extends VersionedMetaData implements ValidationError.Sink {
   public static final String COMMENTLINK = "commentlink";
   private static final String KEY_MATCH = "match";
   private static final String KEY_HTML = "html";
@@ -83,7 +81,6 @@
   private static final String KEY_ENABLED = "enabled";
 
   public static final String PROJECT_CONFIG = "project.config";
-  private static final String GROUP_LIST = "groups";
 
   private static final String PROJECT = "project";
   private static final String KEY_DESCRIPTION = "description";
@@ -115,6 +112,8 @@
   private static final String RECEIVE = "receive";
   private static final String KEY_REQUIRE_SIGNED_OFF_BY = "requireSignedOffBy";
   private static final String KEY_REQUIRE_CHANGE_ID = "requireChangeId";
+  private static final String KEY_USE_ALL_NOT_IN_TARGET =
+      "createNewChangeForAllNotInTarget";
   private static final String KEY_MAX_OBJECT_SIZE_LIMIT = "maxObjectSizeLimit";
   private static final String KEY_REQUIRE_CONTRIBUTOR_AGREEMENT =
       "requireContributorAgreement";
@@ -135,7 +134,8 @@
   private static final String KEY_COPY_MIN_SCORE = "copyMinScore";
   private static final String KEY_COPY_MAX_SCORE = "copyMaxScore";
   private static final String KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE = "copyAllScoresOnTrivialRebase";
-  private static final String KEY_COPY_ALL_SCORES_IF_NO_CHANGE = "copyAllScoresIfNoCodeChange";
+  private static final String KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE = "copyAllScoresIfNoCodeChange";
+  private static final String KEY_COPY_ALL_SCORES_IF_NO_CHANGE = "copyAllScoresIfNoChange";
   private static final String KEY_VALUE = "value";
   private static final String KEY_CAN_OVERRIDE = "canOverride";
   private static final String KEY_Branch = "branch";
@@ -152,7 +152,7 @@
   private Project.NameKey projectName;
   private Project project;
   private AccountsSection accountsSection;
-  private Map<AccountGroup.UUID, GroupReference> groupsByUUID;
+  private GroupList groupList;
   private Map<String, AccessSection> accessSections;
   private BranchOrderSection branchOrderSection;
   private Map<String, ContributorAgreement> contributorAgreements;
@@ -220,6 +220,10 @@
     this.projectName = projectName;
   }
 
+  public Project.NameKey getName() {
+    return projectName;
+  }
+
   public Project getProject() {
     return project;
   }
@@ -318,24 +322,17 @@
   }
 
   public GroupReference resolve(GroupReference group) {
-    if (group != null) {
-      GroupReference ref = groupsByUUID.get(group.getUUID());
-      if (ref != null) {
-        return ref;
-      }
-      groupsByUUID.put(group.getUUID(), group);
-    }
-    return group;
+    return groupList.resolve(group);
   }
 
   /** @return the group reference, if the group is used by at least one rule. */
   public GroupReference getGroup(AccountGroup.UUID uuid) {
-    return groupsByUUID.get(uuid);
+    return groupList.byUUID(uuid);
   }
 
   /** @return set of all groups used by this configuration. */
   public Set<AccountGroup.UUID> getAllGroupUUIDs() {
-    return Collections.unmodifiableSet(groupsByUUID.keySet());
+    return groupList.uuids();
   }
 
   /**
@@ -369,7 +366,7 @@
    */
   public boolean updateGroupNames(GroupBackend groupBackend) {
     boolean dirty = false;
-    for (GroupReference ref : groupsByUUID.values()) {
+    for (GroupReference ref : groupList.references()) {
       GroupDescription.Basic g = groupBackend.get(ref.getUUID());
       if (g != null && !g.getName().equals(ref.getName())) {
         dirty = true;
@@ -399,7 +396,8 @@
 
   @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
-    Map<String, GroupReference> groupsByName = readGroupList();
+    readGroupList();
+    Map<String, GroupReference> groupsByName = mapGroupReferences();
 
     rulesId = getObjectId("rules.pl");
     Config rc = readConfig(PROJECT_CONFIG);
@@ -415,6 +413,7 @@
     p.setUseContributorAgreements(getEnum(rc, RECEIVE, null, KEY_REQUIRE_CONTRIBUTOR_AGREEMENT, InheritableBoolean.INHERIT));
     p.setUseSignedOffBy(getEnum(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_OFF_BY, InheritableBoolean.INHERIT));
     p.setRequireChangeID(getEnum(rc, RECEIVE, null, KEY_REQUIRE_CHANGE_ID, InheritableBoolean.INHERIT));
+    p.setCreateNewChangeForAllNotInTarget(getEnum(rc, RECEIVE, null, KEY_USE_ALL_NOT_IN_TARGET, InheritableBoolean.INHERIT));
     p.setMaxObjectSizeLimit(rc.getString(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT));
 
     p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, defaultSubmitAction));
@@ -508,8 +507,7 @@
           NOTIFY, sectionName, KEY_TYPE,
           NotifyType.ALL));
       n.setTypes(types);
-      n.setHeader(ConfigUtil.getEnum(rc,
-          NOTIFY, sectionName, KEY_HEADER,
+      n.setHeader(rc.getEnum(NOTIFY, sectionName, KEY_HEADER,
           NotifyConfig.Header.BCC));
 
       for (String dst : rc.getStringList(NOTIFY, sectionName, KEY_EMAIL)) {
@@ -524,7 +522,7 @@
             n.addEmail(ref);
           } else {
             error(new ValidationError(PROJECT_CONFIG,
-                "group \"" + ref.getName() + "\" not in " + GROUP_LIST));
+                "group \"" + ref.getName() + "\" not in " + GroupList.FILE_NAME));
           }
         } else if (dst.startsWith("user ")) {
           error(new ValidationError(PROJECT_CONFIG, dst + " not supported"));
@@ -620,7 +618,7 @@
         ref = rule.getGroup();
         groupsByName.put(ref.getName(), ref);
         error(new ValidationError(PROJECT_CONFIG,
-            "group \"" + ref.getName() + "\" not in " + GROUP_LIST));
+            "group \"" + ref.getName() + "\" not in " + GroupList.FILE_NAME));
       }
 
       rule.setGroup(ref);
@@ -641,7 +639,7 @@
         valueText);
   }
 
-  private void loadLabelSections(Config rc) throws IOException {
+  private void loadLabelSections(Config rc) {
     Map<String, String> lowerNames = Maps.newHashMapWithExpectedSize(2);
     labelSections = Maps.newLinkedHashMap();
     for (String name : rc.getSubsections(LABEL)) {
@@ -673,7 +671,7 @@
         continue;
       }
 
-      String functionName = Objects.firstNonNull(
+      String functionName = MoreObjects.firstNonNull(
           rc.getString(LABEL, name, KEY_FUNCTION), "MaxWithBlock");
       if (LABEL_FUNCTIONS.contains(functionName)) {
         label.setFunctionName(functionName);
@@ -693,15 +691,23 @@
             KEY_DEFAULT_VALUE, dv, name)));
       }
       label.setCopyMinScore(
-          rc.getBoolean(LABEL, name, KEY_COPY_MIN_SCORE, false));
+          rc.getBoolean(LABEL, name, KEY_COPY_MIN_SCORE,
+              LabelType.DEF_COPY_MIN_SCORE));
       label.setCopyMaxScore(
-          rc.getBoolean(LABEL, name, KEY_COPY_MAX_SCORE, false));
+          rc.getBoolean(LABEL, name, KEY_COPY_MAX_SCORE,
+              LabelType.DEF_COPY_MAX_SCORE));
       label.setCopyAllScoresOnTrivialRebase(
-          rc.getBoolean(LABEL, name, KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE, false));
+          rc.getBoolean(LABEL, name, KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE,
+              LabelType.DEF_COPY_ALL_SCORES_ON_TRIVIAL_REBASE));
       label.setCopyAllScoresIfNoCodeChange(
-          rc.getBoolean(LABEL, name, KEY_COPY_ALL_SCORES_IF_NO_CHANGE, false));
+          rc.getBoolean(LABEL, name, KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE,
+              LabelType.DEF_COPY_ALL_SCORES_IF_NO_CODE_CHANGE));
+      label.setCopyAllScoresIfNoChange(
+          rc.getBoolean(LABEL, name, KEY_COPY_ALL_SCORES_IF_NO_CHANGE,
+              LabelType.DEF_COPY_ALL_SCORES_IF_NO_CHANGE));
       label.setCanOverride(
-          rc.getBoolean(LABEL, name, KEY_CAN_OVERRIDE, true));
+          rc.getBoolean(LABEL, name, KEY_CAN_OVERRIDE,
+              LabelType.DEF_CAN_OVERRIDE));
       label.setRefPatterns(getStringListOrNull(rc, LABEL, name, KEY_Branch));
       labelSections.put(name, label);
     }
@@ -767,31 +773,18 @@
     return new PluginConfig(pluginName, pluginConfig, this);
   }
 
-  private Map<String, GroupReference> readGroupList() throws IOException {
-    groupsByUUID = new HashMap<>();
-    Map<String, GroupReference> groupsByName = new HashMap<>();
+  private void readGroupList() throws IOException {
+    groupList = GroupList.parse(readUTF8(GroupList.FILE_NAME), this);
+  }
 
-    BufferedReader br = new BufferedReader(new StringReader(readUTF8(GROUP_LIST)));
-    String s;
-    for (int lineNumber = 1; (s = br.readLine()) != null; lineNumber++) {
-      if (s.isEmpty() || s.startsWith("#")) {
-        continue;
-      }
-
-      int tab = s.indexOf('\t');
-      if (tab < 0) {
-        error(new ValidationError(GROUP_LIST, lineNumber, "missing tab delimiter"));
-        continue;
-      }
-
-      AccountGroup.UUID uuid = new AccountGroup.UUID(s.substring(0, tab).trim());
-      String name = s.substring(tab + 1).trim();
-      GroupReference ref = new GroupReference(uuid, name);
-
-      groupsByUUID.put(uuid, ref);
-      groupsByName.put(name, ref);
+  private Map<String, GroupReference> mapGroupReferences() {
+    Collection<GroupReference> references = groupList.references();
+    Map<String, GroupReference> result = new HashMap<>(references.size());
+    for (GroupReference ref : references) {
+      result.put(ref.getName(), ref);
     }
-    return groupsByName;
+
+    return result;
   }
 
   @Override
@@ -814,6 +807,7 @@
     set(rc, RECEIVE, null, KEY_REQUIRE_CONTRIBUTOR_AGREEMENT, p.getUseContributorAgreements(), InheritableBoolean.INHERIT);
     set(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_OFF_BY, p.getUseSignedOffBy(), InheritableBoolean.INHERIT);
     set(rc, RECEIVE, null, KEY_REQUIRE_CHANGE_ID, p.getRequireChangeID(), InheritableBoolean.INHERIT);
+    set(rc, RECEIVE, null, KEY_USE_ALL_NOT_IN_TARGET, p.getCreateNewChangeForAllNotInTarget(), InheritableBoolean.INHERIT);
     set(rc, RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT, validMaxObjectSizeLimit(p.getMaxObjectSizeLimit()));
 
     set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), defaultSubmitAction);
@@ -829,7 +823,7 @@
     saveContributorAgreements(rc, keepGroups);
     saveAccessSections(rc, keepGroups);
     saveNotifySections(rc, keepGroups);
-    groupsByUUID.keySet().retainAll(keepGroups);
+    groupList.retainUUIDs(keepGroups);
     saveLabelSections(rc);
     savePluginSections(rc);
 
@@ -1046,32 +1040,22 @@
       toUnset.remove(name);
       rc.setString(LABEL, name, KEY_FUNCTION, label.getFunctionName());
       rc.setInt(LABEL, name, KEY_DEFAULT_VALUE, label.getDefaultValue());
-      if (label.isCopyMinScore()) {
-        rc.setBoolean(LABEL, name, KEY_COPY_MIN_SCORE, true);
-      } else {
-        rc.unset(LABEL, name, KEY_COPY_MIN_SCORE);
-      }
-      if (label.isCopyMaxScore()) {
-        rc.setBoolean(LABEL, name, KEY_COPY_MAX_SCORE, true);
-      } else {
-        rc.unset(LABEL, name, KEY_COPY_MAX_SCORE);
-      }
-      if (label.isCopyAllScoresOnTrivialRebase()) {
-        rc.setBoolean(LABEL, name, KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE, true);
-      } else {
-        rc.unset(LABEL, name, KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE);
-      }
-      if (label.isCopyAllScoresIfNoCodeChange()) {
-        rc.setBoolean(LABEL, name, KEY_COPY_ALL_SCORES_IF_NO_CHANGE, true);
-      } else {
-        rc.unset(LABEL, name, KEY_COPY_ALL_SCORES_IF_NO_CHANGE);
-      }
-      if (!label.canOverride()) {
-        rc.setBoolean(LABEL, name, KEY_CAN_OVERRIDE, false);
-      } else {
-        rc.unset(LABEL, name, KEY_CAN_OVERRIDE);
-      }
 
+      setBooleanConfigKey(rc, name, KEY_COPY_MIN_SCORE, label.isCopyMinScore(),
+          LabelType.DEF_COPY_MIN_SCORE);
+      setBooleanConfigKey(rc, name, KEY_COPY_MAX_SCORE, label.isCopyMaxScore(),
+          LabelType.DEF_COPY_MAX_SCORE);
+      setBooleanConfigKey(rc, name, KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE,
+          label.isCopyAllScoresOnTrivialRebase(),
+          LabelType.DEF_COPY_ALL_SCORES_ON_TRIVIAL_REBASE);
+      setBooleanConfigKey(rc, name, KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE,
+          label.isCopyAllScoresIfNoCodeChange(),
+          LabelType.DEF_COPY_ALL_SCORES_IF_NO_CODE_CHANGE);
+      setBooleanConfigKey(rc, name, KEY_COPY_ALL_SCORES_IF_NO_CHANGE,
+          label.isCopyAllScoresIfNoChange(),
+          LabelType.DEF_COPY_ALL_SCORES_IF_NO_CHANGE);
+      setBooleanConfigKey(rc, name, KEY_CAN_OVERRIDE, label.canOverride(),
+          LabelType.DEF_CAN_OVERRIDE);
       List<String> values =
           Lists.newArrayListWithCapacity(label.getValues().size());
       for (LabelValue value : label.getValues()) {
@@ -1085,6 +1069,15 @@
     }
   }
 
+  private static void setBooleanConfigKey(
+      Config rc, String name, String key, boolean value, boolean defaultValue) {
+    if (value == defaultValue) {
+      rc.unset(LABEL, name, key);
+    } else {
+      rc.setBoolean(LABEL, name, key, value);
+    }
+  }
+
   private void savePluginSections(Config rc) {
     List<String> existing = Lists.newArrayList(rc.getSubsections(PLUGIN));
     for (String name : existing) {
@@ -1102,30 +1095,7 @@
   }
 
   private void saveGroupList() throws IOException {
-    if (groupsByUUID.isEmpty()) {
-      saveFile(GROUP_LIST, null);
-      return;
-    }
-
-    final int uuidLen = 40;
-    StringBuilder buf = new StringBuilder();
-    buf.append(pad(uuidLen, "# UUID"));
-    buf.append('\t');
-    buf.append("Group Name");
-    buf.append('\n');
-
-    buf.append('#');
-    buf.append('\n');
-
-    for (GroupReference g : sort(groupsByUUID.values())) {
-      if (g.getUUID() != null && g.getName() != null) {
-        buf.append(pad(uuidLen, g.getUUID().get()));
-        buf.append('\t');
-        buf.append(g.getName());
-        buf.append('\n');
-      }
-    }
-    saveUTF8(GROUP_LIST, buf.toString());
+    saveUTF8(GroupList.FILE_NAME, groupList.asText());
   }
 
   private <E extends Enum<?>> E getEnum(Config rc, String section,
@@ -1138,26 +1108,14 @@
     }
   }
 
-  private void error(ValidationError error) {
+  @Override
+  public void error(ValidationError error) {
     if (validationErrors == null) {
       validationErrors = new ArrayList<>(4);
     }
     validationErrors.add(error);
   }
 
-  private static String pad(int len, String src) {
-    if (len <= src.length()) {
-      return src;
-    }
-
-    StringBuilder r = new StringBuilder(len);
-    r.append(src);
-    while (r.length() < len) {
-      r.append(' ');
-    }
-    return r.toString();
-  }
-
   private static <T extends Comparable<? super T>> List<T> sort(Collection<T> m) {
     ArrayList<T> r = new ArrayList<>(m);
     Collections.sort(r);
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 cedf11a43..f83bab6 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
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.server.git;
 
+import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
+import static com.google.gerrit.server.change.HashtagsUtil.cleanupHashtag;
 import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
 import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
 import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromReviewers;
@@ -28,11 +30,13 @@
 
 import com.google.common.base.Function;
 import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.BiMap;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.HashBiMap;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Iterables;
@@ -40,6 +44,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Ordering;
 import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.CheckedFuture;
@@ -49,6 +54,7 @@
 import com.google.gerrit.common.ChangeHookRunner.HookResult;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
@@ -72,7 +78,6 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResolver;
@@ -80,13 +85,14 @@
 import com.google.gerrit.server.change.ChangeKind;
 import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.change.ChangesCollection;
-import com.google.gerrit.server.change.MergeabilityChecker;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.Submit;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.ProjectConfigEntry;
+import com.google.gerrit.server.edit.ChangeEdit;
+import com.google.gerrit.server.edit.ChangeEditUtil;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.MultiProgressMonitor.Task;
@@ -94,11 +100,12 @@
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.mail.CreateChangeSender;
 import com.google.gerrit.server.mail.MailUtil.MailRecipients;
 import com.google.gerrit.server.mail.MergedSender;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -107,11 +114,11 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
@@ -131,7 +138,6 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.notes.NoteMap;
-import org.eclipse.jgit.revwalk.FooterKey;
 import org.eclipse.jgit.revwalk.FooterLine;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
@@ -176,8 +182,6 @@
   public static final Pattern NEW_PATCHSET = Pattern.compile(
       "^" + REFS_CHANGES + "(?:[0-9][0-9]/)?([1-9][0-9]*)(?:/new)?$");
 
-  private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
-
   private static final String COMMAND_REJECTION_MESSAGE_FOOTER =
       "Please read the documentation and contact an administrator\n"
           + "if you feel the configuration is incorrect";
@@ -271,12 +275,12 @@
 
   private final IdentifiedUser currentUser;
   private final ReviewDb db;
+  private final Provider<InternalChangeQuery> queryProvider;
   private final ChangeData.Factory changeDataFactory;
   private final ChangeUpdate.Factory updateFactory;
   private final SchemaFactory<ReviewDb> schemaFactory;
   private final AccountResolver accountResolver;
   private final CmdLineParser.Factory optionParserFactory;
-  private final CreateChangeSender.Factory createChangeSenderFactory;
   private final MergedSender.Factory mergedSenderFactory;
   private final ReplacePatchSetSender.Factory replacePatchSetFactory;
   private final GitReferenceUpdated gitRefUpdated;
@@ -297,7 +301,6 @@
   private final ListeningExecutorService changeUpdateExector;
   private final RequestScopePropagator requestScopePropagator;
   private final ChangeIndexer indexer;
-  private final MergeabilityChecker mergeabilityChecker;
   private final SshInfo sshInfo;
   private final AllProjectsName allProjectsName;
   private final ReceiveConfig receiveConfig;
@@ -310,12 +313,11 @@
   private final ReceivePack rp;
   private final NoteMap rejectCommits;
   private MagicBranchInput magicBranch;
+  private boolean newChangeForAllNotInTarget;
 
   private List<CreateRequest> newChanges = Collections.emptyList();
   private final Map<Change.Id, ReplaceRequest> replaceByChange =
       new HashMap<>();
-  private final Map<RevCommit, ReplaceRequest> replaceByCommit =
-      new HashMap<>();
   private final Set<RevCommit> validCommits = new HashSet<>();
 
   private ListMultimap<Change.Id, Ref> refsByChange;
@@ -326,6 +328,8 @@
   private final Provider<Submit> submitProvider;
   private final MergeQueue mergeQueue;
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
+  private final NotesMigration notesMigration;
+  private final ChangeEditUtil editUtil;
 
   private final List<CommitValidationMessage> messages = new ArrayList<>();
   private ListMultimap<Error, String> errors = LinkedListMultimap.create();
@@ -338,12 +342,12 @@
 
   @Inject
   ReceiveCommits(final ReviewDb db,
+      final Provider<InternalChangeQuery> queryProvider,
       final SchemaFactory<ReviewDb> schemaFactory,
       final ChangeData.Factory changeDataFactory,
       final ChangeUpdate.Factory updateFactory,
       final AccountResolver accountResolver,
       final CmdLineParser.Factory optionParserFactory,
-      final CreateChangeSender.Factory createChangeSenderFactory,
       final MergedSender.Factory mergedSenderFactory,
       final ReplacePatchSetSender.Factory replacePatchSetFactory,
       final GitReferenceUpdated gitRefUpdated,
@@ -361,12 +365,10 @@
       final ChangeInserter.Factory changeInserterFactory,
       final CommitValidators.Factory commitValidatorsFactory,
       @CanonicalWebUrl final String canonicalWebUrl,
-      @GerritPersonIdent final PersonIdent gerritIdent,
       final WorkQueue workQueue,
       @ChangeUpdateExecutor ListeningExecutorService changeUpdateExector,
       final RequestScopePropagator requestScopePropagator,
       final ChangeIndexer indexer,
-      final MergeabilityChecker mergeabilityChecker,
       final SshInfo sshInfo,
       final AllProjectsName allProjectsName,
       ReceiveConfig config,
@@ -376,15 +378,17 @@
       final Provider<Submit> submitProvider,
       final MergeQueue mergeQueue,
       final ChangeKindCache changeKindCache,
-      final DynamicMap<ProjectConfigEntry> pluginConfigEntries) throws IOException {
+      final DynamicMap<ProjectConfigEntry> pluginConfigEntries,
+      final NotesMigration notesMigration,
+      final ChangeEditUtil editUtil) throws IOException {
     this.currentUser = (IdentifiedUser) projectControl.getCurrentUser();
     this.db = db;
+    this.queryProvider = queryProvider;
     this.changeDataFactory = changeDataFactory;
     this.updateFactory = updateFactory;
     this.schemaFactory = schemaFactory;
     this.accountResolver = accountResolver;
     this.optionParserFactory = optionParserFactory;
-    this.createChangeSenderFactory = createChangeSenderFactory;
     this.mergedSenderFactory = mergedSenderFactory;
     this.replacePatchSetFactory = replacePatchSetFactory;
     this.gitRefUpdated = gitRefUpdated;
@@ -405,7 +409,6 @@
     this.changeUpdateExector = changeUpdateExector;
     this.requestScopePropagator = requestScopePropagator;
     this.indexer = indexer;
-    this.mergeabilityChecker = mergeabilityChecker;
     this.sshInfo = sshInfo;
     this.allProjectsName = allProjectsName;
     this.receiveConfig = config;
@@ -416,17 +419,21 @@
     this.project = projectControl.getProject();
     this.repo = repo;
     this.rp = new ReceivePack(repo);
-    this.rejectCommits = BanCommit.loadRejectCommitsMap(repo);
+    this.rejectCommits = BanCommit.loadRejectCommitsMap(repo, rp.getRevWalk());
 
     this.subOpFactory = subOpFactory;
     this.submitProvider = submitProvider;
     this.mergeQueue = mergeQueue;
     this.pluginConfigEntries = pluginConfigEntries;
+    this.notesMigration = notesMigration;
+
+    this.editUtil = editUtil;
 
     this.messageSender = new ReceivePackMessageSender();
 
     ProjectState ps = projectControl.getProjectState();
 
+    this.newChangeForAllNotInTarget = ps.isCreateNewChangeForAllNotInTarget();
     rp.setAllowCreates(true);
     rp.setAllowDeletes(true);
     rp.setAllowNonFastForwards(true);
@@ -476,7 +483,7 @@
     });
     advHooks.add(rp.getAdvertiseRefsHook());
     advHooks.add(new ReceiveCommitsAdvertiseRefsHook(
-        db, projectControl.getProject().getNameKey()));
+        db, queryProvider, projectControl.getProject().getNameKey()));
     rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
   }
 
@@ -554,12 +561,16 @@
 
     parseCommands(commands);
     if (magicBranch != null && magicBranch.cmd.getResult() == NOT_ATTEMPTED) {
-      newChanges = selectNewChanges();
+      selectNewAndReplacedChangesFromMagicBranch();
     }
     preparePatchSetsForReplace();
 
     if (!batch.getCommands().isEmpty()) {
       try {
+        if (!batch.isAllowNonFastForwards() && magicBranch != null
+            && magicBranch.edit) {
+          batch.setAllowNonFastForwards(true);
+        }
         batch.execute(rp.getRevWalk(), commandProgress);
       } catch (IOException err) {
         int cnt = 0;
@@ -588,28 +599,19 @@
 
     for (final ReceiveCommand c : commands) {
         if (c.getResult() == OK) {
-          try {
+          if (c.getType() == ReceiveCommand.Type.UPDATE) { // aka fast-forward
+              tagCache.updateFastForward(project.getNameKey(),
+                  c.getRefName(),
+                  c.getOldId(),
+                  c.getNewId());
+          }
+
+          if (isHead(c) || isConfig(c)) {
             switch (c.getType()) {
               case CREATE:
-                if (isHead(c) || isConfig(c)) {
-                  autoCloseChanges(c);
-                }
-                break;
-
-              case UPDATE: // otherwise known as a fast-forward
-                tagCache.updateFastForward(project.getNameKey(),
-                    c.getRefName(),
-                    c.getOldId(),
-                    c.getNewId());
-                if (isHead(c) || isConfig(c)) {
-                  autoCloseChanges(c);
-                }
-                break;
-
+              case UPDATE:
               case UPDATE_NONFASTFORWARD:
-                if (isHead(c) || isConfig(c)) {
-                  autoCloseChanges(c);
-                }
+                autoCloseChanges(c);
                 break;
 
               case DELETE:
@@ -626,36 +628,36 @@
                 }
                 break;
             }
+          }
 
-            if (isConfig(c)) {
-              projectCache.evict(project);
-              ProjectState ps = projectCache.get(project.getNameKey());
-              repoManager.setProjectDescription(project.getNameKey(), //
-                  ps.getProject().getDescription());
-            }
+          if (isConfig(c)) {
+            projectCache.evict(project);
+            ProjectState ps = projectCache.get(project.getNameKey());
+            repoManager.setProjectDescription(project.getNameKey(), //
+                ps.getProject().getDescription());
+          }
 
-            if (!MagicBranch.isMagicBranch(c.getRefName())) {
-              // We only fire gitRefUpdated for direct refs updates.
-              // Events for change refs are fired when they are created.
-              //
-              gitRefUpdated.fire(project.getNameKey(), c.getRefName(),
-                  c.getOldId(), c.getNewId());
-              hooks.doRefUpdatedHook(
-                  new Branch.NameKey(project.getNameKey(), c.getRefName()),
-                  c.getOldId(),
-                  c.getNewId(),
-                  currentUser.getAccount());
-            }
-          } catch (NoSuchChangeException e) {
-            c.setResult(REJECTED_OTHER_REASON,
-                "No such change: " + e.getMessage());
+          if (!MagicBranch.isMagicBranch(c.getRefName())) {
+            // We only fire gitRefUpdated for direct refs updates.
+            // Events for change refs are fired when they are created.
+            //
+            gitRefUpdated.fire(project.getNameKey(), c.getRefName(),
+                c.getOldId(), c.getNewId());
+            hooks.doRefUpdatedHook(
+                new Branch.NameKey(project.getNameKey(), c.getRefName()),
+                c.getOldId(),
+                c.getNewId(),
+                currentUser.getAccount());
           }
         }
     }
     closeProgress.end();
     commandProgress.end();
     progress.end();
+    reportMessages();
+  }
 
+  private void reportMessages() {
     Iterable<CreateRequest> created =
         Iterables.filter(newChanges, new Predicate<CreateRequest>() {
           @Override
@@ -667,30 +669,39 @@
       addMessage("");
       addMessage("New Changes:");
       for (CreateRequest c : created) {
-        addMessage(formatChangeUrl(canonicalWebUrl, c.change));
+        addMessage(formatChangeUrl(canonicalWebUrl, c.change, false));
       }
       addMessage("");
     }
 
-    Iterable<ReplaceRequest> updated =
-        Iterables.filter(replaceByChange.values(),
-            new Predicate<ReplaceRequest>() {
+    List<ReplaceRequest> updated = FluentIterable
+        .from(replaceByChange.values())
+        .filter(new Predicate<ReplaceRequest>() {
+          @Override
+          public boolean apply(ReplaceRequest input) {
+            return !input.skip && input.inputCommand.getResult() == OK;
+          }
+        })
+        .toSortedList(Ordering.natural().onResultOf(
+            new Function<ReplaceRequest, Integer>() {
               @Override
-              public boolean apply(ReplaceRequest input) {
-                return !input.skip && input.inputCommand.getResult() == OK;
+              public Integer apply(ReplaceRequest in) {
+                return in.change.getId().get();
               }
-            });
-    if (!Iterables.isEmpty(updated)) {
+            }));
+    if (!updated.isEmpty()) {
       addMessage("");
       addMessage("Updated Changes:");
+      boolean edit = magicBranch != null && magicBranch.edit;
       for (ReplaceRequest u : updated) {
-        addMessage(formatChangeUrl(canonicalWebUrl, u.change));
+        addMessage(formatChangeUrl(canonicalWebUrl, u.change, edit));
       }
       addMessage("");
     }
   }
 
-  private static String formatChangeUrl(String url, Change change) {
+  private static String formatChangeUrl(String url, Change change,
+      boolean edit) {
     StringBuilder m = new StringBuilder()
         .append("  ")
         .append(url)
@@ -700,6 +711,9 @@
     if (change.getStatus() == Change.Status.DRAFT) {
       m.append(" [DRAFT]");
     }
+    if (edit) {
+      m.append(" [EDIT]");
+    }
     return m.toString();
   }
 
@@ -1003,7 +1017,8 @@
     }
 
     RefControl ctl = projectControl.controlForRef(cmd.getRefName());
-    if (ctl.canCreate(rp.getRevWalk(), obj, allRefs.values().contains(obj))) {
+    rp.getRevWalk().reset();
+    if (ctl.canCreate(db, rp.getRevWalk(), obj)) {
       validateNewCommits(ctl, cmd);
       batch.addCommand(cmd);
     } else {
@@ -1107,6 +1122,8 @@
     List<RevCommit> baseCommit;
     LabelTypes labelTypes;
     CmdLineParser clp;
+    Set<String> hashtags = new HashSet<>();
+    NotesMigration notesMigration;
 
     @Option(name = "--base", metaVar = "BASE", usage = "merge base of changes")
     List<ObjectId> base;
@@ -1117,10 +1134,14 @@
     @Option(name = "--draft", usage = "mark new/updated changes as draft")
     boolean draft;
 
+    @Option(name = "--edit", aliases = {"-e"}, usage = "upload as change edit")
+    boolean edit;
+
     @Option(name = "--submit", usage = "immediately submit the change")
     boolean submit;
 
-    @Option(name = "-r", metaVar = "EMAIL", usage = "add reviewer to changes")
+    @Option(name = "--reviewer", aliases = {"-r"}, metaVar = "EMAIL",
+        usage = "add reviewer to changes")
     void reviewer(Account.Id id) {
       reviewer.add(id);
     }
@@ -1135,41 +1156,45 @@
       draft = !publish;
     }
 
-    @Option(name = "-l", metaVar = "LABEL+VALUE",
+    @Option(name = "--label", aliases = {"-l"}, metaVar = "LABEL+VALUE",
         usage = "label(s) to assign (defaults to +1 if no value provided")
     void addLabel(final String token) throws CmdLineException {
       LabelVote v = LabelVote.parse(token);
       try {
-        LabelType.checkName(v.getLabel());
-        ApprovalsUtil.checkLabel(labelTypes, v.getLabel(), v.getValue());
+        LabelType.checkName(v.label());
+        ApprovalsUtil.checkLabel(labelTypes, v.label(), v.value());
       } catch (IllegalArgumentException e) {
         throw clp.reject(e.getMessage());
       }
-      labels.put(v.getLabel(), v.getValue());
+      labels.put(v.label(), v.value());
     }
 
-    MagicBranchInput(ReceiveCommand cmd, LabelTypes labelTypes) {
+    @Option(name = "--hashtag", aliases = {"-t"}, metaVar = "HASHTAG",
+        usage = "add hashtag to changes")
+    void addHashtag(String token) throws CmdLineException {
+      if (!notesMigration.enabled()) {
+        throw clp.reject("cannot add hashtags; noteDb is disabled");
+      }
+      String hashtag = cleanupHashtag(token);
+      if (!hashtag.isEmpty()) {
+        hashtags.add(hashtag);
+      }
+      //TODO(dpursehouse): validate hashtags
+    }
+
+    @Inject
+    MagicBranchInput(ReceiveCommand cmd, LabelTypes labelTypes,
+        NotesMigration notesMigration) {
       this.cmd = cmd;
       this.draft = cmd.getRefName().startsWith(MagicBranch.NEW_DRAFT_CHANGE);
       this.labelTypes = labelTypes;
-    }
-
-    boolean isDraft() {
-      return draft;
-    }
-
-    boolean isSubmit() {
-      return submit;
+      this.notesMigration = notesMigration;
     }
 
     MailRecipients getMailRecipients() {
       return new MailRecipients(reviewer, cc);
     }
 
-    Map<String, Short> getLabels() {
-      return labels;
-    }
-
     String parse(CmdLineParser clp, Repository repo, Set<String> refs)
         throws CmdLineException {
       String ref = MagicBranch.getDestBranchName(cmd.getRefName());
@@ -1212,10 +1237,6 @@
       }
       return ref.substring(0, split);
     }
-
-    void setCmdLineParser(CmdLineParser clp) {
-      this.clp = clp;
-    }
   }
 
   private void parseMagicBranch(final ReceiveCommand cmd) {
@@ -1225,13 +1246,13 @@
       return;
     }
 
-    magicBranch = new MagicBranchInput(cmd, labelTypes);
+    magicBranch = new MagicBranchInput(cmd, labelTypes, notesMigration);
     magicBranch.reviewer.addAll(reviewersFromCommandLine);
     magicBranch.cc.addAll(ccFromCommandLine);
 
     String ref;
     CmdLineParser clp = optionParserFactory.create(magicBranch);
-    magicBranch.setCmdLineParser(clp);
+    magicBranch.clp = clp;
     try {
       ref = magicBranch.parse(clp, repo, rp.getAdvertisedRefs().keySet());
     } catch (CmdLineException e) {
@@ -1266,13 +1287,17 @@
       return;
     }
 
-    if (magicBranch.isDraft()
-        && (!receiveConfig.allowDrafts
-            || projectControl.controlForRef("refs/drafts/" + ref)
-            .isBlocked(Permission.PUSH))) {
-      errors.put(Error.CODE_REVIEW, ref);
-      reject(cmd, "cannot upload drafts");
-      return;
+    if (magicBranch.draft) {
+      if (!receiveConfig.allowDrafts) {
+        errors.put(Error.CODE_REVIEW, ref);
+        reject(cmd, "draft workflow is disabled");
+        return;
+      } else if (projectControl.controlForRef("refs/drafts/" + ref)
+          .isBlocked(Permission.PUSH)) {
+        errors.put(Error.CODE_REVIEW, ref);
+        reject(cmd, "cannot upload drafts");
+        return;
+      }
     }
 
     if (!magicBranch.ctl.canUpload()) {
@@ -1281,18 +1306,35 @@
       return;
     }
 
-    if (magicBranch.isDraft() && magicBranch.isSubmit()) {
+    if (magicBranch.draft && magicBranch.submit) {
       reject(cmd, "cannot submit draft");
       return;
     }
 
-    if (magicBranch.isSubmit() && !projectControl.controlForRef(
+    if (magicBranch.submit && !projectControl.controlForRef(
         MagicBranch.NEW_CHANGE + ref).canSubmit()) {
       reject(cmd, "submit not allowed");
       return;
     }
 
     RevWalk walk = rp.getRevWalk();
+    RevCommit tip;
+    try {
+      tip = walk.parseCommit(magicBranch.cmd.getNewId());
+    } catch (IOException ex) {
+      magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
+      log.error("Invalid pack upload; one or more objects weren't sent", ex);
+      return;
+    }
+
+    // If tip is a merge commit, or the root commit or
+    // if %base was specified, ignore newChangeForAllNotInTarget
+    if (tip.getParentCount() > 1
+        || magicBranch.base != null
+        || tip.getParentCount() == 0) {
+      newChangeForAllNotInTarget = false;
+    }
+
     if (magicBranch.base != null) {
       magicBranch.baseCommit = Lists.newArrayListWithCapacity(
           magicBranch.base.size());
@@ -1313,6 +1355,18 @@
           return;
         }
       }
+    } else if (newChangeForAllNotInTarget) {
+      String destBranch = magicBranch.dest.get();
+      try {
+        ObjectId baseHead = repo.getRef(destBranch).getObjectId();
+        magicBranch.baseCommit =
+            Collections.singletonList(walk.parseCommit(baseHead));
+      } catch (IOException ex) {
+        log.warn(String.format("Project %s cannot read %s", project.getName(),
+            destBranch), ex);
+        reject(cmd, "internal server error");
+        return;
+      }
     }
 
     // Validate that the new commits are connected with the target
@@ -1321,7 +1375,6 @@
     // commits and the target branch head.
     //
     try {
-      final RevCommit tip = walk.parseCommit(magicBranch.cmd.getNewId());
       Ref targetRef = rp.getAdvertisedRefs().get(magicBranch.ctl.getRefName());
       if (targetRef == null || targetRef.getObjectId() == null) {
         // The destination branch does not yet exist. Assume the
@@ -1408,29 +1461,22 @@
       reject(cmd, "duplicate request");
       return false;
     }
-    if (replaceByCommit.containsKey(req.newCommit)) {
-      reject(cmd, "duplicate request");
-      return false;
-    }
     replaceByChange.put(req.ontoChange, req);
-    replaceByCommit.put(req.newCommit, req);
     return true;
   }
 
-  private List<CreateRequest> selectNewChanges() {
-    final List<CreateRequest> newChanges = Lists.newArrayList();
+  private void selectNewAndReplacedChangesFromMagicBranch() {
+    newChanges = Lists.newArrayList();
     final RevWalk walk = rp.getRevWalk();
     walk.reset();
     walk.sort(RevSort.TOPO);
     walk.sort(RevSort.REVERSE, true);
     try {
-      Set<ObjectId> existing = Sets.newHashSet();
       walk.markStart(walk.parseCommit(magicBranch.cmd.getNewId()));
       if (magicBranch.baseCommit != null) {
         for (RevCommit c : magicBranch.baseCommit) {
           walk.markUninteresting(c);
         }
-        assert magicBranch.ctl != null;
         Ref targetRef = allRefs.get(magicBranch.ctl.getRefName());
         if (targetRef != null) {
           walk.markUninteresting(walk.parseCommit(targetRef.getObjectId()));
@@ -1438,27 +1484,34 @@
       } else {
         markHeadsAsUninteresting(
             walk,
-            existing,
             magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
       }
 
+      Set<ObjectId> existing = changeRefsById().keySet();
       List<ChangeLookup> pending = Lists.newArrayList();
       final Set<Change.Key> newChangeIds = new HashSet<>();
+      final int maxBatchChanges =
+          receiveConfig.getEffectiveMaxBatchChangesLimit(currentUser);
       for (;;) {
         final RevCommit c = walk.next();
         if (c == null) {
           break;
         }
-        if (existing.contains(c) || replaceByCommit.containsKey(c)) {
-          // This commit was already scheduled to replace an existing PatchSet.
-          //
+        if (existing.contains(c)) { // Commit is already tracked.
           continue;
         }
 
         if (!validCommit(magicBranch.ctl, magicBranch.cmd, c)) {
           // Not a change the user can propose? Abort as early as possible.
-          //
-          return Collections.emptyList();
+          newChanges = Collections.emptyList();
+          return;
+        }
+
+        // Don't allow merges to be uploaded in commit chain via all-not-in-target
+        if (newChangeForAllNotInTarget && c.getParentCount() > 1) {
+          reject(magicBranch.cmd,
+              "Pushing merges in commit chains with 'all not in target' is not allowed,\n"
+            + "to override please set the base manually");
         }
 
         Change.Key changeKey = new Change.Key("I" + c.name());
@@ -1472,20 +1525,30 @@
         if (idStr.matches("^I00*$")) {
           // Reject this invalid line from EGit.
           reject(magicBranch.cmd, "invalid Change-Id");
-          return Collections.emptyList();
+          newChanges = Collections.emptyList();
+          return;
         }
 
         changeKey = new Change.Key(idStr);
         pending.add(new ChangeLookup(c, changeKey));
+        if (maxBatchChanges != 0
+            && pending.size() + newChanges.size() > maxBatchChanges) {
+          reject(magicBranch.cmd,
+              "the number of pushed changes in a batch exceeds the max limit "
+                  + maxBatchChanges);
+          newChanges = Collections.emptyList();
+          return;
+        }
       }
 
       for (ChangeLookup p : pending) {
         if (newChangeIds.contains(p.changeKey)) {
           reject(magicBranch.cmd, "squash commits first");
-          return Collections.emptyList();
+          newChanges = Collections.emptyList();
+          return;
         }
 
-        List<Change> changes = p.changes.toList();
+        List<ChangeData> changes = p.destChanges;
         if (changes.size() > 1) {
           // WTF, multiple changes in this project have the same key?
           // Since the commit is new, the user should recreate it with
@@ -1493,23 +1556,27 @@
           // this error message as Change-Id should be unique.
           //
           reject(magicBranch.cmd, p.changeKey.get() + " has duplicates");
-          return Collections.emptyList();
+          newChanges = Collections.emptyList();
+          return;
         }
 
         if (changes.size() == 1) {
           // Schedule as a replacement to this one matching change.
           //
-          if (requestReplace(magicBranch.cmd, false, changes.get(0), p.commit)) {
+          if (requestReplace(
+              magicBranch.cmd, false, changes.get(0).change(), p.commit)) {
             continue;
           } else {
-            return Collections.emptyList();
+            newChanges = Collections.emptyList();
+            return;
           }
         }
 
         if (changes.size() == 0) {
           if (!isValidChangeId(p.changeKey.get())) {
             reject(magicBranch.cmd, "invalid Change-Id");
-            return Collections.emptyList();
+            newChanges = Collections.emptyList();
+            return;
           }
 
           newChangeIds.add(p.changeKey);
@@ -1522,40 +1589,37 @@
       //
       magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
       log.error("Invalid pack upload; one or more objects weren't sent", e);
-      return Collections.emptyList();
+      newChanges = Collections.emptyList();
+      return;
     } catch (OrmException e) {
       log.error("Cannot query database to locate prior changes", e);
       reject(magicBranch.cmd, "database error");
-      return Collections.emptyList();
+      newChanges = Collections.emptyList();
+      return;
     }
 
     if (newChanges.isEmpty() && replaceByChange.isEmpty()) {
       reject(magicBranch.cmd, "no new changes");
-      return Collections.emptyList();
+      return;
+    }
+    if (!newChanges.isEmpty() && magicBranch.edit) {
+      reject(magicBranch.cmd, "edit is not supported for new changes");
+      return;
     }
     for (CreateRequest create : newChanges) {
       batch.addCommand(create.cmd);
     }
-    return newChanges;
   }
 
-  private void markHeadsAsUninteresting(
-      final RevWalk walk,
-      Set<ObjectId> existing,
-      @Nullable String forRef) {
+  private void markHeadsAsUninteresting(RevWalk rw, @Nullable String forRef) {
     for (Ref ref : allRefs.values()) {
-      if (ref.getObjectId() == null) {
-        continue;
-      } else if (ref.getName().startsWith(REFS_CHANGES)) {
-        existing.add(ref.getObjectId());
-      } else if (ref.getName().startsWith(R_HEADS)
-          || (forRef != null && forRef.equals(ref.getName()))) {
+      if ((ref.getName().startsWith(R_HEADS) || ref.getName().equals(forRef))
+          && ref.getObjectId() != null) {
         try {
-          walk.markUninteresting(walk.parseCommit(ref.getObjectId()));
+          rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
         } catch (IOException e) {
           log.warn(String.format("Invalid ref %s in %s",
               ref.getName(), project.getName()), e);
-          continue;
         }
       }
     }
@@ -1568,12 +1632,12 @@
   private class ChangeLookup {
     final RevCommit commit;
     final Change.Key changeKey;
-    final ResultSet<Change> changes;
+    final List<ChangeData> destChanges;
 
     ChangeLookup(RevCommit c, Change.Key key) throws OrmException {
       commit = c;
       changeKey = key;
-      changes = db.changes().byBranchKey(magicBranch.dest, key);
+      destChanges = queryProvider.get().byBranchKey(magicBranch.dest, key);
     }
   }
 
@@ -1593,8 +1657,8 @@
           magicBranch.dest,
           TimeUtil.nowTs());
       change.setTopic(magicBranch.topic);
-      ins = changeInserterFactory.create(ctl, change, c)
-          .setDraft(magicBranch.isDraft());
+      ins = changeInserterFactory.create(ctl.getProjectControl(), change, c)
+          .setDraft(magicBranch.draft);
       cmd = new ReceiveCommand(ObjectId.zeroId(), c,
           ins.getPatchSet().getRefName());
     }
@@ -1634,7 +1698,8 @@
       Map<String, Short> approvals = new HashMap<>();
       if (magicBranch != null) {
         recipients.add(magicBranch.getMailRecipients());
-        approvals = magicBranch.getLabels();
+        approvals = magicBranch.labels;
+        ins.setHashtags(magicBranch.hashtags);
       }
       recipients.add(getRecipientsFromFooters(accountResolver, ps, footerLines));
       recipients.remove(me);
@@ -1646,36 +1711,15 @@
 
       ins
         .setReviewers(recipients.getReviewers())
+        .setExtraCC(recipients.getCcOnly())
         .setApprovals(approvals)
         .setMessage(msg)
-        .setSendMail(false)
+        .setRequestScopePropagator(requestScopePropagator)
+        .setSendMail(true)
         .insert();
       created = true;
 
-      workQueue.getDefaultQueue()
-          .submit(requestScopePropagator.wrap(new Runnable() {
-        @Override
-        public void run() {
-          try {
-            CreateChangeSender cm =
-                createChangeSenderFactory.create(change);
-            cm.setFrom(me);
-            cm.setPatchSet(ps, ins.getPatchSetInfo());
-            cm.addReviewers(recipients.getReviewers());
-            cm.addExtraCC(recipients.getCcOnly());
-            cm.send();
-          } catch (Exception e) {
-            log.error("Cannot send email for new change " + change.getId(), e);
-          }
-        }
-
-        @Override
-        public String toString() {
-          return "send-email newchange";
-        }
-      }));
-
-      if (magicBranch != null && magicBranch.isSubmit()) {
+      if (magicBranch != null && magicBranch.submit) {
         submit(projectControl.controlFor(change), ps);
       }
     }
@@ -1712,6 +1756,7 @@
             addMessage("Change " + c.getChangeId() + ": " + msg.getMessage());
             break;
           }
+          //$FALL-THROUGH$
         default:
           addMessage("change " + c.getChangeId() + " is "
               + c.getStatus().name().toLowerCase());
@@ -1729,7 +1774,6 @@
           req.validate(false);
           if (req.skip && req.cmd == null) {
             itr.remove();
-            replaceByCommit.remove(req.newCommit);
           }
         }
       }
@@ -1755,6 +1799,9 @@
 
     for (ReplaceRequest req : replaceByChange.values()) {
       if (req.inputCommand.getResult() == NOT_ATTEMPTED && req.cmd != null) {
+        if (req.prev != null) {
+          batch.addCommand(req.prev);
+        }
         batch.addCommand(req.cmd);
       }
     }
@@ -1795,6 +1842,7 @@
     ChangeControl changeCtl;
     BiMap<RevCommit, PatchSet.Id> revisions;
     PatchSet newPatchSet;
+    ReceiveCommand prev;
     ReceiveCommand cmd;
     PatchSetInfo info;
     ChangeMessage msg;
@@ -1917,13 +1965,66 @@
         }
       }
 
+      if (magicBranch != null && magicBranch.edit) {
+        return newEdit();
+      }
+
+      newPatchSet();
+      return true;
+    }
+
+    private boolean newEdit() {
+      newPatchSet = new PatchSet(change.currentPatchSetId());
+      Optional<ChangeEdit> edit = null;
+
+      try {
+        edit = editUtil.byChange(change, currentUser);
+      } catch (IOException e) {
+        log.error("Cannt retrieve edit", e);
+        return false;
+      }
+
+      if (edit.isPresent()) {
+        if (edit.get().getBasePatchSet().getId().equals(newPatchSet.getId())) {
+          // replace edit
+          cmd = new ReceiveCommand(
+              edit.get().getRef().getObjectId(),
+              newCommit,
+              edit.get().getRefName());
+        } else {
+          // delete old edit ref on rebase
+          prev = new ReceiveCommand(
+              edit.get().getRef().getObjectId(),
+              ObjectId.zeroId(),
+              edit.get().getRefName());
+          createEditCommand();
+        }
+      } else {
+        createEditCommand();
+      }
+
+      return true;
+    }
+
+    private void createEditCommand() {
+      // create new edit
+      cmd = new ReceiveCommand(
+          ObjectId.zeroId(),
+          newCommit,
+          RefNames.refsEdit(
+              currentUser.getAccountId(),
+              change.getId(),
+              newPatchSet.getId()));
+    }
+
+    private void newPatchSet() {
       PatchSet.Id id =
           ChangeUtil.nextPatchSetId(allRefs, change.currentPatchSetId());
       newPatchSet = new PatchSet(id);
       newPatchSet.setCreatedOn(TimeUtil.nowTs());
       newPatchSet.setUploader(currentUser.getAccountId());
       newPatchSet.setRevision(toRevId(newCommit));
-      if (magicBranch != null && magicBranch.isDraft()) {
+      if (magicBranch != null && magicBranch.draft) {
         newPatchSet.setDraft(true);
       }
       info = patchSetInfoFactory.get(newCommit, newPatchSet.getId());
@@ -1931,7 +2032,6 @@
           ObjectId.zeroId(),
           newCommit,
           newPatchSet.getRefName());
-      return true;
     }
 
     CheckedFuture<PatchSet.Id, InsertException> insertPatchSet()
@@ -1944,7 +2044,9 @@
         @Override
         public PatchSet.Id call() throws OrmException, IOException, NoSuchChangeException {
           try {
-            if (caller == Thread.currentThread()) {
+            if (magicBranch != null && magicBranch.edit) {
+              return upsertEdit();
+            } else if (caller == Thread.currentThread()) {
               return insertPatchSet(db);
             } else {
               ReviewDb db = schemaFactory.open();
@@ -1964,17 +2066,16 @@
       return Futures.makeChecked(future, INSERT_EXCEPTION);
     }
 
-    private ChangeMessage newChangeMessage(ReviewDb db) throws OrmException {
+    private ChangeMessage newChangeMessage(ReviewDb db, ChangeKind changeKind)
+        throws OrmException {
       msg =
           new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil
               .messageUUID(db)), currentUser.getAccountId(), newPatchSet.getCreatedOn(),
               newPatchSet.getId());
-      RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
-      ChangeKind changeKind = changeKindCache.getChangeKind(
-          projectControl.getProjectState(), repo, priorCommit, newCommit);
       String message = "Uploaded patch set " + newPatchSet.getPatchSetId();
       switch (changeKind) {
         case TRIVIAL_REBASE:
+        case NO_CHANGE:
           message += ": Patch Set " + priorPatchSet.get() + " was rebased";
           break;
         case NO_CODE_CHANGE:
@@ -1988,20 +2089,37 @@
       return msg;
     }
 
+    PatchSet.Id upsertEdit() {
+      if (cmd.getResult() == NOT_ATTEMPTED) {
+        cmd.execute(rp);
+      }
+      return newPatchSet.getId();
+    }
+
     PatchSet.Id insertPatchSet(ReviewDb db) throws OrmException, IOException {
       final Account.Id me = currentUser.getAccountId();
       final List<FooterLine> footerLines = newCommit.getFooterLines();
       final MailRecipients recipients = new MailRecipients();
       Map<String, Short> approvals = new HashMap<>();
+      ChangeUpdate update = updateFactory.create(
+          changeCtl, newPatchSet.getCreatedOn());
+      update.setPatchSetId(newPatchSet.getId());
+
       if (magicBranch != null) {
         recipients.add(magicBranch.getMailRecipients());
-        approvals = magicBranch.getLabels();
+        approvals = magicBranch.labels;
+        Set<String> hashtags = magicBranch.hashtags;
+        if (!hashtags.isEmpty()) {
+          ChangeNotes notes = changeCtl.getNotes().load();
+          hashtags.addAll(notes.getHashtags());
+          update.setHashtags(hashtags);
+        }
       }
       recipients.add(getRecipientsFromFooters(accountResolver, newPatchSet, footerLines));
       recipients.remove(me);
 
-      ChangeUpdate update = updateFactory.create(changeCtl, newPatchSet.getCreatedOn());
       db.changes().beginTransaction(change.getId());
+      ChangeKind changeKind = ChangeKind.REWORK;
       try {
         change = db.changes().get(change.getId());
         if (change == null || change.getStatus().isClosed()) {
@@ -2024,10 +2142,14 @@
         approvalsUtil.addReviewers(db, update, labelTypes, change, newPatchSet,
             info, recipients.getReviewers(), oldRecipients.getAll());
         approvalsUtil.addApprovals(db, update, labelTypes, newPatchSet, info,
-            change, changeCtl, approvals);
+            changeCtl, approvals);
         recipients.add(oldRecipients);
 
-        cmUtil.addChangeMessage(db, update, newChangeMessage(db));
+        RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
+        changeKind = changeKindCache.getChangeKind(
+            projectControl.getProjectState(), repo, priorCommit, newCommit);
+
+        cmUtil.addChangeMessage(db, update, newChangeMessage(db, changeKind));
 
         if (mergedIntoRef == null) {
           // Change should be new, so it can go through review again.
@@ -2052,7 +2174,6 @@
                   } else {
                     change.setStatus(Change.Status.NEW);
                   }
-                  change.setLastSha1MergeTested(null);
                   change.setCurrentPatchSet(info);
 
                   final List<String> idList = newCommit.getFooterLines(CHANGE_ID);
@@ -2089,36 +2210,35 @@
       if (cmd.getResult() == NOT_ATTEMPTED) {
         cmd.execute(rp);
       }
-      CheckedFuture<?, IOException> f = mergeabilityChecker.newCheck()
-          .addChange(change)
-          .reindex()
-          .runAsync();
-      workQueue.getDefaultQueue()
-          .submit(requestScopePropagator.wrap(new Runnable() {
-        @Override
-        public void run() {
-          try {
-            ReplacePatchSetSender cm =
-                replacePatchSetFactory.create(change);
-            cm.setFrom(me);
-            cm.setPatchSet(newPatchSet, info);
-            cm.setChangeMessage(msg);
-            cm.addReviewers(recipients.getReviewers());
-            cm.addExtraCC(recipients.getCcOnly());
-            cm.send();
-          } catch (Exception e) {
-            log.error("Cannot send email for new patch set " + newPatchSet.getId(), e);
+      CheckedFuture<?, IOException> f = indexer.indexAsync(change.getId());
+      if (changeKind != ChangeKind.TRIVIAL_REBASE) {
+        workQueue.getDefaultQueue()
+            .submit(requestScopePropagator.wrap(new Runnable() {
+          @Override
+          public void run() {
+            try {
+              ReplacePatchSetSender cm =
+                  replacePatchSetFactory.create(change);
+              cm.setFrom(me);
+              cm.setPatchSet(newPatchSet, info);
+              cm.setChangeMessage(msg);
+              cm.addReviewers(recipients.getReviewers());
+              cm.addExtraCC(recipients.getCcOnly());
+              cm.send();
+            } catch (Exception e) {
+              log.error("Cannot send email for new patch set " + newPatchSet.getId(), e);
+            }
+            if (mergedIntoRef != null) {
+              sendMergedEmail(ReplaceRequest.this);
+            }
           }
-          if (mergedIntoRef != null) {
-            sendMergedEmail(ReplaceRequest.this);
-          }
-        }
 
-        @Override
-        public String toString() {
-          return "send-email newpatchset";
-        }
-      }));
+          @Override
+          public String toString() {
+            return "send-email newpatchset";
+          }
+        }));
+      }
       f.checkedGet();
 
       gitRefUpdated.fire(project.getNameKey(), newPatchSet.getRefName(),
@@ -2126,10 +2246,10 @@
       hooks.doPatchsetCreatedHook(change, newPatchSet, db);
       if (mergedIntoRef != null) {
         hooks.doChangeMergedHook(
-            change, currentUser.getAccount(), newPatchSet, db);
+            change, currentUser.getAccount(), newPatchSet, db, newCommit.getName());
       }
 
-      if (magicBranch != null && magicBranch.isSubmit()) {
+      if (magicBranch != null && magicBranch.submit) {
         submit(changeCtl, newPatchSet);
       }
 
@@ -2138,18 +2258,37 @@
   }
 
   private List<Ref> refs(Change.Id changeId) {
+    return refsByChange().get(changeId);
+  }
+
+  private void initChangeRefMaps() {
     if (refsByChange == null) {
       int estRefsPerChange = 4;
+      refsById = HashMultimap.create();
       refsByChange = ArrayListMultimap.create(
           allRefs.size() / estRefsPerChange,
           estRefsPerChange);
       for (Ref ref : allRefs.values()) {
-        if (ref.getObjectId() != null && PatchSet.isRef(ref.getName())) {
-          refsByChange.put(Change.Id.fromRef(ref.getName()), ref);
+        ObjectId obj = ref.getObjectId();
+        if (obj != null) {
+          PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
+          if (psId != null) {
+            refsById.put(obj, ref);
+            refsByChange.put(psId.getParentKey(), ref);
+          }
         }
       }
     }
-    return refsByChange.get(changeId);
+  }
+
+  private ListMultimap<Change.Id, Ref> refsByChange() {
+    initChangeRefMaps();
+    return refsByChange;
+  }
+
+  private SetMultimap<ObjectId, Ref> changeRefsById() {
+    initChangeRefMaps();
+    return refsById;
   }
 
   static boolean parentsEqual(RevCommit a, RevCommit b) {
@@ -2233,19 +2372,18 @@
     walk.reset();
     walk.sort(RevSort.NONE);
     try {
-      Set<ObjectId> existing = Sets.newHashSet();
       walk.markStart(walk.parseCommit(cmd.getNewId()));
-      markHeadsAsUninteresting(walk, existing, cmd.getRefName());
+      markHeadsAsUninteresting(walk, cmd.getRefName());
 
-      RevCommit c;
-      while ((c = walk.next()) != null) {
+      Set<ObjectId> existing = changeRefsById().keySet();
+      for (RevCommit c; (c = walk.next()) != null;) {
         if (existing.contains(c)) {
           continue;
         } else if (!validCommit(ctl, cmd, c)) {
           break;
         }
 
-        if (defaultName && currentUser.getEmailAddresses().contains(
+        if (defaultName && currentUser.hasEmailAddress(
               c.getCommitterIdent().getEmailAddress())) {
           try {
             Account a = db.accounts().get(currentUser.getAccountId());
@@ -2269,7 +2407,7 @@
   }
 
   private boolean validCommit(final RefControl ctl, final ReceiveCommand cmd,
-      final RevCommit c) throws MissingObjectException, IOException {
+      final RevCommit c) {
 
     if (validCommits.contains(c)) {
       return true;
@@ -2281,7 +2419,8 @@
         commitValidatorsFactory.create(ctl, sshInfo, repo);
 
     try {
-      messages.addAll(commitValidators.validateForReceiveCommits(receiveEvent));
+      messages.addAll(commitValidators.validateForReceiveCommits(
+          receiveEvent, rejectCommits));
     } catch (CommitValidationException e) {
       messages.addAll(e.getMessages());
       reject(cmd, e.getMessage());
@@ -2291,40 +2430,47 @@
     return true;
   }
 
-  private void autoCloseChanges(final ReceiveCommand cmd) throws NoSuchChangeException {
+  private void autoCloseChanges(final ReceiveCommand cmd) {
     final RevWalk rw = rp.getRevWalk();
     try {
+      RevCommit newTip = rw.parseCommit(cmd.getNewId());
+      Branch.NameKey branch =
+          new Branch.NameKey(project.getNameKey(), cmd.getRefName());
+
       rw.reset();
-      rw.markStart(rw.parseCommit(cmd.getNewId()));
+      rw.markStart(newTip);
       if (!ObjectId.zeroId().equals(cmd.getOldId())) {
         rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
       }
 
       final SetMultimap<ObjectId, Ref> byCommit = changeRefsById();
-      final Map<Change.Key, Change.Id> byKey = openChangesByKey(
-          new Branch.NameKey(project.getNameKey(), cmd.getRefName()));
+      Map<Change.Key, Change> byKey = null;
       final List<ReplaceRequest> toClose = new ArrayList<>();
-      RevCommit c;
-      while ((c = rw.next()) != null) {
-        final Set<Ref> refs = byCommit.get(c.copy());
-        for (Ref ref : refs) {
-          if (ref != null) {
-            rw.parseBody(c);
-            Change.Key closedChange =
-                closeChange(cmd, PatchSet.Id.fromRef(ref.getName()), c);
-            closeProgress.update(1);
-            if (closedChange != null) {
-              byKey.remove(closedChange);
+      for (RevCommit c; (c = rw.next()) != null;) {
+        rw.parseBody(c);
+
+        for (Ref ref : byCommit.get(c.copy())) {
+          Change.Key closedChange =
+              closeChange(cmd, PatchSet.Id.fromRef(ref.getName()), c);
+          closeProgress.update(1);
+          if (closedChange != null) {
+            if (byKey == null) {
+              byKey = openChangesByBranch(branch);
             }
+            byKey.remove(closedChange);
           }
         }
 
-        rw.parseBody(c);
         for (final String changeId : c.getFooterLines(CHANGE_ID)) {
-          final Change.Id onto = byKey.get(new Change.Key(changeId.trim()));
+          if (byKey == null) {
+            byKey = openChangesByBranch(branch);
+          }
+
+          final Change onto = byKey.get(new Change.Key(changeId.trim()));
           if (onto != null) {
-            final ReplaceRequest req = new ReplaceRequest(onto, c, cmd, false);
-            req.change = db.changes().get(onto);
+            final ReplaceRequest req =
+                new ReplaceRequest(onto.getId(), c, cmd, false);
+            req.change = onto;
             toClose.add(req);
             break;
           }
@@ -2341,18 +2487,12 @@
         }
       }
 
-      // It handles gitlinks if required.
-
-      rw.reset();
-      final RevCommit codeReviewCommit = rw.parseCommit(cmd.getNewId());
-
-      final SubmoduleOp subOp =
-          subOpFactory.create(
-              new Branch.NameKey(project.getNameKey(), cmd.getRefName()),
-              codeReviewCommit, rw, repo, project, new ArrayList<Change>(),
-              new HashMap<Change.Id, CodeReviewCommit>(),
-              currentUser.getAccount());
-      subOp.update();
+      // Update superproject gitlinks if required.
+      subOpFactory.create(
+          branch, newTip, rw, repo, project,
+          new ArrayList<Change>(),
+          new HashMap<Change.Id, CodeReviewCommit>(),
+          currentUser.getAccount()).update();
     } catch (InsertException e) {
       log.error("Can't insert patchset", e);
     } catch (IOException e) {
@@ -2393,28 +2533,16 @@
     result.mergedIntoRef = refName;
     markChangeMergedByPush(db, result, result.changeCtl);
     hooks.doChangeMergedHook(
-        change, currentUser.getAccount(), result.newPatchSet, db);
+        change, currentUser.getAccount(), result.newPatchSet, db, commit.getName());
     sendMergedEmail(result);
     return change.getKey();
   }
 
-  private SetMultimap<ObjectId, Ref> changeRefsById() throws IOException {
-    if (refsById == null) {
-      refsById =  HashMultimap.create();
-      for (Ref r : repo.getRefDatabase().getRefs(REFS_CHANGES).values()) {
-        if (PatchSet.isRef(r.getName())) {
-          refsById.put(r.getObjectId(), r);
-        }
-      }
-    }
-    return refsById;
-  }
-
-  private Map<Change.Key, Change.Id> openChangesByKey(Branch.NameKey branch)
+  private Map<Change.Key, Change> openChangesByBranch(Branch.NameKey branch)
       throws OrmException {
-    final Map<Change.Key, Change.Id> r = new HashMap<>();
-    for (Change c : db.changes().byBranchOpenAll(branch)) {
-      r.put(c.getKey(), c.getId());
+    final Map<Change.Key, Change> r = new HashMap<>();
+    for (ChangeData cd : queryProvider.get().byBranchOpen(branch)) {
+      r.put(cd.change().getKey(), cd.change());
     }
     return r;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
index 7ae8271..7095552 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
@@ -18,13 +18,15 @@
 
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
 
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -48,11 +50,14 @@
       .getLogger(ReceiveCommitsAdvertiseRefsHook.class);
 
   private final ReviewDb db;
+  private final Provider<InternalChangeQuery> queryProvider;
   private final Project.NameKey projectName;
 
   public ReceiveCommitsAdvertiseRefsHook(ReviewDb db,
+      Provider<InternalChangeQuery> queryProvider,
       Project.NameKey projectName) {
     this.db = db;
+    this.queryProvider = queryProvider;
     this.projectName = projectName;
   }
 
@@ -93,10 +98,14 @@
     Set<ObjectId> toInclude = Sets.newHashSet();
 
     // Advertise some recent open changes, in case a commit is based one.
+    final int limit = 32;
     try {
-      Set<PatchSet.Id> toGet = Sets.newHashSetWithExpectedSize(32);
-      for (Change c : db.changes().byProjectOpenNext(projectName, "z", 32)) {
-        PatchSet.Id id = c.currentPatchSetId();
+      Set<PatchSet.Id> toGet = Sets.newHashSetWithExpectedSize(limit);
+      for (ChangeData cd : queryProvider.get()
+          .enforceVisibility(true)
+          .setLimit(limit)
+          .byProjectOpen(projectName)) {
+        PatchSet.Id id = cd.change().currentPatchSetId();
         if (id != null) {
           toGet.add(id);
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
index 81ce05d..25fbfb9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
@@ -60,7 +60,7 @@
   public ListeningExecutorService createChangeUpdateExecutor(@GerritServerConfig Config config) {
     int poolSize = config.getInt("receive", null, "changeUpdateThreads", 1);
     if (poolSize <= 1) {
-      return MoreExecutors.sameThreadExecutor();
+      return MoreExecutors.newDirectExecutorService();
     }
     return MoreExecutors.listeningDecorator(
         MoreExecutors.getExitingExecutorService(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java
index 2efc94c..ac6116c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.git;
 
+import static com.google.gerrit.common.data.GlobalCapability.BATCH_CHANGES_LIMIT;
+
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -25,6 +28,7 @@
   final boolean checkMagicRefs;
   final boolean checkReferencedObjectsAreReachable;
   final boolean allowDrafts;
+  private final int systemMaxBatchChanges;
 
   @Inject
   ReceiveConfig(@GerritServerConfig Config config) {
@@ -37,5 +41,13 @@
     allowDrafts = config.getBoolean(
         "change", null, "allowDrafts",
         true);
+    systemMaxBatchChanges = config.getInt("receive", "maxBatchChanges", 0);
+  }
+
+  public int getEffectiveMaxBatchChangesLimit(CurrentUser user) {
+    if (user.getCapabilities().canPerform(BATCH_CHANGES_LIMIT)) {
+      return user.getCapabilities().getRange(BATCH_CHANGES_LIMIT).getMax();
+    }
+    return systemMaxBatchChanges;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java
index dd7a85b..734512f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java
@@ -15,11 +15,12 @@
 package com.google.gerrit.server.git;
 
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.slf4j.Logger;
@@ -32,34 +33,39 @@
   private static final Logger log =
       LoggerFactory.getLogger(ReloadSubmitQueueOp.class);
 
-  private final SchemaFactory<ReviewDb> schema;
+  private final OneOffRequestContext requestContext;
+  private final Provider<InternalChangeQuery> queryProvider;
   private final MergeQueue mergeQueue;
 
   @Inject
-  ReloadSubmitQueueOp(final WorkQueue wq, final SchemaFactory<ReviewDb> sf,
-      final MergeQueue mq) {
+  ReloadSubmitQueueOp(
+      OneOffRequestContext rc,
+      WorkQueue wq,
+      Provider<InternalChangeQuery> qp,
+      MergeQueue mq) {
     super(wq);
-    schema = sf;
+    requestContext = rc;
+    queryProvider = qp;
     mergeQueue = mq;
   }
 
+  @Override
   public void run() {
-    final HashSet<Branch.NameKey> pending = new HashSet<>();
-    try {
-      final ReviewDb c = schema.open();
-      try {
-        for (final Change change : c.changes().allSubmitted()) {
-          pending.add(change.getDest());
+    try (AutoCloseable ctx = requestContext.open()) {
+      HashSet<Branch.NameKey> pending = new HashSet<>();
+      for (ChangeData cd : queryProvider.get().allSubmitted()) {
+        try {
+          pending.add(cd.change().getDest());
+        } catch (OrmException e) {
+          log.error("Error reading submitted change", e);
         }
-      } finally {
-        c.close();
       }
-    } catch (OrmException e) {
-      log.error("Cannot reload MergeQueue", e);
-    }
 
-    for (final Branch.NameKey branch : pending) {
-      mergeQueue.schedule(branch);
+      for (Branch.NameKey branch : pending) {
+        mergeQueue.schedule(branch);
+      }
+    } catch (Exception e) {
+      log.error("Cannot reload MergeQueue", e);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
index 2b3994c..a89a89b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -74,7 +73,9 @@
 
   @Override
   public void run() {
-    Iterable<NameKey> names = tryingAgain ? retryOn : projectCache.all();
+    Iterable<Project.NameKey> names = tryingAgain
+        ? retryOn
+        : projectCache.all();
     for (Project.NameKey projectName : names) {
       ProjectConfig config = projectCache.get(projectName).getConfig();
       GroupReference ref = config.getGroup(uuid);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteMerger.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteMerger.java
index 60f1d0d..8b6da7b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteMerger.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteMerger.java
@@ -43,6 +43,7 @@
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.ObjectStream;
 import org.eclipse.jgit.notes.Note;
 import org.eclipse.jgit.notes.NoteMerger;
 import org.eclipse.jgit.util.io.UnionInputStream;
@@ -51,6 +52,7 @@
 import java.io.IOException;
 
 class ReviewNoteMerger implements NoteMerger {
+  @Override
   public Note merge(Note base, Note ours, Note theirs, ObjectReader reader,
       ObjectInserter inserter) throws IOException {
     if (ours == null) {
@@ -66,12 +68,13 @@
     ObjectLoader lo = reader.open(ours.getData());
     byte[] sep = new byte[] {'\n'};
     ObjectLoader lt = reader.open(theirs.getData());
-    UnionInputStream union = new UnionInputStream(
-        lo.openStream(),
-        new ByteArrayInputStream(sep),
-        lt.openStream());
-    ObjectId noteData = inserter.insert(Constants.OBJ_BLOB,
-        lo.getSize() + sep.length + lt.getSize(), union);
-    return new Note(ours, noteData);
+    try (ObjectStream os = lo.openStream();
+        ByteArrayInputStream b = new ByteArrayInputStream(sep);
+        ObjectStream ts = lt.openStream();
+        UnionInputStream union = new UnionInputStream(os, b, ts)) {
+      ObjectId noteData = inserter.insert(Constants.OBJ_BLOB,
+          lo.getSize() + sep.length + lt.getSize(), union);
+      return new Note(ours, noteData);
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ScanningChangeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ScanningChangeCacheImpl.java
new file mode 100644
index 0000000..2c78133
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ScanningChangeCacheImpl.java
@@ -0,0 +1,123 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static com.google.gerrit.server.git.SearchingChangeCacheImpl.ID_CACHE;
+
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+@Singleton
+public class ScanningChangeCacheImpl implements ChangeCache {
+  private static final Logger log =
+      LoggerFactory.getLogger(SearchingChangeCacheImpl.class);
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        bind(ChangeCache.class).to(ScanningChangeCacheImpl.class);
+        cache(ID_CACHE,
+            Project.NameKey.class,
+            new TypeLiteral<List<Change>>() {})
+          .maximumWeight(0)
+          .loader(Loader.class);
+      }
+    };
+  }
+
+  private final LoadingCache<Project.NameKey, List<Change>> cache;
+
+  @Inject
+  ScanningChangeCacheImpl(
+      @Named(ID_CACHE) LoadingCache<Project.NameKey, List<Change>> cache) {
+    this.cache = cache;
+  }
+
+  @Override
+  public List<Change> get(Project.NameKey name) {
+    try {
+      return cache.get(name);
+    } catch (ExecutionException e) {
+      log.warn("Cannot fetch changes for " + name, e);
+      return Collections.emptyList();
+    }
+  }
+
+  static class Loader extends CacheLoader<Project.NameKey, List<Change>> {
+    private final GitRepositoryManager repoManager;
+    private final OneOffRequestContext requestContext;
+
+    @Inject
+    Loader(GitRepositoryManager repoManager,
+        OneOffRequestContext requestContext) {
+      this.repoManager = repoManager;
+      this.requestContext = requestContext;
+    }
+
+    @Override
+    public List<Change> load(Project.NameKey key) throws Exception {
+      Repository repo = repoManager.openRepository(key);
+      try (ManualRequestContext ctx = requestContext.open()) {
+        ReviewDb db = ctx.getReviewDbProvider().get();
+        Map<String, Ref> refs =
+            repo.getRefDatabase().getRefs(RefNames.REFS_CHANGES);
+        Set<Change.Id> ids = new LinkedHashSet<>();
+        for (Ref r : refs.values()) {
+          Change.Id id = Change.Id.fromRef(r.getName());
+          if (id != null) {
+            ids.add(id);
+          }
+        }
+        List<Change> changes = new ArrayList<>(ids.size());
+         // A batch size of N may overload get(Iterable), so use something smaller,
+         // but still >1.
+        for (List<Change.Id> batch : Iterables.partition(ids, 30)) {
+          Iterables.addAll(changes, db.changes().get(batch));
+        }
+        return changes;
+      } finally {
+        repo.close();
+      }
+    }
+
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
new file mode 100644
index 0000000..233a892
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+@Singleton
+public class SearchingChangeCacheImpl
+    implements ChangeCache, GitReferenceUpdatedListener {
+  private static final Logger log =
+      LoggerFactory.getLogger(SearchingChangeCacheImpl.class);
+  static final String ID_CACHE = "changes";
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        bind(ChangeCache.class).to(SearchingChangeCacheImpl.class);
+        cache(ID_CACHE,
+            Project.NameKey.class,
+            new TypeLiteral<List<Change>>() {})
+          .maximumWeight(0)
+          .loader(Loader.class);
+      }
+    };
+  }
+
+  private final LoadingCache<Project.NameKey, List<Change>> cache;
+
+  @Inject
+  SearchingChangeCacheImpl(
+      @Named(ID_CACHE) LoadingCache<Project.NameKey, List<Change>> cache) {
+    this.cache = cache;
+  }
+
+  @Override
+  public List<Change> get(Project.NameKey name) {
+    try {
+      return cache.get(name);
+    } catch (ExecutionException e) {
+      log.warn("Cannot fetch changes for " + name, e);
+      return Collections.emptyList();
+    }
+  }
+
+  @Override
+  public void onGitReferenceUpdated(GitReferenceUpdatedListener.Event event) {
+    if (event.getRefName().startsWith(RefNames.REFS_CHANGES)) {
+      cache.invalidate(new Project.NameKey(event.getProjectName()));
+    }
+  }
+
+  static class Loader extends CacheLoader<Project.NameKey, List<Change>> {
+    private final OneOffRequestContext requestContext;
+    private final Provider<InternalChangeQuery> queryProvider;
+
+    @Inject
+    Loader(OneOffRequestContext requestContext,
+        Provider<InternalChangeQuery> queryProvider) {
+      this.requestContext = requestContext;
+      this.queryProvider = queryProvider;
+    }
+
+    @Override
+    public List<Change> load(Project.NameKey key) throws Exception {
+      try (AutoCloseable ctx = requestContext.open()) {
+        return Collections.unmodifiableList(
+            ChangeData.asChanges(queryProvider.get().byProject(key)));
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index 52e2268..3cf9fed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -207,11 +207,11 @@
           schema.submoduleSubscriptions().bySubmodule(updatedBranch).toList();
 
       if (!subscribers.isEmpty()) {
-        String msgbuf = msg;
-        if (msgbuf == null) {
-          // Initialize the message buffer
-          msgbuf = "";
-
+        // Initialize the message buffer
+        StringBuilder sb = new StringBuilder();
+        if (msg != null) {
+          sb.append(msg);
+        } else {
           // The first updatedBranch on a cascade event of automatic
           // updates of repos is added to updatedSubscribers set so
           // if we face a situation having
@@ -227,8 +227,8 @@
                 && (c.getStatusCode() == CommitMergeStatus.CLEAN_MERGE
                     || c.getStatusCode() == CommitMergeStatus.CLEAN_PICK
                     || c.getStatusCode() == CommitMergeStatus.CLEAN_REBASE)) {
-              msgbuf += "\n";
-              msgbuf += c.getFullMessage();
+              sb.append("\n")
+                .append(c.getFullMessage());
             }
           }
         }
@@ -246,7 +246,7 @@
 
             Map<Branch.NameKey, String> paths = new HashMap<>(1);
               paths.put(updatedBranch, s.getPath());
-              updateGitlinks(s.getSuperProject(), myRw, modules, paths, msgbuf);
+              updateGitlinks(s.getSuperProject(), myRw, modules, paths, sb.toString());
             }
           } catch (SubmoduleException e) {
               log.warn("Cannot update gitlinks for " + s + " due to " + e.getMessage());
@@ -332,6 +332,7 @@
       DirCacheEditor ed = dc.editor();
       for (final Map.Entry<Branch.NameKey, ObjectId> me : modules.entrySet()) {
         ed.add(new PathEdit(paths.get(me.getKey())) {
+          @Override
           public void apply(DirCacheEntry ent) {
             ent.setFileMode(FileMode.GITLINK);
             ent.setObjectId(me.getValue().copy());
@@ -357,9 +358,7 @@
       rfu.setForceUpdate(false);
       rfu.setNewObjectId(commitId);
       rfu.setExpectedOldObjectId(currentCommitId);
-      rfu
-          .setRefLogMessage("Submit to " + subscriber.getParentKey().get(),
-              true);
+      rfu.setRefLogMessage("Submit to " + subscriber.getParentKey().get(), true);
 
       switch (rfu.update()) {
         case NEW:
@@ -383,7 +382,7 @@
             + subscriber.get(), e);
     } finally {
       if (recRw != null) {
-        recRw.release();
+        recRw.close();
       }
       if (pdb != null) {
         pdb.close();
@@ -393,8 +392,7 @@
 
   private static DirCache readTree(final Repository pdb, final Ref branch)
       throws MissingObjectException, IncorrectObjectTypeException, IOException {
-    final RevWalk rw = new RevWalk(pdb);
-    try {
+    try (RevWalk rw = new RevWalk(pdb)) {
       final DirCache dc = DirCache.newInCore();
       final DirCacheBuilder b = dc.builder();
       b.addTree(new byte[0], // no prefix path
@@ -402,8 +400,6 @@
           pdb.newObjectReader(), rw.parseTree(branch.getObjectId()));
       b.finish();
       return dc;
-    } finally {
-      rw.release();
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
index 761d5d6..a003235 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
@@ -147,7 +147,7 @@
       }
     } finally {
       if (rw != null) {
-        rw.release();
+        rw.close();
       }
     }
   }
@@ -157,9 +157,8 @@
       return;
     }
 
-    TagWalk rw = new TagWalk(git);
-    rw.setRetainBody(false);
-    try {
+    try (TagWalk rw = new TagWalk(git)) {
+      rw.setRetainBody(false);
       for (Ref ref : git.getRefDatabase().getRefs(RefDatabase.ALL).values()) {
         if (skip(ref)) {
           continue;
@@ -188,8 +187,6 @@
       }
     } catch (IOException e) {
       log.warn("Error building tags for repository " + projectName, e);
-    } finally {
-      rw.release();
     }
   }
 
@@ -332,7 +329,7 @@
     }
   }
 
-  private static boolean skip(Ref ref) {
+  static boolean skip(Ref ref) {
     return ref.isSymbolic() || ref.getObjectId() == null
         || PatchSet.isRef(ref.getName());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
index d923e51..5260aab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
 import com.google.gerrit.reviewdb.client.Project;
 
 import org.eclipse.jgit.lib.Ref;
@@ -43,6 +45,13 @@
   }
 
   TagMatcher matcher(TagCache cache, Repository db, Collection<Ref> include) {
+    include = FluentIterable.from(include).filter(new Predicate<Ref>() {
+      @Override
+      public boolean apply(Ref ref) {
+        return !TagSet.skip(ref);
+      }
+    }).toList();
+
     TagSet tags = this.tags;
     if (tags == null) {
       tags = build(cache, db);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java
index e1ab41d..ad84046 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java
@@ -38,4 +38,8 @@
   public String toString() {
     return "ValidationError[" + message + "]";
   }
+
+  public interface Sink {
+    void error(ValidationError error);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
index 7828973..b905f67 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
@@ -26,6 +26,7 @@
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
@@ -40,10 +41,14 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.util.RawParseUtils;
 
+import java.io.BufferedReader;
 import java.io.IOException;
+import java.io.StringReader;
+import java.util.Objects;
 
 /**
  * Support for metadata stored within a version controlled branch.
@@ -117,7 +122,7 @@
       revision = id != null ? new RevWalk(reader).parseCommit(id) : null;
       onLoad();
     } finally {
-      reader.release();
+      reader.close();
       reader = null;
     }
   }
@@ -175,11 +180,27 @@
     void write(CommitBuilder commit) throws IOException;
     void write(VersionedMetaData config, CommitBuilder commit) throws IOException;
     RevCommit createRef(String refName) throws IOException;
+    void removeRef(String refName) throws IOException;
     RevCommit commit() throws IOException;
     RevCommit commitAt(ObjectId revision) throws IOException;
     void close();
   }
 
+  /**
+   * Open a batch of updates to the same metadata ref.
+   * <p>
+   * This allows making multiple commits to a single metadata ref, at the end of
+   * which is a single ref update. For batching together updates to multiple
+   * refs (each consisting of one or more commits against their respective
+   * refs), create the {@link MetaDataUpdate} with a {@link BatchRefUpdate}.
+   * <p>
+   * A ref update produced by this {@link BatchMetaDataUpdate} is only committed
+   * if there is no associated {@link BatchRefUpdate}. As a result, the
+   * configured ref updated event is not fired if there is an associated batch.
+   *
+   * @param update helper info about the update.
+   * @throws IOException if the update failed.
+   */
   public BatchMetaDataUpdate openUpdate(final MetaDataUpdate update) throws IOException {
     final Repository db = update.getRepository();
 
@@ -222,13 +243,23 @@
           return;
         }
 
-        ObjectId res = newTree.writeTree(inserter);
+        // Reuse tree from parent commit unless there are contents in newTree or
+        // there is no tree for a parent commit.
+        ObjectId res = newTree.getEntryCount() != 0 || srcTree == null
+            ? newTree.writeTree(inserter) : srcTree.copy();
         if (res.equals(srcTree) && !update.allowEmpty()
             && (commit.getTreeId() == null)) {
           // If there are no changes to the content, don't create the commit.
           return;
         }
 
+        // If changes are made to the DirCache and those changes are written as
+        // a commit and then the tree ID is set for the CommitBuilder, then
+        // those previous DirCache changes will be ignored and the commit's
+        // tree will be replaced with the ID in the CommitBuilder. The same is
+        // true if you explicitly set tree ID in a commit and then make changes
+        // to the DirCache; that tree ID will be ignored and replaced by that of
+        // the tree for the updated DirCache.
         if (commit.getTreeId() == null) {
           commit.setTreeId(res);
         } else {
@@ -246,23 +277,26 @@
 
       @Override
       public RevCommit createRef(String refName) throws IOException {
-        if (Objects.equal(src, revision)) {
+        if (Objects.equals(src, revision)) {
           return revision;
         }
+        return updateRef(ObjectId.zeroId(), src, refName);
+      }
 
+      @Override
+      public void removeRef(String refName) throws IOException {
         RefUpdate ru = db.updateRef(refName);
-        ru.setExpectedOldObjectId(ObjectId.zeroId());
-        ru.setNewObjectId(src);
-        ru.disableRefLog();
-        inserter.flush();
-        RefUpdate.Result result = ru.update();
+        ru.setForceUpdate(true);
+        if (revision != null) {
+          ru.setExpectedOldObjectId(revision);
+        }
+        RefUpdate.Result result = ru.delete();
         switch (result) {
-          case NEW:
-            revision = rw.parseCommit(ru.getNewObjectId());
+          case FORCED:
             update.fireGitRefUpdatedEvent(ru);
-            return revision;
+            return;
           default:
-            throw new IOException("Cannot update " + ru.getName() + " in "
+            throw new IOException("Cannot delete " + ru.getName() + " in "
                 + db.getDirectory() + ": " + ru.getResult());
         }
       }
@@ -274,47 +308,66 @@
 
       @Override
       public RevCommit commitAt(ObjectId expected) throws IOException {
-        if (Objects.equal(src, expected)) {
+        if (Objects.equals(src, expected)) {
           return revision;
         }
-
-        RefUpdate ru = db.updateRef(getRefName());
-        if (expected != null) {
-          ru.setExpectedOldObjectId(expected);
-        } else {
-          ru.setExpectedOldObjectId(ObjectId.zeroId());
-        }
-        ru.setNewObjectId(src);
-        ru.disableRefLog();
-        inserter.flush();
-
-        switch (ru.update(rw)) {
-          case NEW:
-          case FAST_FORWARD:
-            revision = rw.parseCommit(ru.getNewObjectId());
-            update.fireGitRefUpdatedEvent(ru);
-            return revision;
-
-          default:
-            throw new IOException("Cannot update " + ru.getName() + " in "
-                + db.getDirectory() + ": " + ru.getResult());
-        }
+        return updateRef(MoreObjects.firstNonNull(expected, ObjectId.zeroId()),
+            src, getRefName());
       }
 
       @Override
       public void close() {
         newTree = null;
 
+        rw.close();
         if (inserter != null) {
-          inserter.release();
+          inserter.close();
           inserter = null;
         }
 
         if (reader != null) {
-          reader.release();
+          reader.close();
           reader = null;
         }
       }
+
+      private RevCommit updateRef(AnyObjectId oldId, AnyObjectId newId,
+          String refName) throws IOException {
+        BatchRefUpdate bru = update.getBatch();
+        if (bru != null) {
+          bru.addCommand(new ReceiveCommand(
+              oldId.toObjectId(), newId.toObjectId(), refName));
+          inserter.flush();
+          revision = rw.parseCommit(newId);
+          return revision;
+        }
+
+        RefUpdate ru = db.updateRef(refName);
+        ru.setExpectedOldObjectId(oldId);
+        ru.setNewObjectId(src);
+        ru.setRefLogIdent(update.getCommitBuilder().getAuthor());
+        String message = update.getCommitBuilder().getMessage();
+        if (message == null) {
+          message = "meta data update";
+        }
+        try (BufferedReader reader = new BufferedReader(
+            new StringReader(message))) {
+          // read the subject line and use it as reflog message
+          ru.setRefLogMessage("commit: " + reader.readLine(), true);
+        }
+        inserter.flush();
+        RefUpdate.Result result = ru.update();
+        switch (result) {
+          case NEW:
+          case FAST_FORWARD:
+            revision = rw.parseCommit(ru.getNewObjectId());
+            update.fireGitRefUpdatedEvent(ru);
+            return revision;
+          default:
+            throw new IOException("Cannot update " + ru.getName() + " in "
+                + db.getDirectory() + ": " + ru.getResult());
+        }
+      }
     };
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
index a913601..9ccf153 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
@@ -16,11 +16,12 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gwtorm.server.OrmException;
 
@@ -53,40 +54,66 @@
   private final Project.NameKey projectName;
   private final ProjectControl projectCtl;
   private final ReviewDb reviewDb;
-  private final boolean showChanges;
+  private final boolean showMetadata;
 
-  public VisibleRefFilter(final TagCache tagCache, final ChangeCache changeCache,
-      final Repository db,
-      final ProjectControl projectControl, final ReviewDb reviewDb,
-      final boolean showChanges) {
+  public VisibleRefFilter(TagCache tagCache, ChangeCache changeCache,
+      Repository db, ProjectControl projectControl, ReviewDb reviewDb,
+      boolean showMetadata) {
     this.tagCache = tagCache;
     this.changeCache = changeCache;
     this.db = db;
     this.projectName = projectControl.getProject().getNameKey();
     this.projectCtl = projectControl;
     this.reviewDb = reviewDb;
-    this.showChanges = showChanges;
+    this.showMetadata = showMetadata;
   }
 
   public Map<String, Ref> filter(Map<String, Ref> refs, boolean filterTagsSeperately) {
-    if (projectCtl.allRefsAreVisibleExcept(
-        ImmutableSet.of(RefNames.REFS_CONFIG))) {
+    if (projectCtl.allRefsAreVisible(ImmutableSet.of(RefNames.REFS_CONFIG))) {
       Map<String, Ref> r = Maps.newHashMap(refs);
-      r.remove(RefNames.REFS_CONFIG);
+      if (!projectCtl.controlForRef(RefNames.REFS_CONFIG).isVisible()) {
+        r.remove(RefNames.REFS_CONFIG);
+      }
       return r;
     }
 
-    final Set<Change.Id> visibleChanges = visibleChanges();
-    final Map<String, Ref> result = new HashMap<>();
-    final List<Ref> deferredTags = new ArrayList<>();
+    Account.Id currAccountId;
+    boolean canViewMetadata;
+    if (projectCtl.getCurrentUser().isIdentifiedUser()) {
+      IdentifiedUser user = ((IdentifiedUser) projectCtl.getCurrentUser());
+      currAccountId = user.getAccountId();
+      canViewMetadata = user.getCapabilities().canAccessDatabase();
+    } else {
+      currAccountId = null;
+      canViewMetadata = false;
+    }
+
+    Set<Change.Id> visibleChanges = visibleChanges();
+    Map<String, Ref> result = new HashMap<>();
+    List<Ref> deferredTags = new ArrayList<>();
 
     for (Ref ref : refs.values()) {
+      Change.Id changeId;
+      Account.Id accountId;
       if (ref.getName().startsWith(RefNames.REFS_CACHE_AUTOMERGE)) {
         continue;
-      } else if (PatchSet.isRef(ref.getName())) {
-        // Reference to a patch set is visible if the change is visible.
+      } else if ((accountId = Account.Id.fromRef(ref.getName())) != null) {
+        // Reference related to an account is visible only for the current
+        // account.
         //
-        if (showChanges && visibleChanges.contains(Change.Id.fromRef(ref.getName()))) {
+        // TODO(dborowitz): If a ref matches an account and a change, verify
+        // both (to exclude e.g. edits on changes that the user has lost access
+        // to).
+        if (showMetadata
+            && (canViewMetadata || accountId.equals(currAccountId))) {
+          result.put(ref.getName(), ref);
+        }
+
+      } else if ((changeId = Change.Id.fromRef(ref.getName())) != null) {
+        // Reference related to a change is visible if the change is visible.
+        //
+        if (showMetadata
+            && (canViewMetadata || visibleChanges.contains(changeId))) {
           result.put(ref.getName(), ref);
         }
 
@@ -143,7 +170,7 @@
   }
 
   private Set<Change.Id> visibleChanges() {
-    if (!showChanges) {
+    if (!showMetadata) {
       return Collections.emptySet();
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
index 161fa9e..ca9b992 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
@@ -18,7 +18,7 @@
 import com.google.common.util.concurrent.ListenableFutureTask;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.inject.Inject;
@@ -314,6 +314,7 @@
       return startTime;
     }
 
+    @Override
     public boolean cancel(boolean mayInterruptIfRunning) {
       if (task.cancel(mayInterruptIfRunning)) {
         // Tiny abuse of running: if the task needs to know it was
@@ -335,35 +336,43 @@
       }
     }
 
+    @Override
     public int compareTo(Delayed o) {
       return task.compareTo(o);
     }
 
+    @Override
     public V get() throws InterruptedException, ExecutionException {
       return task.get();
     }
 
+    @Override
     public V get(long timeout, TimeUnit unit) throws InterruptedException,
         ExecutionException, TimeoutException {
       return task.get(timeout, unit);
     }
 
+    @Override
     public long getDelay(TimeUnit unit) {
       return task.getDelay(unit);
     }
 
+    @Override
     public boolean isCancelled() {
       return task.isCancelled();
     }
 
+    @Override
     public boolean isDone() {
       return task.isDone();
     }
 
+    @Override
     public boolean isPeriodic() {
       return task.isPeriodic();
     }
 
+    @Override
     public void run() {
       if (running.compareAndSet(false, true)) {
         try {
@@ -419,7 +428,7 @@
     }
 
     @Override
-    public NameKey getProjectNameKey() {
+    public Project.NameKey getProjectNameKey() {
       return runnable.getProjectNameKey();
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
index b89d91f..60af3003 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git.strategy;
 
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetAncestor;
@@ -26,10 +27,12 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CommitMergeStatus;
+import com.google.gerrit.server.git.MergeConflictException;
 import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.MergeIdenticalTreeException;
+import com.google.gerrit.server.git.MergeTip;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 
 import org.eclipse.jgit.lib.ObjectId;
@@ -39,6 +42,7 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -49,9 +53,9 @@
   private final GitReferenceUpdated gitRefUpdated;
   private final Map<Change.Id, CodeReviewCommit> newCommits;
 
-  CherryPick(final SubmitStrategy.Arguments args,
-      final PatchSetInfoFactory patchSetInfoFactory,
-      final GitReferenceUpdated gitRefUpdated) {
+  CherryPick(SubmitStrategy.Arguments args,
+      PatchSetInfoFactory patchSetInfoFactory,
+      GitReferenceUpdated gitRefUpdated) {
     super(args);
 
     this.patchSetInfoFactory = patchSetInfoFactory;
@@ -60,68 +64,22 @@
   }
 
   @Override
-  protected CodeReviewCommit _run(CodeReviewCommit mergeTip,
-      final List<CodeReviewCommit> toMerge) throws MergeException {
-    while (!toMerge.isEmpty()) {
-      final CodeReviewCommit n = toMerge.remove(0);
-
+  protected MergeTip _run(CodeReviewCommit branchTip,
+      Collection<CodeReviewCommit> toMerge) throws MergeException {
+    MergeTip mergeTip = new MergeTip(branchTip, toMerge);
+    List<CodeReviewCommit> sorted = CodeReviewCommit.ORDER.sortedCopy(toMerge);
+    while (!sorted.isEmpty()) {
+      CodeReviewCommit n = sorted.remove(0);
       try {
-        if (mergeTip == null) {
-          // The branch is unborn. Take a fast-forward resolution to
-          // create the branch.
-          //
-          mergeTip = n;
-          n.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
-
+        if (mergeTip.getCurrentTip() == null) {
+          cherryPickUnbornRoot(n, mergeTip);
         } else if (n.getParentCount() == 0) {
-          // Refuse to merge a root commit into an existing branch,
-          // we cannot obtain a delta for the cherry-pick to apply.
-          //
-          n.setStatusCode(CommitMergeStatus.CANNOT_CHERRY_PICK_ROOT);
-
+          cherryPickRootOntoBranch(n);
         } else if (n.getParentCount() == 1) {
-          // If there is only one parent, a cherry-pick can be done by
-          // taking the delta relative to that one parent and redoing
-          // that on the current merge tip.
-          //
-
-          mergeTip = writeCherryPickCommit(mergeTip, n);
-
-          if (mergeTip != null) {
-            newCommits.put(mergeTip.getPatchsetId().getParentKey(), mergeTip);
-          } else {
-            n.setStatusCode(CommitMergeStatus.PATH_CONFLICT);
-          }
-
+          cherryPickOne(n, mergeTip);
         } else {
-          // There are multiple parents, so this is a merge commit. We
-          // don't want to cherry-pick the merge as clients can't easily
-          // rebase their history with that merge present and replaced
-          // by an equivalent merge with a different first parent. So
-          // instead behave as though MERGE_IF_NECESSARY was configured.
-          //
-          if (!args.mergeUtil.hasMissingDependencies(args.mergeSorter, n)) {
-            if (args.rw.isMergedInto(mergeTip, n)) {
-              mergeTip = n;
-            } else {
-              mergeTip =
-                  args.mergeUtil.mergeOneCommit(args.serverIdent.get(), args.repo,
-                      args.rw, args.inserter, args.canMergeFlag,
-                      args.destBranch, mergeTip, n);
-           }
-            final PatchSetApproval submitApproval =
-                args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
-                    mergeTip, args.alreadyAccepted);
-            setRefLogIdent(submitApproval);
-
-          } else {
-            // One or more dependencies were not met. The status was
-            // already marked on the commit so we have nothing further
-            // to perform at this time.
-            //
-          }
+          cherryPickMultipleParents(n, mergeTip);
         }
-
       } catch (NoSuchChangeException | IOException | OrmException e) {
         throw new MergeException("Cannot merge " + n.name(), e);
       }
@@ -129,13 +87,72 @@
     return mergeTip;
   }
 
+  private void cherryPickUnbornRoot(CodeReviewCommit n, MergeTip mergeTip) {
+    // The branch is unborn. Take fast-forward resolution to create the branch.
+    mergeTip.moveTipTo(n, n);
+    n.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
+  }
+
+  private void cherryPickRootOntoBranch(CodeReviewCommit n) {
+    // Refuse to merge a root commit into an existing branch, we cannot obtain a
+    // delta for the cherry-pick to apply.
+    n.setStatusCode(CommitMergeStatus.CANNOT_CHERRY_PICK_ROOT);
+  }
+
+  private void cherryPickOne(CodeReviewCommit n, MergeTip mergeTip)
+      throws NoSuchChangeException, OrmException, IOException {
+    // If there is only one parent, a cherry-pick can be done by taking the
+    // delta relative to that one parent and redoing that on the current merge
+    // tip.
+    //
+    // Keep going in the case of a single merge failure; the goal is to
+    // cherry-pick as many commits as possible.
+    try {
+      CodeReviewCommit merge =
+          writeCherryPickCommit(mergeTip.getCurrentTip(), n);
+      mergeTip.moveTipTo(merge, merge);
+      newCommits.put(mergeTip.getCurrentTip().getPatchsetId()
+          .getParentKey(), mergeTip.getCurrentTip());
+    } catch (MergeConflictException mce) {
+      n.setStatusCode(CommitMergeStatus.PATH_CONFLICT);
+    } catch (MergeIdenticalTreeException mie) {
+      n.setStatusCode(CommitMergeStatus.ALREADY_MERGED);
+    }
+  }
+
+  private void cherryPickMultipleParents(CodeReviewCommit n, MergeTip mergeTip)
+      throws IOException, MergeException {
+    // There are multiple parents, so this is a merge commit. We don't want
+    // to cherry-pick the merge as clients can't easily rebase their history
+    // with that merge present and replaced by an equivalent merge with a
+    // different first parent. So instead behave as though MERGE_IF_NECESSARY
+    // was configured.
+    if (!args.mergeUtil.hasMissingDependencies(args.mergeSorter, n)) {
+      if (args.rw.isMergedInto(mergeTip.getCurrentTip(), n)) {
+        mergeTip.moveTipTo(n, n);
+      } else {
+        CodeReviewCommit result = args.mergeUtil.mergeOneCommit(
+            args.serverIdent.get(), args.repo, args.rw, args.inserter,
+            args.canMergeFlag, args.destBranch, mergeTip.getCurrentTip(), n);
+        mergeTip.moveTipTo(result, n);
+      }
+      PatchSetApproval submitApproval = args.mergeUtil.markCleanMerges(args.rw,
+          args.canMergeFlag, mergeTip.getCurrentTip(), args.alreadyAccepted);
+      setRefLogIdent(submitApproval);
+    } else {
+      // One or more dependencies were not met. The status was already marked on
+      // the commit so we have nothing further to perform at this time.
+    }
+  }
+
   private CodeReviewCommit writeCherryPickCommit(CodeReviewCommit mergeTip,
       CodeReviewCommit n) throws IOException, OrmException,
-      NoSuchChangeException {
+      NoSuchChangeException, MergeConflictException,
+      MergeIdenticalTreeException {
 
     args.rw.parseBody(n);
 
-    final PatchSetApproval submitAudit = args.mergeUtil.getSubmitter(n);
+    PatchSetApproval submitAudit = args.mergeUtil.getSubmitter(n);
 
     IdentifiedUser cherryPickUser;
     PersonIdent serverNow = args.serverIdent.get();
@@ -150,25 +167,21 @@
       cherryPickCommitterIdent = serverNow;
     }
 
-    final String cherryPickCmtMsg = args.mergeUtil.createCherryPickCommitMessage(n);
+    String cherryPickCmtMsg = args.mergeUtil.createCherryPickCommitMessage(n);
 
-    final CodeReviewCommit newCommit =
+    CodeReviewCommit newCommit =
         (CodeReviewCommit) args.mergeUtil.createCherryPickFromCommit(args.repo,
             args.inserter, mergeTip, n, cherryPickCommitterIdent,
             cherryPickCmtMsg, args.rw);
 
-    if (newCommit == null) {
-        return null;
-    }
-
     PatchSet.Id id =
         ChangeUtil.nextPatchSetId(args.repo, n.change().currentPatchSetId());
-    final PatchSet ps = new PatchSet(id);
+    PatchSet ps = new PatchSet(id);
     ps.setCreatedOn(TimeUtil.nowTs());
     ps.setUploader(cherryPickUser.getAccountId());
     ps.setRevision(new RevId(newCommit.getId().getName()));
 
-    final RefUpdate ru;
+    RefUpdate ru;
 
     args.db.changes().beginTransaction(n.change().getId());
     try {
@@ -178,9 +191,9 @@
           .setCurrentPatchSet(patchSetInfoFactory.get(newCommit, ps.getId()));
       args.db.changes().update(Collections.singletonList(n.change()));
 
-      final List<PatchSetApproval> approvals = Lists.newArrayList();
-      for (PatchSetApproval a
-          : args.approvalsUtil.byPatchSet(args.db, n.getControl(), n.getPatchsetId())) {
+      List<PatchSetApproval> approvals = Lists.newArrayList();
+      for (PatchSetApproval a : args.approvalsUtil.byPatchSet(
+          args.db, n.getControl(), n.getPatchsetId())) {
         approvals.add(new PatchSetApproval(ps.getId(), a));
       }
       args.db.patchSetApprovals().insert(approvals);
@@ -204,15 +217,16 @@
 
     newCommit.copyFrom(n);
     newCommit.setStatusCode(CommitMergeStatus.CLEAN_PICK);
-    newCommit.setControl(args.changeControlFactory.controlFor(n.change(), cherryPickUser));
+    newCommit.setControl(
+        args.changeControlFactory.controlFor(n.change(), cherryPickUser));
     newCommits.put(newCommit.getPatchsetId().getParentKey(), newCommit);
     setRefLogIdent(submitAudit);
     return newCommit;
   }
 
-  private static void insertAncestors(ReviewDb db, PatchSet.Id id, RevCommit src)
-      throws OrmException {
-    final int cnt = src.getParentCount();
+  private static void insertAncestors(ReviewDb db, PatchSet.Id id,
+      RevCommit src) throws OrmException {
+    int cnt = src.getParentCount();
     List<PatchSetAncestor> toInsert = new ArrayList<>(cnt);
     for (int p = 0; p < cnt; p++) {
       PatchSetAncestor a;
@@ -230,8 +244,8 @@
   }
 
   @Override
-  public boolean dryRun(final CodeReviewCommit mergeTip,
-      final CodeReviewCommit toMerge) throws MergeException {
+  public boolean dryRun(CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
+      throws MergeException {
     return args.mergeUtil.canCherryPick(args.mergeSorter, args.repo,
         mergeTip, args.rw, toMerge);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
index 0b18c0f..7ff2107 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
@@ -18,33 +18,37 @@
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CommitMergeStatus;
 import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.MergeTip;
 
+import java.util.Collection;
 import java.util.List;
 
 public class FastForwardOnly extends SubmitStrategy {
-
-  FastForwardOnly(final SubmitStrategy.Arguments args) {
+  FastForwardOnly(SubmitStrategy.Arguments args) {
     super(args);
   }
 
   @Override
-  protected CodeReviewCommit _run(final CodeReviewCommit mergeTip,
-      final List<CodeReviewCommit> toMerge) throws MergeException {
-    args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
-    final CodeReviewCommit newMergeTip =
-        args.mergeUtil.getFirstFastForward(mergeTip, args.rw, toMerge);
+  protected MergeTip _run(final CodeReviewCommit branchTip,
+      final Collection<CodeReviewCommit> toMerge) throws MergeException {
+    MergeTip mergeTip = new MergeTip(branchTip, toMerge);
+    List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(
+        args.mergeSorter, toMerge);
+    final CodeReviewCommit newMergeTipCommit =
+        args.mergeUtil.getFirstFastForward(branchTip, args.rw, sorted);
+    mergeTip.moveTipTo(newMergeTipCommit, newMergeTipCommit);
 
-    while (!toMerge.isEmpty()) {
-      final CodeReviewCommit n = toMerge.remove(0);
+    while (!sorted.isEmpty()) {
+      final CodeReviewCommit n = sorted.remove(0);
       n.setStatusCode(CommitMergeStatus.NOT_FAST_FORWARD);
     }
 
-    final PatchSetApproval submitApproval =
-        args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, newMergeTip,
+    PatchSetApproval submitApproval =
+        args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, newMergeTipCommit,
             args.alreadyAccepted);
     setRefLogIdent(submitApproval);
 
-    return newMergeTip;
+    return mergeTip;
   }
 
   @Override
@@ -52,8 +56,9 @@
     return false;
   }
 
-  public boolean dryRun(final CodeReviewCommit mergeTip,
-      final CodeReviewCommit toMerge) throws MergeException {
+  @Override
+  public boolean dryRun(CodeReviewCommit mergeTip,
+      CodeReviewCommit toMerge) throws MergeException {
     return args.mergeUtil.canFastForward(args.mergeSorter, mergeTip, args.rw,
         toMerge);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
index 9023623..3c13af9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
@@ -17,34 +17,40 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.MergeTip;
 
+import java.util.Collection;
 import java.util.List;
 
 public class MergeAlways extends SubmitStrategy {
-
-  MergeAlways(final SubmitStrategy.Arguments args) {
+  MergeAlways(SubmitStrategy.Arguments args) {
     super(args);
   }
 
   @Override
-  protected CodeReviewCommit _run(CodeReviewCommit mergeTip,
-      List<CodeReviewCommit> toMerge) throws MergeException {
-    args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
-
-    if (mergeTip == null) {
+  protected MergeTip _run(CodeReviewCommit branchTip,
+      Collection<CodeReviewCommit> toMerge) throws MergeException {
+  List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
+    MergeTip mergeTip;
+    if (branchTip == null) {
       // The branch is unborn. Take a fast-forward resolution to
       // create the branch.
-      mergeTip = toMerge.remove(0);
+      mergeTip = new MergeTip(sorted.get(0), toMerge);
+      sorted.remove(0);
+    } else {
+      mergeTip = new MergeTip(branchTip, toMerge);
     }
-    while (!toMerge.isEmpty()) {
-      mergeTip =
+    while (!sorted.isEmpty()) {
+      CodeReviewCommit mergedFrom = sorted.remove(0);
+      CodeReviewCommit newTip =
           args.mergeUtil.mergeOneCommit(args.serverIdent.get(), args.repo, args.rw,
-              args.inserter, args.canMergeFlag, args.destBranch, mergeTip,
-              toMerge.remove(0));
+              args.inserter, args.canMergeFlag, args.destBranch, mergeTip.getCurrentTip(),
+              mergedFrom);
+      mergeTip.moveTipTo(newTip, mergedFrom);
     }
 
     final PatchSetApproval submitApproval =
-        args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, mergeTip,
+        args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, mergeTip.getCurrentTip(),
             args.alreadyAccepted);
     setRefLogIdent(submitApproval);
 
@@ -52,8 +58,8 @@
   }
 
   @Override
-  public boolean dryRun(final CodeReviewCommit mergeTip,
-      final CodeReviewCommit toMerge) throws MergeException {
+  public boolean dryRun(CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
+      throws MergeException {
     return args.mergeUtil.canMerge(args.mergeSorter, args.repo, mergeTip,
         toMerge);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
index b84586f6..b49cb0a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
@@ -17,47 +17,55 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.MergeTip;
 
+import java.util.Collection;
 import java.util.List;
 
 public class MergeIfNecessary extends SubmitStrategy {
-
-  MergeIfNecessary(final SubmitStrategy.Arguments args) {
+  MergeIfNecessary(SubmitStrategy.Arguments args) {
     super(args);
   }
 
   @Override
-  protected CodeReviewCommit _run(CodeReviewCommit mergeTip,
-      List<CodeReviewCommit> toMerge) throws MergeException {
-    args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
-
-    if (mergeTip == null) {
+  protected MergeTip _run(CodeReviewCommit branchTip,
+      Collection<CodeReviewCommit> toMerge) throws MergeException {
+    List<CodeReviewCommit> sorted =
+        args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
+    MergeTip mergeTip;
+    if (branchTip == null) {
       // The branch is unborn. Take a fast-forward resolution to
       // create the branch.
-      mergeTip = toMerge.remove(0);
+      mergeTip = new MergeTip(sorted.get(0), toMerge);
+      branchTip = sorted.remove(0);
+    } else {
+      mergeTip = new MergeTip(branchTip, toMerge);
+      branchTip =
+          args.mergeUtil.getFirstFastForward(branchTip, args.rw, sorted);
     }
-
-    mergeTip =
-        args.mergeUtil.getFirstFastForward(mergeTip, args.rw, toMerge);
+    mergeTip.moveTipTo(branchTip, branchTip);
 
     // For every other commit do a pair-wise merge.
-    while (!toMerge.isEmpty()) {
-      mergeTip =
-          args.mergeUtil.mergeOneCommit(args.serverIdent.get(), args.repo, args.rw,
-              args.inserter, args.canMergeFlag, args.destBranch, mergeTip,
-              toMerge.remove(0));
+    while (!sorted.isEmpty()) {
+      CodeReviewCommit mergedFrom = sorted.remove(0);
+      branchTip =
+          args.mergeUtil.mergeOneCommit(args.serverIdent.get(), args.repo,
+              args.rw, args.inserter, args.canMergeFlag, args.destBranch,
+              branchTip, mergedFrom);
+      mergeTip.moveTipTo(branchTip, mergedFrom);
     }
 
-    final PatchSetApproval submitApproval = args.mergeUtil.markCleanMerges(
-        args.rw, args.canMergeFlag, mergeTip, args.alreadyAccepted);
+    final PatchSetApproval submitApproval =
+        args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, branchTip,
+            args.alreadyAccepted);
     setRefLogIdent(submitApproval);
 
     return mergeTip;
   }
 
   @Override
-  public boolean dryRun(final CodeReviewCommit mergeTip,
-      final CodeReviewCommit toMerge) throws MergeException {
+  public boolean dryRun(CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
+      throws MergeException {
     return args.mergeUtil.canFastForward(
           args.mergeSorter, mergeTip, args.rw, toMerge)
         || args.mergeUtil.canMerge(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
index 130d170..acd32c7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
@@ -20,11 +20,12 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy;
-import com.google.gerrit.server.changedetail.PathConflictException;
 import com.google.gerrit.server.changedetail.RebaseChange;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CommitMergeStatus;
+import com.google.gerrit.server.git.MergeConflictException;
 import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.MergeTip;
 import com.google.gerrit.server.git.RebaseSorter;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
@@ -34,6 +35,8 @@
 import org.eclipse.jgit.lib.ObjectId;
 
 import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -53,20 +56,19 @@
   }
 
   @Override
-  protected CodeReviewCommit _run(final CodeReviewCommit mergeTip,
-      final List<CodeReviewCommit> toMerge) throws MergeException {
-    CodeReviewCommit newMergeTip = mergeTip;
-    sort(toMerge);
+  protected MergeTip _run(final CodeReviewCommit branchTip,
+      final Collection<CodeReviewCommit> toMerge) throws MergeException {
+    MergeTip mergeTip = new MergeTip(branchTip, toMerge);
+    List<CodeReviewCommit> sorted = sort(toMerge);
+    while (!sorted.isEmpty()) {
+      CodeReviewCommit n = sorted.remove(0);
 
-    while (!toMerge.isEmpty()) {
-      final CodeReviewCommit n = toMerge.remove(0);
-
-      if (newMergeTip == null) {
+      if (mergeTip.getCurrentTip() == null) {
         // The branch is unborn. Take a fast-forward resolution to
         // create the branch.
         //
-        newMergeTip = n;
         n.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
+        mergeTip.moveTipTo(n, n);
 
       } else if (n.getParentCount() == 0) {
         // Refuse to merge a root commit into an existing branch,
@@ -75,42 +77,47 @@
         n.setStatusCode(CommitMergeStatus.CANNOT_REBASE_ROOT);
 
       } else if (n.getParentCount() == 1) {
-        if (args.mergeUtil.canFastForward(
-            args.mergeSorter, newMergeTip, args.rw, n)) {
-          newMergeTip = n;
+        if (args.mergeUtil.canFastForward(args.mergeSorter,
+            mergeTip.getCurrentTip(), args.rw, n)) {
           n.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
+          mergeTip.moveTipTo(n, n);
 
         } else {
           try {
-            final IdentifiedUser uploader = args.identifiedUserFactory.create(
-                args.mergeUtil.getSubmitter(n).getAccountId());
-            final PatchSet newPatchSet =
+            IdentifiedUser uploader =
+                args.identifiedUserFactory.create(args.mergeUtil
+                    .getSubmitter(n).getAccountId());
+            PatchSet newPatchSet =
                 rebaseChange.rebase(args.repo, args.rw, args.inserter,
                     n.getPatchsetId(), n.change(), uploader,
-                    newMergeTip, args.mergeUtil, args.serverIdent.get(),
-                    false, false, ValidatePolicy.NONE);
+                    mergeTip.getCurrentTip(), args.mergeUtil,
+                    args.serverIdent.get(), false, ValidatePolicy.NONE);
 
             List<PatchSetApproval> approvals = Lists.newArrayList();
-            for (PatchSetApproval a : args.approvalsUtil.byPatchSet(
-                args.db, n.getControl(), n.getPatchsetId())) {
+            for (PatchSetApproval a : args.approvalsUtil.byPatchSet(args.db,
+                n.getControl(), n.getPatchsetId())) {
               approvals.add(new PatchSetApproval(newPatchSet.getId(), a));
             }
             // rebaseChange.rebase() may already have copied some approvals,
             // use upsert, not insert, to avoid constraint violation on database
             args.db.patchSetApprovals().upsert(approvals);
-            newMergeTip =
-                (CodeReviewCommit) args.rw.parseCommit(ObjectId
-                    .fromString(newPatchSet.getRevision().get()));
+            CodeReviewCommit newTip = (CodeReviewCommit) args.rw.parseCommit(
+                ObjectId.fromString(newPatchSet.getRevision().get()));
+            mergeTip.moveTipTo(newTip, newTip);
             n.change().setCurrentPatchSet(
-                patchSetInfoFactory.get(newMergeTip, newPatchSet.getId()));
-            newMergeTip.copyFrom(n);
-            newMergeTip.setControl(args.changeControlFactory.controlFor(n.change(), uploader));
-            newMergeTip.setPatchsetId(newPatchSet.getId());
-            newMergeTip.setStatusCode(CommitMergeStatus.CLEAN_REBASE);
-            newCommits.put(newPatchSet.getId().getParentKey(), newMergeTip);
+                patchSetInfoFactory.get(mergeTip.getCurrentTip(),
+                    newPatchSet.getId()));
+            mergeTip.getCurrentTip().copyFrom(n);
+            mergeTip.getCurrentTip().setControl(
+                args.changeControlFactory.controlFor(n.change(), uploader));
+            mergeTip.getCurrentTip().setPatchsetId(newPatchSet.getId());
+            mergeTip.getCurrentTip().setStatusCode(
+                CommitMergeStatus.CLEAN_REBASE);
+            newCommits.put(newPatchSet.getId().getParentKey(),
+                mergeTip.getCurrentTip());
             setRefLogIdent(args.mergeUtil.getSubmitter(n));
-          } catch (PathConflictException e) {
-            n.setStatusCode(CommitMergeStatus.PATH_CONFLICT);
+          } catch (MergeConflictException e) {
+            n.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
           } catch (NoSuchChangeException | OrmException | IOException
               | InvalidChangeOperationException e) {
             throw new MergeException("Cannot rebase " + n.name(), e);
@@ -125,34 +132,36 @@
         // instead behave as though MERGE_IF_NECESSARY was configured.
         //
         try {
-          if (args.rw.isMergedInto(newMergeTip, n)) {
-            newMergeTip = n;
+          if (args.rw.isMergedInto(mergeTip.getCurrentTip(), n)) {
+            mergeTip.moveTipTo(n, n);
           } else {
-            newMergeTip = args.mergeUtil.mergeOneCommit(
-                args.serverIdent.get(), args.repo, args.rw, args.inserter,
-                args.canMergeFlag, args.destBranch, newMergeTip, n);
+            mergeTip.moveTipTo(
+                args.mergeUtil.mergeOneCommit(args.serverIdent.get(),
+                    args.repo, args.rw, args.inserter, args.canMergeFlag,
+                    args.destBranch, mergeTip.getCurrentTip(), n), n);
           }
-          final PatchSetApproval submitApproval = args.mergeUtil.markCleanMerges(
-              args.rw, args.canMergeFlag, newMergeTip, args.alreadyAccepted);
+          PatchSetApproval submitApproval =
+              args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
+                  mergeTip.getCurrentTip(), args.alreadyAccepted);
           setRefLogIdent(submitApproval);
         } catch (IOException e) {
           throw new MergeException("Cannot merge " + n.name(), e);
         }
       }
 
-      args.alreadyAccepted.add(newMergeTip);
+      args.alreadyAccepted.add(mergeTip.getCurrentTip());
     }
 
-    return newMergeTip;
+    return mergeTip;
   }
 
-  private void sort(final List<CodeReviewCommit> toSort) throws MergeException {
+  private List<CodeReviewCommit> sort(Collection<CodeReviewCommit> toSort)
+      throws MergeException {
     try {
-      final List<CodeReviewCommit> sorted =
-          new RebaseSorter(args.rw, args.alreadyAccepted, args.canMergeFlag)
-              .sort(toSort);
-      toSort.clear();
-      toSort.addAll(sorted);
+      List<CodeReviewCommit> result = new RebaseSorter(
+          args.rw, args.alreadyAccepted, args.canMergeFlag).sort(toSort);
+      Collections.sort(result, CodeReviewCommit.ORDER);
+      return result;
     } catch (IOException e) {
       throw new MergeException("Commit sorting failed", e);
     }
@@ -164,8 +173,8 @@
   }
 
   @Override
-  public boolean dryRun(final CodeReviewCommit mergeTip,
-      final CodeReviewCommit toMerge) throws MergeException {
+  public boolean dryRun(CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
+      throws MergeException {
     return !args.mergeUtil.hasMissingDependencies(args.mergeSorter, toMerge)
         && args.mergeUtil.canCherryPick(args.mergeSorter, args.repo, mergeTip,
             args.rw, toMerge);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
index a864b6c..b25b17e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git.strategy;
 
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
@@ -24,6 +24,7 @@
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.git.MergeSorter;
+import com.google.gerrit.server.git.MergeTip;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.project.ChangeControl;
@@ -37,20 +38,18 @@
 import org.eclipse.jgit.revwalk.RevFlag;
 import org.eclipse.jgit.revwalk.RevWalk;
 
+import java.util.Collection;
 import java.util.Collections;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 /**
- * Base class that submit strategies must extend. A submit strategy for a
- * certain {@link SubmitType} defines how the submitted commits should be
- * merged.
+ * Base class that submit strategies must extend.
+ * <p>
+ * A submit strategy for a certain {@link SubmitType} defines how the submitted
+ * commits should be merged.
  */
 public abstract class SubmitStrategy {
-
-  private PersonIdent refLogIdent;
-
   static class Arguments {
     protected final IdentifiedUser.GenericFactory identifiedUserFactory;
     protected final Provider<PersonIdent> serverIdent;
@@ -68,13 +67,13 @@
     protected final ChangeIndexer indexer;
     protected final MergeSorter mergeSorter;
 
-    Arguments(final IdentifiedUser.GenericFactory identifiedUserFactory,
-        final Provider<PersonIdent> serverIdent, final ReviewDb db,
-        final ChangeControl.GenericFactory changeControlFactory,
-        final Repository repo, final RevWalk rw, final ObjectInserter inserter,
-        final RevFlag canMergeFlag, final Set<RevCommit> alreadyAccepted,
-        final Branch.NameKey destBranch, final ApprovalsUtil approvalsUtil,
-        final MergeUtil mergeUtil, final ChangeIndexer indexer) {
+    Arguments(IdentifiedUser.GenericFactory identifiedUserFactory,
+        Provider<PersonIdent> serverIdent, ReviewDb db,
+        ChangeControl.GenericFactory changeControlFactory, Repository repo,
+        RevWalk rw, ObjectInserter inserter, RevFlag canMergeFlag,
+        Set<RevCommit> alreadyAccepted, Branch.NameKey destBranch,
+        ApprovalsUtil approvalsUtil, MergeUtil mergeUtil,
+        ChangeIndexer indexer) {
       this.identifiedUserFactory = identifiedUserFactory;
       this.serverIdent = serverIdent;
       this.db = db;
@@ -95,48 +94,42 @@
 
   protected final Arguments args;
 
-  SubmitStrategy(final Arguments args) {
+  private PersonIdent refLogIdent;
+
+  SubmitStrategy(Arguments args) {
     this.args = args;
   }
 
   /**
-   * Runs this submit strategy. If possible the provided commits will be merged
-   * with this submit strategy.
+   * Runs this submit strategy.
+   * <p>
+   * If possible, the provided commits will be merged with this submit strategy.
    *
-   * @param mergeTip the mergeTip
+   * @param currentTip the mergeTip
    * @param toMerge the list of submitted commits that should be merged using
-   *        this submit strategy
-   * @return the new mergeTip
+   *        this submit strategy. Implementations are responsible for ordering
+   *        of commits, and should not modify the input in place.
+   * @return the new merge tip.
    * @throws MergeException
    */
-  public final CodeReviewCommit run(final CodeReviewCommit mergeTip,
-      final List<CodeReviewCommit> toMerge) throws MergeException {
+  public final MergeTip run(final CodeReviewCommit currentTip,
+      final Collection<CodeReviewCommit> toMerge) throws MergeException {
     refLogIdent = null;
-    return _run(mergeTip, toMerge);
+    return _run(currentTip, toMerge);
   }
 
-  /**
-   * Runs this submit strategy. If possible the provided commits will be merged
-   * with this submit strategy.
-   *
-   * @param mergeTip the mergeTip
-   * @param toMerge the list of submitted commits that should be merged using
-   *        this submit strategy
-   * @return the new mergeTip
-   * @throws MergeException
-   */
-  protected abstract CodeReviewCommit _run(CodeReviewCommit mergeTip,
-      List<CodeReviewCommit> toMerge) throws MergeException;
+  /** @see #run(CodeReviewCommit, Collection) */
+  protected abstract MergeTip _run(CodeReviewCommit currentTip,
+      Collection<CodeReviewCommit> toMerge) throws MergeException;
 
   /**
    * Checks whether the given commit can be merged.
    *
-   * Subclasses must ensure that invoking this method does neither modify the
+   * Implementations must ensure that invoking this method modifies neither the
    * git repository nor the Gerrit database.
    *
-   * @param mergeTip the mergeTip
-   * @param toMerge the commit for which it should be checked whether it can be
-   *        merged or not
+   * @param mergeTip the merge tip.
+   * @param toMerge the commit that should be checked.
    * @return {@code true} if the given commit can be merged, otherwise
    *         {@code false}
    * @throws MergeException
@@ -145,14 +138,13 @@
       CodeReviewCommit toMerge) throws MergeException;
 
   /**
-   * Returns the PersonIdent that should be used for the ref log entries when
-   * updating the destination branch. The ref log identity may be set after the
-   * {@link #run(CodeReviewCommit, List)} method finished.
+   * Returns the identity that should be used for reflog entries when updating
+   * the destination branch.
+   * <p>
+   * The reflog identity may only be set during {@link #run(CodeReviewCommit,
+   * Collection)}, and this method is invalid to call beforehand.
    *
-   * Do only call this method after the {@link #run(CodeReviewCommit, List)}
-   * method has been invoked.
-   *
-   * @return the ref log identity, may be {@code null}
+   * @return the ref log identity, which may be {@code null}.
    */
   public final PersonIdent getRefLogIdent() {
     return refLogIdent;
@@ -161,24 +153,24 @@
   /**
    * Returns all commits that have been newly created for the changes that are
    * getting merged.
+   * <p>
+   * By default this method returns an empty map, but subclasses may override
+   * this method to provide any newly created commits.
    *
-   * By default this method is returning an empty map, but subclasses may
-   * overwrite this method to provide newly created commits.
+   * This method may only be called after {@link #run(CodeReviewCommit,
+   * Collection)}.
    *
-   * Do only call this method after the {@link #run(CodeReviewCommit, List)}
-   * method has been invoked.
-   *
-   * @return new commits created for changes that are getting merged
+   * @return new commits created for changes that were merged.
    */
   public Map<Change.Id, CodeReviewCommit> getNewCommits() {
     return Collections.emptyMap();
   }
 
   /**
-   * Returns whether a merge that failed with
-   * {@link Result#LOCK_FAILURE} should be retried.
-   *
-   * May be overwritten by subclasses.
+   * Returns whether a merge that failed with {@link Result#LOCK_FAILURE} should
+   * be retried.
+   * <p>
+   * May be overridden by subclasses.
    *
    * @return {@code true} if a merge that failed with
    *         {@link Result#LOCK_FAILURE} should be retried, otherwise
@@ -189,15 +181,14 @@
   }
 
   /**
-   * Sets the ref log identity if it wasn't set yet.
+   * Set the ref log identity if it wasn't set yet.
    *
    * @param submitApproval the approval that submitted the patch set
    */
-  protected final void setRefLogIdent(final PatchSetApproval submitApproval) {
+  protected final void setRefLogIdent(PatchSetApproval submitApproval) {
     if (refLogIdent == null && submitApproval != null) {
-      refLogIdent =
-          args.identifiedUserFactory.create(submitApproval.getAccountId())
-              .newRefLogIdent();
+      refLogIdent = args.identifiedUserFactory.create(
+          submitApproval.getAccountId()) .newRefLogIdent();
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
index 091523b..ac65f8d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git.strategy;
 
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationMessage.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationMessage.java
index ab86317..a778482 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationMessage.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationMessage.java
@@ -14,20 +14,8 @@
 
 package com.google.gerrit.server.git.validators;
 
-public class CommitValidationMessage {
-  private final String message;
-  private final boolean isError;
-
-  public CommitValidationMessage(final String message, final boolean isError) {
-    this.message = message;
-    this.isError = isError;
-  }
-
-  public String getMessage() {
-    return message;
-  }
-
-  public boolean isError() {
-    return isError;
+public class CommitValidationMessage extends ValidationMessage {
+  public CommitValidationMessage(String message, boolean isError) {
+    super(message, isError);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 9d0eb66..d030a55 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git.validators;
 
 import com.google.common.base.CharMatcher;
+import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -24,7 +25,6 @@
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.events.CommitReceivedEvent;
-import com.google.gerrit.server.git.BanCommit;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.git.ReceiveCommits;
 import com.google.gerrit.server.git.ValidationError;
@@ -60,8 +60,6 @@
   private static final Logger log = LoggerFactory
       .getLogger(CommitValidators.class);
 
-  private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
-
   public interface Factory {
     CommitValidators create(RefControl refControl, SshInfo sshInfo,
         Repository repo);
@@ -93,7 +91,8 @@
   }
 
   public List<CommitValidationMessage> validateForReceiveCommits(
-      CommitReceivedEvent receiveEvent) throws CommitValidationException {
+      CommitReceivedEvent receiveEvent, NoteMap rejectCommits)
+      throws CommitValidationException {
 
     List<CommitValidationListener> validators = new LinkedList<>();
 
@@ -102,7 +101,7 @@
         refControl, gerritIdent));
     validators.add(new AuthorUploaderValidator(refControl, canonicalWebUrl));
     validators.add(new CommitterUploaderValidator(refControl, canonicalWebUrl));
-    validators.add(new SignedOffByValidator(refControl, canonicalWebUrl));
+    validators.add(new SignedOffByValidator(refControl));
     if (MagicBranch.isMagicBranch(receiveEvent.command.getRefName())
         || ReceiveCommits.NEW_PATCHSET.matcher(
             receiveEvent.command.getRefName()).matches()) {
@@ -110,7 +109,7 @@
           installCommitMsgHookCommand, sshInfo));
     }
     validators.add(new ConfigValidator(refControl, repo));
-    validators.add(new BannedCommitsValidator(repo));
+    validators.add(new BannedCommitsValidator(rejectCommits));
     validators.add(new PluginCommitValidationListener(commitValidationListeners));
 
     List<CommitValidationMessage> messages = new LinkedList<>();
@@ -136,7 +135,7 @@
     validators.add(new AmendedGerritMergeCommitValidationListener(
         refControl, gerritIdent));
     validators.add(new AuthorUploaderValidator(refControl, canonicalWebUrl));
-    validators.add(new SignedOffByValidator(refControl, canonicalWebUrl));
+    validators.add(new SignedOffByValidator(refControl));
     if (MagicBranch.isMagicBranch(receiveEvent.command.getRefName())
         || ReceiveCommits.NEW_PATCHSET.matcher(
             receiveEvent.command.getRefName()).matches()) {
@@ -179,14 +178,15 @@
     @Override
     public List<CommitValidationMessage> onCommitReceived(
         CommitReceivedEvent receiveEvent) throws CommitValidationException {
-      final List<String> idList = receiveEvent.commit.getFooterLines(CHANGE_ID);
+      final List<String> idList = receiveEvent.commit.getFooterLines(
+          FooterConstants.CHANGE_ID);
 
       List<CommitValidationMessage> messages = new LinkedList<>();
 
       if (idList.isEmpty()) {
         if (projectControl.getProjectState().isRequireChangeID()) {
           String shortMsg = receiveEvent.commit.getShortMessage();
-          String changeIdPrefix = CHANGE_ID.getName() + ":";
+          String changeIdPrefix = FooterConstants.CHANGE_ID.getName() + ":";
           if (shortMsg.startsWith(changeIdPrefix)
               && shortMsg.substring(changeIdPrefix.length()).trim()
                   .matches("^I[0-9a-f]{8,}.*$")) {
@@ -194,7 +194,7 @@
                 "missing subject; Change-Id must be in commit message footer");
           } else {
             String errMsg = "missing Change-Id in commit message footer";
-            messages.add(getFixedCommitMsgWithChangeId(
+            messages.add(getMissingChangeIdErrorMsg(
                 errMsg, receiveEvent.commit));
             throw new CommitValidationException(errMsg, messages);
           }
@@ -208,49 +208,25 @@
           final String errMsg =
               "missing or invalid Change-Id line format in commit message footer";
           messages.add(
-              getFixedCommitMsgWithChangeId(errMsg, receiveEvent.commit));
+              getMissingChangeIdErrorMsg(errMsg, receiveEvent.commit));
           throw new CommitValidationException(errMsg, messages);
         }
       }
       return Collections.emptyList();
     }
 
-    /**
-     * We handle 3 cases:
-     * 1. No change id in the commit message at all.
-     * 2. Change id last in the commit message but missing empty line to create the footer.
-     * 3. There is a change-id somewhere in the commit message, but we ignore it.
-     *
-     * @return The fixed up commit message
-     */
-    private CommitValidationMessage getFixedCommitMsgWithChangeId(
+    private CommitValidationMessage getMissingChangeIdErrorMsg(
         final String errMsg, final RevCommit c) {
       final String changeId = "Change-Id:";
       StringBuilder sb = new StringBuilder();
       sb.append("ERROR: ").append(errMsg);
-      sb.append('\n');
-      sb.append("Suggestion for commit message:\n");
 
-      if (c.getFullMessage().indexOf(changeId) == -1) {
-        sb.append(c.getFullMessage());
-        sb.append('\n');
-        sb.append(changeId).append(" I").append(c.name());
-      } else {
+      if (c.getFullMessage().indexOf(changeId) >= 0) {
         String lines[] = c.getFullMessage().trim().split("\n");
         String lastLine = lines.length > 0 ? lines[lines.length - 1] : "";
 
-        if (lastLine.indexOf(changeId) == 0) {
-          for (int i = 0; i < lines.length - 1; i++) {
-            sb.append(lines[i]);
-            sb.append('\n');
-          }
-
+        if (lastLine.indexOf(changeId) == -1) {
           sb.append('\n');
-          sb.append(lastLine);
-        } else {
-          sb.append(c.getFullMessage());
-          sb.append('\n');
-          sb.append(changeId).append(" I").append(c.name());
           sb.append('\n');
           sb.append("Hint: A potential Change-Id was found, but it was not in the ");
           sb.append("footer (last paragraph) of the commit message.");
@@ -259,8 +235,10 @@
       sb.append('\n');
       sb.append('\n');
       sb.append("Hint: To automatically insert Change-Id, install the hook:\n");
-      sb.append(getCommitMessageHookInstallationHint()).append('\n');
+      sb.append(getCommitMessageHookInstallationHint());
       sb.append('\n');
+      sb.append("And then amend the commit:\n");
+      sb.append("  git commit --amend\n");
 
       return new CommitValidationMessage(sb.toString(), false);
     }
@@ -396,7 +374,7 @@
   public static class SignedOffByValidator implements CommitValidationListener {
     private final RefControl refControl;
 
-    public SignedOffByValidator(RefControl refControl, String canonicalWebUrl) {
+    public SignedOffByValidator(RefControl refControl) {
       this.refControl = refControl;
     }
 
@@ -416,7 +394,7 @@
             if (e != null) {
               sboAuthor |= author.getEmailAddress().equals(e);
               sboCommitter |= committer.getEmailAddress().equals(e);
-              sboMe |= currentUser.getEmailAddresses().contains(e);
+              sboMe |= currentUser.hasEmailAddress(e);
             }
           }
         }
@@ -447,7 +425,7 @@
       IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
       final PersonIdent author = receiveEvent.commit.getAuthorIdent();
 
-      if (!currentUser.getEmailAddresses().contains(author.getEmailAddress())
+      if (!currentUser.hasEmailAddress(author.getEmailAddress())
           && !refControl.canForgeAuthor()) {
         List<CommitValidationMessage> messages = new LinkedList<>();
 
@@ -476,8 +454,7 @@
         CommitReceivedEvent receiveEvent) throws CommitValidationException {
       IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
       final PersonIdent committer = receiveEvent.commit.getCommitterIdent();
-      if (!currentUser.getEmailAddresses()
-          .contains(committer.getEmailAddress())
+      if (!currentUser.hasEmailAddress(committer.getEmailAddress())
           && !refControl.canForgeCommitter()) {
         List<CommitValidationMessage> messages = new LinkedList<>();
         messages.add(getInvalidEmailError(receiveEvent.commit, "committer", committer,
@@ -522,24 +499,25 @@
   /** Reject banned commits. */
   public static class BannedCommitsValidator implements
       CommitValidationListener {
-    private final Repository repo;
+    private final NoteMap rejectCommits;
 
-    public BannedCommitsValidator(Repository repo) {
-      this.repo = repo;
+    public BannedCommitsValidator(NoteMap rejectCommits) {
+      this.rejectCommits = rejectCommits;
     }
 
     @Override
     public List<CommitValidationMessage> onCommitReceived(
         CommitReceivedEvent receiveEvent) throws CommitValidationException {
       try {
-        NoteMap rejectCommits = BanCommit.loadRejectCommitsMap(repo);
         if (rejectCommits.contains(receiveEvent.commit)) {
           throw new CommitValidationException("contains banned commit "
               + receiveEvent.commit.getName());
         }
         return Collections.emptyList();
       } catch (IOException e) {
-        throw new CommitValidationException(e.getMessage(), e);
+        String m = "error checking banned commits";
+        log.warn(m, e);
+        throw new CommitValidationException(m, e);
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
index 76998b7..6f70d46 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -16,8 +16,8 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.DynamicMap.Entry;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidationException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidationException.java
new file mode 100644
index 0000000..5864833
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidationException.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.validators;
+
+import com.google.gerrit.server.validators.ValidationException;
+
+public class RefOperationValidationException extends ValidationException {
+  private static final long serialVersionUID = 1L;
+  private final Iterable<ValidationMessage> messages;
+
+  public RefOperationValidationException(String reason,
+      Iterable<ValidationMessage> messages) {
+    super(reason);
+    this.messages = messages;
+  }
+
+  public Iterable<ValidationMessage> getMessages() {
+    return messages;
+  }
+
+  @Override
+  public String getMessage() {
+    StringBuilder msg = new StringBuilder(super.getMessage());
+    for (ValidationMessage error : messages) {
+      msg.append("\n").append(error.getMessage());
+    }
+    return msg.toString();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidationListener.java
new file mode 100644
index 0000000..5d04e2a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidationListener.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.server.git.validators;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.server.events.RefReceivedEvent;
+import com.google.gerrit.server.validators.ValidationException;
+
+import java.util.List;
+
+/**
+ * Listener to provide validation on operation that is going to be performed on
+ * given ref
+ */
+@ExtensionPoint
+public interface RefOperationValidationListener {
+  /**
+   * Validate a ref operation before it is performed.
+   *
+   * @param refEvent ref operation specification
+   * @return empty list or informational messages on success
+   * @throws ValidationException if the ref operation fails to validate
+   */
+  List<ValidationMessage> onRefOperation(RefReceivedEvent refEvent)
+      throws ValidationException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
new file mode 100644
index 0000000..6fd0f5c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
@@ -0,0 +1,100 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.server.git.validators;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.events.RefReceivedEvent;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+public class RefOperationValidators {
+  private static final GetErrorMessages GET_ERRORS = new GetErrorMessages();
+  private static final Logger LOG = LoggerFactory
+      .getLogger(RefOperationValidators.class);
+
+  public interface Factory {
+    RefOperationValidators create(Project project, IdentifiedUser user, ReceiveCommand cmd);
+  }
+
+  public static ReceiveCommand getCommand(RefUpdate update, ReceiveCommand.Type type) {
+    return new ReceiveCommand(update.getOldObjectId(), update.getNewObjectId(),
+        update.getName(), type);
+  }
+
+  private final RefReceivedEvent event;
+  private final DynamicSet<RefOperationValidationListener> refOperationValidationListeners;
+
+  @Inject
+  RefOperationValidators(
+      DynamicSet<RefOperationValidationListener> refOperationValidationListeners,
+      @Assisted Project project, @Assisted IdentifiedUser user,
+      @Assisted ReceiveCommand cmd) {
+    this.refOperationValidationListeners = refOperationValidationListeners;
+    event = new RefReceivedEvent();
+    event.command = cmd;
+    event.project = project;
+    event.user = user;
+  }
+
+  public List<ValidationMessage> validateForRefOperation()
+    throws RefOperationValidationException {
+
+    List<ValidationMessage> messages = Lists.newArrayList();
+    boolean withException = false;
+    try {
+      for (RefOperationValidationListener listener : refOperationValidationListeners) {
+        messages.addAll(listener.onRefOperation(event));
+      }
+    } catch (ValidationException e) {
+      messages.add(new ValidationMessage(e.getMessage(), true));
+      withException = true;
+    }
+
+    if (withException) {
+      throwException(messages, event);
+    }
+
+    return messages;
+  }
+
+  private void throwException(Iterable<ValidationMessage> messages,
+      RefReceivedEvent event) throws RefOperationValidationException {
+    Iterable<ValidationMessage> errors = Iterables.filter(messages, GET_ERRORS);
+    String header = String.format(
+        "Ref \"%s\" %S in project %s validation failed", event.command.getRefName(),
+        event.command.getType(), event.project.getName());
+    LOG.error(header);
+    throw new RefOperationValidationException(header, errors);
+  }
+
+  private static class GetErrorMessages implements Predicate<ValidationMessage> {
+    @Override
+    public boolean apply(ValidationMessage input) {
+      return input.isError();
+    }
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidators.java
index 1735d28..eb2e136 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/UploadValidators.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.lib.ObjectId;
@@ -27,8 +28,6 @@
 
 import java.util.Collection;
 
-import javax.inject.Inject;
-
 public class UploadValidators implements PreUploadHook {
 
   private final DynamicSet<UploadValidationListener> uploadValidationListeners;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/ValidationMessage.java
similarity index 61%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/git/validators/ValidationMessage.java
index 407b7c7..e1098aa 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/ValidationMessage.java
@@ -12,10 +12,22 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.server.git.validators;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+public class ValidationMessage {
+  private final String message;
+  private final boolean isError;
+
+  public ValidationMessage(String message, boolean isError) {
+    this.message = message;
+    this.isError = isError;
+  }
+
+  public String getMessage() {
+    return message;
+  }
+
+  public boolean isError() {
+    return isError;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
index 163b335..9a3f02f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
@@ -27,14 +28,12 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupById;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.account.GroupIncludeCache;
 import com.google.gerrit.server.group.AddIncludedGroups.Input;
 import com.google.gerrit.server.group.GroupJson.GroupInfo;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -75,15 +74,17 @@
   private final GroupIncludeCache groupIncludeCache;
   private final Provider<ReviewDb> db;
   private final GroupJson json;
+  private final AuditService auditService;
 
   @Inject
   public AddIncludedGroups(GroupsCollection groupsCollection,
-      GroupIncludeCache groupIncludeCache,
-      Provider<ReviewDb> db, GroupJson json) {
+      GroupIncludeCache groupIncludeCache, Provider<ReviewDb> db,
+      GroupJson json, AuditService auditService) {
     this.groupsCollection = groupsCollection;
     this.groupIncludeCache = groupIncludeCache;
     this.db = db;
     this.json = json;
+    this.auditService = auditService;
   }
 
   @Override
@@ -98,13 +99,12 @@
 
     GroupControl control = resource.getControl();
     Map<AccountGroup.UUID, AccountGroupById> newIncludedGroups = Maps.newHashMap();
-    List<AccountGroupByIdAud> newIncludedGroupsAudits = Lists.newLinkedList();
     List<GroupInfo> result = Lists.newLinkedList();
     Account.Id me = ((IdentifiedUser) control.getCurrentUser()).getAccountId();
 
     for (String includedGroup : input.groups) {
       GroupDescription.Basic d = groupsCollection.parse(includedGroup);
-      if (!control.canAddGroup(d.getGroupUUID())) {
+      if (!control.canAddGroup()) {
         throw new AuthException(String.format("Cannot add group: %s",
             d.getName()));
       }
@@ -117,15 +117,13 @@
         if (agi == null) {
           agi = new AccountGroupById(agiKey);
           newIncludedGroups.put(d.getGroupUUID(), agi);
-          newIncludedGroupsAudits.add(
-              new AccountGroupByIdAud(agi, me, TimeUtil.nowTs()));
         }
       }
       result.add(json.format(d));
     }
 
     if (!newIncludedGroups.isEmpty()) {
-      db.get().accountGroupByIdAud().insert(newIncludedGroupsAudits);
+      auditService.dispatchAddGroupsToGroup(me, newIncludedGroups.values());
       db.get().accountGroupById().insert(newIncludedGroups.values());
       for (AccountGroupById agi : newIncludedGroups.values()) {
         groupIncludeCache.evictParentGroupsOf(agi.getIncludeUUID());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
index df58c8f..5c7fcbc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
@@ -17,6 +17,8 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.gerrit.audit.AuditService;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -25,13 +27,12 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountException;
-import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.AccountsCollection;
@@ -39,7 +40,6 @@
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.group.AddMembers.Input;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -80,8 +80,9 @@
   private final AccountsCollection accounts;
   private final AccountResolver accountResolver;
   private final AccountCache accountCache;
-  private final AccountInfo.Loader.Factory infoFactory;
+  private final AccountLoader.Factory infoFactory;
   private final Provider<ReviewDb> db;
+  private final AuditService auditService;
 
   @Inject
   AddMembers(AccountManager accountManager,
@@ -89,9 +90,11 @@
       AccountsCollection accounts,
       AccountResolver accountResolver,
       AccountCache accountCache,
-      AccountInfo.Loader.Factory infoFactory,
-      Provider<ReviewDb> db) {
+      AccountLoader.Factory infoFactory,
+      Provider<ReviewDb> db,
+      AuditService auditService) {
     this.accountManager = accountManager;
+    this.auditService = auditService;
     this.authType = authConfig.getAuthType();
     this.accounts = accounts;
     this.accountResolver = accountResolver;
@@ -112,10 +115,9 @@
 
     GroupControl control = resource.getControl();
     Map<Account.Id, AccountGroupMember> newAccountGroupMembers = Maps.newHashMap();
-    List<AccountGroupMemberAudit> newAccountGroupMemberAudits = Lists.newLinkedList();
     List<AccountInfo> result = Lists.newLinkedList();
     Account.Id me = ((IdentifiedUser) control.getCurrentUser()).getAccountId();
-    AccountInfo.Loader loader = infoFactory.create(true);
+    AccountLoader loader = infoFactory.create(true);
 
     for (String nameOrEmail : input.members) {
       Account a = findAccount(nameOrEmail);
@@ -124,7 +126,7 @@
             "Account Inactive: %s", nameOrEmail));
       }
 
-      if (!control.canAddMember(a.getId())) {
+      if (!control.canAddMember()) {
         throw new AuthException("Cannot add member: " + a.getFullName());
       }
 
@@ -135,14 +137,12 @@
         if (m == null) {
           m = new AccountGroupMember(key);
           newAccountGroupMembers.put(m.getAccountId(), m);
-          newAccountGroupMemberAudits.add(
-              new AccountGroupMemberAudit(m, me, TimeUtil.nowTs()));
         }
       }
       result.add(loader.get(a.getId()));
     }
 
-    db.get().accountGroupMembersAudit().insert(newAccountGroupMemberAudits);
+    auditService.dispatchAddAccountsToGroup(me, newAccountGroupMembers.values());
     db.get().accountGroupMembers().insert(newAccountGroupMembers.values());
     for (AccountGroupMember m : newAccountGroupMembers.values()) {
       accountCache.evict(m.getAccountId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
index 4ca0200..cb80702 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.group;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupDescription;
@@ -101,7 +101,8 @@
       CreateGroupArgs args = new CreateGroupArgs();
       args.setGroupName(name);
       args.groupDescription = Strings.emptyToNull(input.description);
-      args.visibleToAll = Objects.firstNonNull(input.visibleToAll, defaultVisibleToAll);
+      args.visibleToAll = MoreObjects.firstNonNull(input.visibleToAll,
+          defaultVisibleToAll);
       args.ownerGroupId = ownerId;
       args.initialMembers = ownerId == null
           ? Collections.singleton(self.get().getAccountId())
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java
new file mode 100644
index 0000000..9e261f9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java
@@ -0,0 +1,220 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+import com.google.gerrit.audit.GroupMemberAuditListener;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.UniversalGroupBackend;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import org.slf4j.Logger;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+class DbGroupMemberAuditListener implements GroupMemberAuditListener {
+  private static final Logger log = org.slf4j.LoggerFactory
+      .getLogger(DbGroupMemberAuditListener.class);
+
+  private final SchemaFactory<ReviewDb> schema;
+  private final AccountCache accountCache;
+  private final GroupCache groupCache;
+  private final UniversalGroupBackend groupBackend;
+
+  @Inject
+  public DbGroupMemberAuditListener(SchemaFactory<ReviewDb> schema,
+      AccountCache accountCache, GroupCache groupCache,
+      UniversalGroupBackend groupBackend) {
+    this.schema = schema;
+    this.accountCache = accountCache;
+    this.groupCache = groupCache;
+    this.groupBackend = groupBackend;
+  }
+
+  @Override
+  public void onAddAccountsToGroup(Account.Id me,
+      Collection<AccountGroupMember> added) {
+    List<AccountGroupMemberAudit> auditInserts = Lists.newLinkedList();
+    for (AccountGroupMember m : added) {
+      AccountGroupMemberAudit audit =
+          new AccountGroupMemberAudit(m, me, TimeUtil.nowTs());
+      auditInserts.add(audit);
+    }
+    try {
+      ReviewDb db = schema.open();
+      try {
+        db.accountGroupMembersAudit().insert(auditInserts);
+      } finally {
+        db.close();
+      }
+    } catch (OrmException e) {
+      logOrmExceptionForAccounts(
+          "Cannot log add accounts to group event performed by user", me,
+          added, e);
+    }
+  }
+
+  @Override
+  public void onDeleteAccountsFromGroup(Account.Id me,
+      Collection<AccountGroupMember> removed) {
+    List<AccountGroupMemberAudit> auditInserts = Lists.newLinkedList();
+    List<AccountGroupMemberAudit> auditUpdates = Lists.newLinkedList();
+    try {
+      ReviewDb db = schema.open();
+      try {
+        for (AccountGroupMember m : removed) {
+          AccountGroupMemberAudit audit = null;
+          for (AccountGroupMemberAudit a : db.accountGroupMembersAudit()
+              .byGroupAccount(m.getAccountGroupId(), m.getAccountId())) {
+            if (a.isActive()) {
+              audit = a;
+              break;
+            }
+          }
+
+          if (audit != null) {
+            audit.removed(me, TimeUtil.nowTs());
+            auditUpdates.add(audit);
+          } else {
+            audit = new AccountGroupMemberAudit(m, me, TimeUtil.nowTs());
+            audit.removedLegacy();
+            auditInserts.add(audit);
+          }
+        }
+        db.accountGroupMembersAudit().update(auditUpdates);
+        db.accountGroupMembersAudit().insert(auditInserts);
+      } finally {
+        db.close();
+      }
+    } catch (OrmException e) {
+      logOrmExceptionForAccounts(
+          "Cannot log delete accounts from group event performed by user", me,
+          removed, e);
+    }
+  }
+
+  @Override
+  public void onAddGroupsToGroup(Account.Id me,
+      Collection<AccountGroupById> added) {
+    List<AccountGroupByIdAud> includesAudit = new ArrayList<>();
+    for (AccountGroupById groupInclude : added) {
+      AccountGroupByIdAud audit =
+          new AccountGroupByIdAud(groupInclude, me, TimeUtil.nowTs());
+      includesAudit.add(audit);
+    }
+    try {
+      ReviewDb db = schema.open();
+      try {
+        db.accountGroupByIdAud().insert(includesAudit);
+      } finally {
+        db.close();
+      }
+    } catch (OrmException e) {
+      logOrmExceptionForGroups(
+          "Cannot log add groups to group event performed by user", me, added,
+          e);
+    }
+  }
+
+  @Override
+  public void onDeleteGroupsFromGroup(Account.Id me,
+      Collection<AccountGroupById> removed) {
+    final List<AccountGroupByIdAud> auditUpdates = Lists.newLinkedList();
+    try {
+      ReviewDb db = schema.open();
+      try {
+        for (final AccountGroupById g : removed) {
+          AccountGroupByIdAud audit = null;
+          for (AccountGroupByIdAud a : db.accountGroupByIdAud()
+              .byGroupInclude(g.getGroupId(), g.getIncludeUUID())) {
+            if (a.isActive()) {
+              audit = a;
+              break;
+            }
+          }
+
+          if (audit != null) {
+            audit.removed(me, TimeUtil.nowTs());
+            auditUpdates.add(audit);
+          }
+        }
+        db.accountGroupByIdAud().update(auditUpdates);
+      } finally {
+        db.close();
+      }
+    } catch (OrmException e) {
+      logOrmExceptionForGroups(
+          "Cannot log delete groups from group event performed by user", me,
+          removed, e);
+    }
+  }
+
+  private void logOrmExceptionForAccounts(String header, Account.Id me,
+      Collection<AccountGroupMember> values, OrmException e) {
+    List<String> descriptions = new ArrayList<>();
+    for (AccountGroupMember m : values) {
+      Account.Id accountId = m.getAccountId();
+      String userName = accountCache.get(accountId).getUserName();
+      AccountGroup.Id groupId = m.getAccountGroupId();
+      String groupName = groupCache.get(groupId).getName();
+
+      descriptions.add(MessageFormat.format("account {0}/{1}, group {2}/{3}",
+          accountId, userName, groupId, groupName));
+    }
+    logOrmException(header, me, descriptions, e);
+  }
+
+  private void logOrmExceptionForGroups(String header, Account.Id me,
+      Collection<AccountGroupById> values, OrmException e) {
+    List<String> descriptions = new ArrayList<>();
+    for (AccountGroupById m : values) {
+      AccountGroup.UUID groupUuid = m.getIncludeUUID();
+      String groupName = groupBackend.get(groupUuid).getName();
+      AccountGroup.Id targetGroupId = m.getGroupId();
+      String targetGroupName = groupCache.get(targetGroupId).getName();
+
+      descriptions.add(MessageFormat.format("group {0}/{1}, group {2}/{3}",
+          groupUuid, groupName, targetGroupId, targetGroupName));
+    }
+    logOrmException(header, me, descriptions, e);
+  }
+
+  private void logOrmException(String header, Account.Id me,
+      Iterable<?> values, OrmException e) {
+    StringBuilder message = new StringBuilder(header);
+    message.append(" ");
+    message.append(me);
+    message.append("/");
+    message.append(accountCache.get(me).getUserName());
+    message.append(": ");
+    message.append(Joiner.on("; ").join(values));
+    log.error(message.toString(), e);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
index 8cc1a4a..3d99565 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -26,14 +27,12 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupById;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
 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.GroupControl;
 import com.google.gerrit.server.account.GroupIncludeCache;
 import com.google.gerrit.server.group.AddIncludedGroups.Input;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -48,16 +47,17 @@
   private final GroupIncludeCache groupIncludeCache;
   private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> self;
+  private final AuditService auditService;
 
   @Inject
   DeleteIncludedGroups(GroupsCollection groupsCollection,
-      GroupIncludeCache groupIncludeCache,
-      Provider<ReviewDb> db,
-      Provider<CurrentUser> self) {
+      GroupIncludeCache groupIncludeCache, Provider<ReviewDb> db,
+      Provider<CurrentUser> self, AuditService auditService) {
     this.groupsCollection = groupsCollection;
     this.groupIncludeCache = groupIncludeCache;
     this.db = db;
     this.self = self;
+    this.auditService = auditService;
   }
 
   @Override
@@ -76,7 +76,7 @@
 
     for (final String includedGroup : input.groups) {
       GroupDescription.Basic d = groupsCollection.parse(includedGroup);
-      if (!control.canRemoveGroup(d.getGroupUUID())) {
+      if (!control.canRemoveGroup()) {
         throw new AuthException(String.format("Cannot delete group: %s",
             d.getName()));
       }
@@ -109,27 +109,9 @@
     return groups;
   }
 
-  private void writeAudits(final List<AccountGroupById> toBeRemoved)
-      throws OrmException {
+  private void writeAudits(final List<AccountGroupById> toRemoved) {
     final Account.Id me = ((IdentifiedUser) self.get()).getAccountId();
-    final List<AccountGroupByIdAud> auditUpdates = Lists.newLinkedList();
-    for (final AccountGroupById g : toBeRemoved) {
-      AccountGroupByIdAud audit = null;
-      for (AccountGroupByIdAud a : db.get()
-          .accountGroupByIdAud().byGroupInclude(g.getGroupId(),
-              g.getIncludeUUID())) {
-        if (a.isActive()) {
-          audit = a;
-          break;
-        }
-      }
-
-      if (audit != null) {
-        audit.removed(me, TimeUtil.nowTs());
-        auditUpdates.add(audit);
-      }
-    }
-    db.get().accountGroupByIdAud().update(auditUpdates);
+    auditService.dispatchDeleteGroupsFromGroup(me, toRemoved);
   }
 
   @Singleton
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
index 654ad88..3047994 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -24,7 +25,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -32,7 +32,6 @@
 import com.google.gerrit.server.account.AccountsCollection;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.AddMembers.Input;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -47,15 +46,18 @@
   private final AccountCache accountCache;
   private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> self;
+  private final AuditService auditService;
 
   @Inject
   DeleteMembers(AccountsCollection accounts,
       AccountCache accountCache, Provider<ReviewDb> db,
-      Provider<CurrentUser> self) {
+      Provider<CurrentUser> self,
+      AuditService auditService) {
     this.accounts = accounts;
     this.accountCache = accountCache;
     this.db = db;
     this.self = self;
+    this.auditService = auditService;
   }
 
   @Override
@@ -75,7 +77,7 @@
     for (final String nameOrEmail : input.members) {
       Account a = accounts.parse(nameOrEmail).getAccount();
 
-      if (!control.canRemoveMember(a.getId())) {
+      if (!control.canRemoveMember()) {
         throw new AuthException("Cannot delete member: " + a.getFullName());
       }
 
@@ -94,32 +96,9 @@
     return Response.none();
   }
 
-  private void writeAudits(final List<AccountGroupMember> toBeRemoved)
-      throws OrmException {
+  private void writeAudits(final List<AccountGroupMember> toRemove) {
     final Account.Id me = ((IdentifiedUser) self.get()).getAccountId();
-    final List<AccountGroupMemberAudit> auditUpdates = Lists.newLinkedList();
-    final List<AccountGroupMemberAudit> auditInserts = Lists.newLinkedList();
-    for (final AccountGroupMember m : toBeRemoved) {
-      AccountGroupMemberAudit audit = null;
-      for (AccountGroupMemberAudit a : db.get().accountGroupMembersAudit()
-          .byGroupAccount(m.getAccountGroupId(), m.getAccountId())) {
-        if (a.isActive()) {
-          audit = a;
-          break;
-        }
-      }
-
-      if (audit != null) {
-        audit.removed(me, TimeUtil.nowTs());
-        auditUpdates.add(audit);
-      } else {
-        audit = new AccountGroupMemberAudit(m, me, TimeUtil.nowTs());
-        audit.removedLegacy();
-        auditInserts.add(audit);
-      }
-    }
-    db.get().accountGroupMembersAudit().update(auditUpdates);
-    db.get().accountGroupMembersAudit().insert(auditInserts);
+    auditService.dispatchDeleteAccountsFromGroup(me, toRemove);
   }
 
   private Map<Account.Id, AccountGroupMember> getMembers(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetMember.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetMember.java
index 2782932e..9d270ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetMember.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetMember.java
@@ -14,24 +14,25 @@
 
 package com.google.gerrit.server.group;
 
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.account.AccountLoader;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 @Singleton
 public class GetMember implements RestReadView<MemberResource> {
-  private final AccountInfo.Loader.Factory infoFactory;
+  private final AccountLoader.Factory infoFactory;
 
   @Inject
-  GetMember(AccountInfo.Loader.Factory infoFactory) {
+  GetMember(AccountLoader.Factory infoFactory) {
     this.infoFactory = infoFactory;
   }
 
   @Override
   public AccountInfo apply(MemberResource rsrc) throws OrmException {
-    AccountInfo.Loader loader = infoFactory.create(true);
+    AccountLoader loader = infoFactory.create(true);
     AccountInfo info = loader.get(rsrc.getMember().getAccountId());
     loader.fill();
     return info;
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 9f851f4..96b4234 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
@@ -21,10 +21,10 @@
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.common.groups.ListGroupsOption;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.account.AccountInfo;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gwtorm.server.OrmException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupResource.java
index 89b99a1..7975f24 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupResource.java
@@ -25,7 +25,8 @@
 
   private final GroupDescription.Basic member;
 
-  IncludedGroupResource(GroupResource group, GroupDescription.Basic member) {
+  public IncludedGroupResource(GroupResource group,
+      GroupDescription.Basic member) {
     super(group);
     this.member = member;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java
index 72f17b4..8d0831d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java
@@ -73,7 +73,7 @@
     GroupDescription.Basic member =
         groupsCollection.parse(TopLevelResource.INSTANCE, id).getGroup();
     if (isMember(parent, member)
-        && resource.getControl().canSeeGroup(member.getGroupUUID())) {
+        && resource.getControl().canSeeGroup()) {
       return new IncludedGroupResource(resource, member);
     }
     throw new ResourceNotFoundException(id);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
index 0998602..40d0420 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.group;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -135,7 +135,7 @@
   public Object apply(TopLevelResource resource) throws OrmException {
     final Map<String, GroupInfo> output = Maps.newTreeMap();
     for (GroupInfo info : get()) {
-      output.put(Objects.firstNonNull(
+      output.put(MoreObjects.firstNonNull(
           info.name,
           "Group " + Url.decode(info.id)), info);
       info.name = null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
index c4b2ae2..aab9efe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
@@ -20,13 +20,14 @@
 import com.google.common.collect.Ordering;
 import com.google.gerrit.common.data.GroupDetail;
 import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupDetailFactory;
 import com.google.gwtorm.server.OrmException;
@@ -43,7 +44,7 @@
 public class ListMembers implements RestReadView<GroupResource> {
   private final GroupCache groupCache;
   private final GroupDetailFactory.Factory groupDetailFactory;
-  private final AccountInfo.Loader accountLoader;
+  private final AccountLoader accountLoader;
 
   @Option(name = "--recursive", usage = "to resolve included groups recursively")
   private boolean recursive;
@@ -51,7 +52,7 @@
   @Inject
   protected ListMembers(GroupCache groupCache,
       GroupDetailFactory.Factory groupDetailFactory,
-      AccountInfo.Loader.Factory accountLoaderFactory) {
+      AccountLoader.Factory accountLoaderFactory) {
     this.groupCache = groupCache;
     this.groupDetailFactory = groupDetailFactory;
     this.accountLoader = accountLoaderFactory.create(true);
@@ -72,12 +73,12 @@
   }
 
   public List<AccountInfo> apply(AccountGroup group)
-      throws MethodNotAllowedException, OrmException {
+      throws OrmException {
     return apply(group.getGroupUUID());
   }
 
   public List<AccountInfo> apply(AccountGroup.UUID groupId)
-      throws MethodNotAllowedException, OrmException {
+      throws OrmException {
     final Map<Account.Id, AccountInfo> members =
         getMembers(groupId, new HashSet<AccountGroup.UUID>());
     final List<AccountInfo> memberInfos = Lists.newArrayList(members.values());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java
index 97338f1..9b5d9ab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java
@@ -18,7 +18,9 @@
 import static com.google.gerrit.server.group.IncludedGroupResource.INCLUDED_GROUP_KIND;
 import static com.google.gerrit.server.group.MemberResource.MEMBER_KIND;
 
+import com.google.gerrit.audit.GroupMemberAuditListener;
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.RestApiModule;
 import com.google.gerrit.server.group.AddIncludedGroups.UpdateIncludedGroup;
 import com.google.gerrit.server.group.AddMembers.UpdateMember;
@@ -65,5 +67,9 @@
     delete(INCLUDED_GROUP_KIND).to(DeleteIncludedGroup.class);
 
     install(new FactoryModuleBuilder().build(CreateGroup.Factory.class));
+
+    DynamicSet.bind(binder(), GroupMemberAuditListener.class).to(
+        DbGroupMemberAuditListener.class);
+
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/SystemGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/SystemGroupBackend.java
index c888f73..59ca272 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/SystemGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/SystemGroupBackend.java
@@ -22,7 +22,7 @@
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.AbstractGroupBackend;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.gerrit.server.project.ProjectControl;
@@ -36,7 +36,7 @@
 import java.util.SortedMap;
 import java.util.TreeMap;
 
-public class SystemGroupBackend implements GroupBackend {
+public class SystemGroupBackend extends AbstractGroupBackend {
   /** Common UUID assigned to the "Anonymous Users" group. */
   public static final AccountGroup.UUID ANONYMOUS_USERS =
       new AccountGroup.UUID("global:Anonymous-Users");
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 f067e27..2993739 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
@@ -14,8 +14,13 @@
 
 package com.google.gerrit.server.index;
 
-import com.google.common.base.Objects;
+import static com.google.common.base.MoreObjects.firstNonNull;
+
+import com.google.common.base.Function;
 import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Account;
@@ -24,17 +29,17 @@
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeStatusPredicate;
-import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
 import com.google.gwtorm.protobuf.CodecFactory;
 import com.google.gwtorm.protobuf.ProtobufCodec;
 import com.google.gwtorm.server.OrmException;
 import com.google.protobuf.CodedOutputStream;
 
+import org.eclipse.jgit.revwalk.FooterLine;
+
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.sql.Timestamp;
@@ -68,7 +73,11 @@
         @Override
         public String get(ChangeData input, FillArgs args)
             throws OrmException {
-          return input.change().getKey().get();
+          Change c = input.change();
+          if (c == null) {
+            return null;
+          }
+          return c.getKey().get();
         }
       };
 
@@ -79,8 +88,11 @@
         @Override
         public String get(ChangeData input, FillArgs args)
             throws OrmException {
-          return ChangeStatusPredicate.VALUES.get(
-              input.change().getStatus());
+          Change c = input.change();
+          if (c == null) {
+            return null;
+          }
+          return ChangeStatusPredicate.canonicalize(c.getStatus());
         }
       };
 
@@ -91,7 +103,11 @@
         @Override
         public String get(ChangeData input, FillArgs args)
             throws OrmException {
-          return input.change().getProject().get();
+          Change c = input.change();
+          if (c == null) {
+            return null;
+          }
+          return c.getProject().get();
         }
       };
 
@@ -102,7 +118,11 @@
         @Override
         public String get(ChangeData input, FillArgs args)
             throws OrmException {
-          return input.change().getProject().get();
+          Change c = input.change();
+          if (c == null) {
+            return null;
+          }
+          return c.getProject().get();
         }
       };
 
@@ -113,19 +133,11 @@
         @Override
         public String get(ChangeData input, FillArgs args)
             throws OrmException {
-          return input.change().getDest().get();
-        }
-      };
-
-  @Deprecated
-  /** Topic, a short annotation on the branch. */
-  public static final FieldDef<ChangeData, String> LEGACY_TOPIC =
-      new FieldDef.Single<ChangeData, String>(
-          ChangeQueryBuilder.FIELD_TOPIC, FieldType.EXACT, false) {
-        @Override
-        public String get(ChangeData input, FillArgs args)
-            throws OrmException {
-          return input.change().getTopic();
+          Change c = input.change();
+          if (c == null) {
+            return null;
+          }
+          return c.getDest().get();
         }
       };
 
@@ -136,24 +148,14 @@
         @Override
         public String get(ChangeData input, FillArgs args)
             throws OrmException {
-          return Objects.firstNonNull(input.change().getTopic(), "");
+          Change c = input.change();
+          if (c == null) {
+            return null;
+          }
+          return firstNonNull(c.getTopic(), "");
         }
       };
 
-  // Same value as UPDATED, but implementations truncated to minutes.
-  @Deprecated
-  /** Last update time since January 1, 1970. */
-  public static final FieldDef<ChangeData, Timestamp> LEGACY_UPDATED =
-      new FieldDef.Single<ChangeData, Timestamp>(
-          "updated", FieldType.TIMESTAMP, true) {
-        @Override
-        public Timestamp get(ChangeData input, FillArgs args)
-            throws OrmException {
-          return input.change().getLastUpdatedOn();
-        }
-      };
-
-
   /** Last update time since January 1, 1970. */
   public static final FieldDef<ChangeData, Timestamp> UPDATED =
       new FieldDef.Single<ChangeData, Timestamp>(
@@ -161,44 +163,11 @@
         @Override
         public Timestamp get(ChangeData input, FillArgs args)
             throws OrmException {
-          return input.change().getLastUpdatedOn();
-        }
-      };
-
-  @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().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.
-   */
-  @Deprecated
-  public static final FieldDef<ChangeData, Long> SORTKEY =
-      new FieldDef.Single<ChangeData, Long>(
-          "sortkey2", FieldType.LONG, true) {
-        @Override
-        public Long get(ChangeData input, FillArgs args)
-            throws OrmException {
-          return ChangeUtil.parseSortKey(input.change().getSortKey());
+          Change c = input.change();
+          if (c == null) {
+            return null;
+          }
+          return c.getLastUpdatedOn();
         }
       };
 
@@ -210,14 +179,19 @@
         @Override
         public Iterable<String> get(ChangeData input, FillArgs args)
             throws OrmException {
-          return input.currentFilePaths();
+          return firstNonNull(input.currentFilePaths(),
+              ImmutableList.<String> of());
         }
       };
 
   public static Set<String> getFileParts(ChangeData cd) throws OrmException {
+    List<String> paths = cd.currentFilePaths();
+    if (paths == null) {
+      return ImmutableSet.of();
+    }
     Splitter s = Splitter.on('/').omitEmptyStrings();
     Set<String> r = Sets.newHashSet();
-    for (String path : cd.currentFilePaths()) {
+    for (String path : paths) {
       for (String part : s.split(path)) {
         r.add(part);
       }
@@ -225,6 +199,25 @@
     return r;
   }
 
+  /** Hashtags tied to a change */
+  public static final FieldDef<ChangeData, Iterable<String>> HASHTAG =
+      new FieldDef.Repeatable<ChangeData, String>(
+          "hashtag", FieldType.EXACT, false) {
+        @Override
+        public Iterable<String> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return ImmutableSet.copyOf(Iterables.transform(input.notes().load()
+              .getHashtags(), new Function<String, String>() {
+
+            @Override
+            public String apply(String input) {
+              return input.toLowerCase();
+            }
+
+          }));
+        }
+      };
+
   /** Components of each file path modified in the current patch set. */
   public static final FieldDef<ChangeData, Iterable<String>> FILE_PART =
       new FieldDef.Repeatable<ChangeData, String>(
@@ -243,7 +236,11 @@
         @Override
         public Integer get(ChangeData input, FillArgs args)
             throws OrmException {
-          return input.change().getOwner().get();
+          Change c = input.change();
+          if (c == null) {
+            return null;
+          }
+          return c.getOwner().get();
         }
       };
 
@@ -254,9 +251,12 @@
         @Override
         public Iterable<Integer> get(ChangeData input, FillArgs args)
             throws OrmException {
+          Change c = input.change();
+          if (c == null) {
+            return null;
+          }
           Set<Integer> r = Sets.newHashSet();
-          if (!args.allowsDrafts &&
-              input.change().getStatus() == Change.Status.DRAFT) {
+          if (!args.allowsDrafts && c.getStatus() == Change.Status.DRAFT) {
             return r;
           }
           for (PatchSetApproval a : input.approvals().values()) {
@@ -291,9 +291,13 @@
         public Iterable<String> get(ChangeData input, FillArgs args)
             throws OrmException {
           try {
-            return Sets.newHashSet(args.trackingFooters.extract(
-                input.commitFooters()).values());
-          } catch (NoSuchChangeException | IOException e) {
+            List<FooterLine> footers = input.commitFooters();
+            if (footers == null) {
+              return null;
+            }
+            return Sets.newHashSet(
+                args.trackingFooters.extract(footers).values());
+          } catch (IOException e) {
             throw new OrmException(e);
           }
         }
@@ -347,7 +351,11 @@
     @Override
     public byte[] get(ChangeData input, FieldDef.FillArgs args)
         throws OrmException {
-      return CODEC.encodeToByteArray(input.change());
+      Change c = input.change();
+      if (c == null) {
+        return null;
+      }
+      return CODEC.encodeToByteArray(c);
     }
   }
 
@@ -394,7 +402,7 @@
         public String get(ChangeData input, FillArgs args) throws OrmException {
           try {
             return input.commitMessage();
-          } catch (NoSuchChangeException | IOException e) {
+          } catch (IOException e) {
             throw new OrmException(e);
           }
         }
@@ -408,7 +416,7 @@
         public Iterable<String> get(ChangeData input, FillArgs args)
             throws OrmException {
           Set<String> r = Sets.newHashSet();
-          for (PatchLineComment c : input.comments()) {
+          for (PatchLineComment c : input.publishedComments()) {
             r.add(c.getMessage());
           }
           for (ChangeMessage m : input.messages()) {
@@ -419,13 +427,33 @@
       };
 
   /** Whether the change is mergeable. */
-  public static final FieldDef<ChangeData, String> MERGEABLE =
+  @Deprecated
+  public static final FieldDef<ChangeData, String> LEGACY_MERGEABLE =
       new FieldDef.Single<ChangeData, String>(
           ChangeQueryBuilder.FIELD_MERGEABLE, FieldType.EXACT, false) {
         @Override
         public String get(ChangeData input, FillArgs args)
             throws OrmException {
-          return input.change().isMergeable() ? "1" : null;
+          Boolean m = input.isMergeable();
+          if (m == null) {
+            return null;
+          }
+          return m ? "1" : null;
+        }
+      };
+
+  /** Whether the change is mergeable. */
+  public static final FieldDef<ChangeData, String> MERGEABLE =
+      new FieldDef.Single<ChangeData, String>(
+          "mergeable2", FieldType.EXACT, true) {
+        @Override
+        public String get(ChangeData input, FillArgs args)
+            throws OrmException {
+          Boolean m = input.isMergeable();
+          if (m == null) {
+            return null;
+          }
+          return m ? "1" : "0";
         }
       };
 
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 1673678..3b04f05 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.index;
 
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -39,23 +40,12 @@
   public void close();
 
   /**
-   * Insert a change document into the index.
-   * <p>
-   * Results may not be immediately visible to searchers, but should be visible
-   * within a reasonable amount of time.
-   *
-   * @param cd change document
-   *
-   * @throws IOException if the change could not be inserted.
-   */
-  public void insert(ChangeData cd) throws IOException;
-
-  /**
    * Update a change document in the index.
    * <p>
    * Semantically equivalent to deleting the document and reinserting it with
-   * new field values. Results may not be immediately visible to searchers, but
-   * should be visible within a reasonable amount of time.
+   * new field values. A document that does not already exist is created. Results
+   * may not be immediately visible to searchers, but should be visible within a
+   * reasonable amount of time.
    *
    * @param cd change document
    *
@@ -64,22 +54,13 @@
   public void replace(ChangeData cd) throws IOException;
 
   /**
-   * Delete a change document from the index.
-   *
-   * @param cd change document
-   *
-   * @throws IOException
-   */
-  public void delete(ChangeData cd) throws IOException;
-
-  /**
    * Delete a change document from the index by id.
    *
-   * @param id change document id
+   * @param id change id
    *
    * @throws IOException
    */
-  public void delete(int id) throws IOException;
+  public void delete(Change.Id id) throws IOException;
 
   /**
    * Delete all change documents from the index.
@@ -103,10 +84,7 @@
    * @param start offset in results list at which to start returning results.
    * @param limit maximum number of results to return.
    * @return a source of documents matching the predicate. Documents must be
-   *     returned in descending sort key order, unless a {@code sortkey_after}
-   *     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.
+   *     returned in descending updated timestamp order.
    *
    * @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 72cb88c..5cb5c65 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
@@ -18,6 +18,7 @@
 import com.google.common.util.concurrent.Atomics;
 import com.google.common.util.concurrent.CheckedFuture;
 import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -38,8 +39,10 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicReference;
@@ -55,8 +58,9 @@
       LoggerFactory.getLogger(ChangeIndexer.class);
 
   public interface Factory {
-    ChangeIndexer create(ChangeIndex index);
-    ChangeIndexer create(IndexCollection indexes);
+    ChangeIndexer create(ListeningExecutorService executor, ChangeIndex index);
+    ChangeIndexer create(ListeningExecutorService executor,
+        IndexCollection indexes);
   }
 
   private static final Function<Exception, IOException> MAPPER =
@@ -82,10 +86,10 @@
   private final ListeningExecutorService executor;
 
   @AssistedInject
-  ChangeIndexer(@IndexExecutor ListeningExecutorService executor,
-      SchemaFactory<ReviewDb> schemaFactory,
+  ChangeIndexer(SchemaFactory<ReviewDb> schemaFactory,
       ChangeData.Factory changeDataFactory,
       ThreadLocalRequestContext context,
+      @Assisted ListeningExecutorService executor,
       @Assisted ChangeIndex index) {
     this.executor = executor;
     this.schemaFactory = schemaFactory;
@@ -96,10 +100,10 @@
   }
 
   @AssistedInject
-  ChangeIndexer(@IndexExecutor ListeningExecutorService executor,
-      SchemaFactory<ReviewDb> schemaFactory,
+  ChangeIndexer(SchemaFactory<ReviewDb> schemaFactory,
       ChangeData.Factory changeDataFactory,
       ThreadLocalRequestContext context,
+      @Assisted ListeningExecutorService executor,
       @Assisted IndexCollection indexes) {
     this.executor = executor;
     this.schemaFactory = schemaFactory;
@@ -117,11 +121,29 @@
    */
   public CheckedFuture<?, IOException> indexAsync(Change.Id id) {
     return executor != null
-        ? submit(new Task(id, false))
+        ? submit(new IndexTask(id))
         : Futures.<Object, IOException> immediateCheckedFuture(null);
   }
 
   /**
+   * Start indexing multiple changes in parallel.
+   *
+   * @param ids changes to index.
+   * @return future for completing indexing of all changes.
+   */
+  public CheckedFuture<?, IOException> indexAsync(Collection<Change.Id> ids) {
+    List<ListenableFuture<?>> futures = new ArrayList<>(ids.size());
+    for (Change.Id id : ids) {
+      futures.add(indexAsync(id));
+    }
+    // allAsList propagates the first seen exception, wrapped in
+    // ExecutionException, so we can reuse the same mapper as for a single
+    // future. Assume the actual contents of the exception are not useful to
+    // callers. All exceptions are already logged by IndexTask.
+    return Futures.makeChecked(Futures.allAsList(futures), MAPPER);
+  }
+
+  /**
    * Synchronously index a change.
    *
    * @param cd change to index.
@@ -150,40 +172,17 @@
    */
   public CheckedFuture<?, IOException> deleteAsync(Change.Id id) {
     return executor != null
-        ? submit(new Task(id, true))
+        ? submit(new DeleteTask(id))
         : Futures.<Object, IOException> immediateCheckedFuture(null);
   }
 
   /**
    * Synchronously delete a change.
    *
-   * @param cd change to delete.
+   * @param id change ID to delete.
    */
-  public void delete(ChangeData cd) throws IOException {
-    for (ChangeIndex i : getWriteIndexes()) {
-      i.delete(cd);
-    }
-  }
-
-  /**
-   * Synchronously delete a change by id.
-   *
-   * @param id change to delete.
-   */
-  public void delete(int id) throws IOException {
-    for (ChangeIndex i : getWriteIndexes()) {
-      i.delete(id);
-    }
-  }
-
-  /**
-   * Synchronously delete a change.
-   *
-   * @param change change to delete.
-   * @param db review database.
-   */
-  public void delete(ReviewDb db, Change change) throws IOException {
-    delete(changeDataFactory.create(db, change));
+  public void delete(Change.Id id) throws IOException {
+    new DeleteTask(id).call();
   }
 
   private Collection<ChangeIndex> getWriteIndexes() {
@@ -196,13 +195,11 @@
     return Futures.makeChecked(executor.submit(task), MAPPER);
   }
 
-  private class Task implements Callable<Void> {
+  private class IndexTask implements Callable<Void> {
     private final Change.Id id;
-    private final boolean delete;
 
-    private Task(Change.Id id, boolean delete) {
+    private IndexTask(Change.Id id) {
       this.id = id;
-      this.delete = delete;
     }
 
     @Override
@@ -237,14 +234,8 @@
         try {
           ChangeData cd = changeDataFactory.create(
               newCtx.getReviewDbProvider().get(), id);
-          if (delete) {
-            for (ChangeIndex i : getWriteIndexes()) {
-              i.delete(cd);
-            }
-          } else {
-            for (ChangeIndex i : getWriteIndexes()) {
-              i.replace(cd);
-            }
+          for (ChangeIndex i : getWriteIndexes()) {
+            i.replace(cd);
           }
           return null;
         } finally  {
@@ -265,4 +256,23 @@
       return "index-change-" + id.get();
     }
   }
+
+  private class DeleteTask implements Callable<Void> {
+    private final Change.Id id;
+
+    private DeleteTask(Change.Id id) {
+      this.id = id;
+    }
+
+    @Override
+    public Void call() throws IOException {
+      // Don't bother setting a RequestContext to provide the DB.
+      // Implementations should not need to access the DB in order to delete a
+      // change ID.
+      for (ChangeIndex i : getWriteIndexes()) {
+        i.delete(id);
+      }
+      return null;
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
index 8bb8f0b..05bf9bd 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
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
@@ -24,177 +25,13 @@
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
 import java.lang.reflect.ParameterizedType;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Map;
 
 /** Secondary index schemas for changes. */
 public class ChangeSchemas {
   @SuppressWarnings("deprecation")
-  static final Schema<ChangeData> V1 = release(
-        ChangeField.LEGACY_ID,
-        ChangeField.ID,
-        ChangeField.STATUS,
-        ChangeField.PROJECT,
-        ChangeField.REF,
-        ChangeField.LEGACY_TOPIC,
-        ChangeField.LEGACY_UPDATED,
-        ChangeField.LEGACY_SORTKEY,
-        ChangeField.PATH,
-        ChangeField.OWNER,
-        ChangeField.REVIEWER,
-        ChangeField.COMMIT,
-        ChangeField.TR,
-        ChangeField.LABEL,
-        ChangeField.REVIEWED,
-        ChangeField.COMMIT_MESSAGE,
-        ChangeField.COMMENT);
-
-  @SuppressWarnings("deprecation")
-  static final Schema<ChangeData> V2 = release(
-        ChangeField.LEGACY_ID,
-        ChangeField.ID,
-        ChangeField.STATUS,
-        ChangeField.PROJECT,
-        ChangeField.REF,
-        ChangeField.LEGACY_TOPIC,
-        ChangeField.LEGACY_UPDATED,
-        ChangeField.LEGACY_SORTKEY,
-        ChangeField.PATH,
-        ChangeField.OWNER,
-        ChangeField.REVIEWER,
-        ChangeField.COMMIT,
-        ChangeField.TR,
-        ChangeField.LABEL,
-        ChangeField.REVIEWED,
-        ChangeField.COMMIT_MESSAGE,
-        ChangeField.COMMENT,
-        ChangeField.CHANGE,
-        ChangeField.APPROVAL);
-
-  @SuppressWarnings("deprecation")
-  static final Schema<ChangeData> V3 = release(
-        ChangeField.LEGACY_ID,
-        ChangeField.ID,
-        ChangeField.STATUS,
-        ChangeField.PROJECT,
-        ChangeField.REF,
-        ChangeField.LEGACY_TOPIC,
-        ChangeField.LEGACY_UPDATED,
-        ChangeField.SORTKEY,
-        ChangeField.PATH,
-        ChangeField.OWNER,
-        ChangeField.REVIEWER,
-        ChangeField.COMMIT,
-        ChangeField.TR,
-        ChangeField.LABEL,
-        ChangeField.REVIEWED,
-        ChangeField.COMMIT_MESSAGE,
-        ChangeField.COMMENT,
-        ChangeField.CHANGE,
-        ChangeField.APPROVAL);
-
-  // For upgrade to Lucene 4.4.0 index format only.
-  static final Schema<ChangeData> V4 = release(V3.getFields().values());
-
-  @SuppressWarnings("deprecation")
-  static final Schema<ChangeData> V5 = release(
-        ChangeField.LEGACY_ID,
-        ChangeField.ID,
-        ChangeField.STATUS,
-        ChangeField.PROJECT,
-        ChangeField.REF,
-        ChangeField.LEGACY_TOPIC,
-        ChangeField.LEGACY_UPDATED,
-        ChangeField.SORTKEY,
-        ChangeField.PATH,
-        ChangeField.OWNER,
-        ChangeField.REVIEWER,
-        ChangeField.COMMIT,
-        ChangeField.TR,
-        ChangeField.LABEL,
-        ChangeField.REVIEWED,
-        ChangeField.COMMIT_MESSAGE,
-        ChangeField.COMMENT,
-        ChangeField.CHANGE,
-        ChangeField.APPROVAL,
-        ChangeField.MERGEABLE);
-
-  // For upgrade to Lucene 4.6.0 index format only.
-  static final Schema<ChangeData> V6 = release(V5.getFields().values());
-
-  @SuppressWarnings("deprecation")
-  static final Schema<ChangeData> V7 = release(
-        ChangeField.LEGACY_ID,
-        ChangeField.ID,
-        ChangeField.STATUS,
-        ChangeField.PROJECT,
-        ChangeField.REF,
-        ChangeField.LEGACY_TOPIC,
-        ChangeField.LEGACY_UPDATED,
-        ChangeField.SORTKEY,
-        ChangeField.FILE_PART,
-        ChangeField.PATH,
-        ChangeField.OWNER,
-        ChangeField.REVIEWER,
-        ChangeField.COMMIT,
-        ChangeField.TR,
-        ChangeField.LABEL,
-        ChangeField.REVIEWED,
-        ChangeField.COMMIT_MESSAGE,
-        ChangeField.COMMENT,
-        ChangeField.CHANGE,
-        ChangeField.APPROVAL,
-        ChangeField.MERGEABLE);
-
-  @SuppressWarnings("deprecation")
-  static final Schema<ChangeData> V8 = release(
-        ChangeField.LEGACY_ID,
-        ChangeField.ID,
-        ChangeField.STATUS,
-        ChangeField.PROJECT,
-        ChangeField.REF,
-        ChangeField.LEGACY_TOPIC,
-        ChangeField.UPDATED,
-        ChangeField.FILE_PART,
-        ChangeField.PATH,
-        ChangeField.OWNER,
-        ChangeField.REVIEWER,
-        ChangeField.COMMIT,
-        ChangeField.TR,
-        ChangeField.LABEL,
-        ChangeField.REVIEWED,
-        ChangeField.COMMIT_MESSAGE,
-        ChangeField.COMMENT,
-        ChangeField.CHANGE,
-        ChangeField.APPROVAL,
-        ChangeField.MERGEABLE);
-
-  @SuppressWarnings("deprecation")
-  static final Schema<ChangeData> V9 = release(
-        ChangeField.LEGACY_ID,
-        ChangeField.ID,
-        ChangeField.STATUS,
-        ChangeField.PROJECT,
-        ChangeField.PROJECTS,
-        ChangeField.REF,
-        ChangeField.LEGACY_TOPIC,
-        ChangeField.UPDATED,
-        ChangeField.FILE_PART,
-        ChangeField.PATH,
-        ChangeField.OWNER,
-        ChangeField.REVIEWER,
-        ChangeField.COMMIT,
-        ChangeField.TR,
-        ChangeField.LABEL,
-        ChangeField.REVIEWED,
-        ChangeField.COMMIT_MESSAGE,
-        ChangeField.COMMENT,
-        ChangeField.CHANGE,
-        ChangeField.APPROVAL,
-        ChangeField.MERGEABLE);
-
-  static final Schema<ChangeData> V10 = release(
+  static final Schema<ChangeData> V11 = schema(
         ChangeField.LEGACY_ID,
         ChangeField.ID,
         ChangeField.STATUS,
@@ -215,49 +52,76 @@
         ChangeField.COMMENT,
         ChangeField.CHANGE,
         ChangeField.APPROVAL,
-        ChangeField.MERGEABLE);
-
-  static final Schema<ChangeData> V11 = release(
-        ChangeField.LEGACY_ID,
-        ChangeField.ID,
-        ChangeField.STATUS,
-        ChangeField.PROJECT,
-        ChangeField.PROJECTS,
-        ChangeField.REF,
-        ChangeField.TOPIC,
-        ChangeField.UPDATED,
-        ChangeField.FILE_PART,
-        ChangeField.PATH,
-        ChangeField.OWNER,
-        ChangeField.REVIEWER,
-        ChangeField.COMMIT,
-        ChangeField.TR,
-        ChangeField.LABEL,
-        ChangeField.REVIEWED,
-        ChangeField.COMMIT_MESSAGE,
-        ChangeField.COMMENT,
-        ChangeField.CHANGE,
-        ChangeField.APPROVAL,
-        ChangeField.MERGEABLE,
+        ChangeField.LEGACY_MERGEABLE,
         ChangeField.ADDED,
         ChangeField.DELETED,
         ChangeField.DELTA);
 
+  // For upgrade to Lucene 4.10.0 index format only.
+  static final Schema<ChangeData> V12 = schema(V11.getFields().values());
 
+  @SuppressWarnings("deprecation")
+  static final Schema<ChangeData> V13 = schema(
+      ChangeField.LEGACY_ID,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.TOPIC,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.REVIEWED,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
+      ChangeField.LEGACY_MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG);
 
-  private static Schema<ChangeData> release(Collection<FieldDef<ChangeData, ?>> fields) {
-    return new Schema<>(true, fields);
+  static final Schema<ChangeData> V14 = schema(
+      ChangeField.LEGACY_ID,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.TOPIC,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.REVIEWED,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
+      ChangeField.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG);
+
+  private static Schema<ChangeData> schema(Collection<FieldDef<ChangeData, ?>> fields) {
+    return new Schema<>(ImmutableList.copyOf(fields));
   }
 
   @SafeVarargs
-  private static Schema<ChangeData> release(FieldDef<ChangeData, ?>... fields) {
-    return release(Arrays.asList(fields));
-  }
-
-  @SafeVarargs
-  @SuppressWarnings("unused")
-  private static Schema<ChangeData> developer(FieldDef<ChangeData, ?>... fields) {
-    return new Schema<>(false, Arrays.asList(fields));
+  private static Schema<ChangeData> schema(FieldDef<ChangeData, ?>... fields) {
+    return schema(ImmutableList.copyOf(fields));
   }
 
   public static final ImmutableMap<Integer, Schema<ChangeData>> ALL;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndex.java
index ff554ff..09226bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndex.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndex.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.index;
 
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -22,7 +23,6 @@
 import java.io.IOException;
 
 public class DummyIndex implements ChangeIndex {
-
   @Override
   public Schema<ChangeData> getSchema() {
     throw new UnsupportedOperationException();
@@ -33,15 +33,11 @@
   }
 
   @Override
-  public void insert(ChangeData cd) throws IOException {
-  }
-
-  @Override
   public void replace(ChangeData cd) throws IOException {
   }
 
   @Override
-  public void delete(ChangeData cd) throws IOException {
+  public void delete(Change.Id id) throws IOException {
   }
 
   @Override
@@ -58,7 +54,7 @@
   public void markReady(boolean ready) throws IOException {
   }
 
-  @Override
-  public void delete(int id) throws IOException {
+  public int getMaxLimit() {
+    return Integer.MAX_VALUE;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndexModule.java
index dd7d2d8..d14a2f7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndexModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndexModule.java
@@ -21,6 +21,7 @@
   @Override
   protected void configure() {
     install(new IndexModule(1));
+    bind(IndexConfig.class).toInstance(IndexConfig.createDefault());
     bind(ChangeIndex.class).toInstance(new DummyIndex());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
index 872179d..557faeb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
@@ -32,7 +32,7 @@
  */
 public abstract class FieldDef<I, T> {
   /** Definition of a single (non-repeatable) field. */
-  public static abstract class Single<I, T> extends FieldDef<I, T> {
+  public abstract static class Single<I, T> extends FieldDef<I, T> {
     Single(String name, FieldType<T> type, boolean stored) {
       super(name, type, stored);
     }
@@ -44,7 +44,7 @@
   }
 
   /** Definition of a repeatable field. */
-  public static abstract class Repeatable<I, T>
+  public abstract static class Repeatable<I, T>
       extends FieldDef<I, Iterable<T>> {
     Repeatable(String name, FieldType<T> type, boolean stored) {
       super(name, type, stored);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java
index 4c40769..dce8a20 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java
@@ -21,35 +21,35 @@
 public class FieldType<T> {
   /** A single integer-valued field. */
   public static final FieldType<Integer> INTEGER =
-      new FieldType<Integer>("INTEGER");
+      new FieldType<>("INTEGER");
 
   /** A single-integer-valued field matched using range queries. */
   public static final FieldType<Integer> INTEGER_RANGE =
-      new FieldType<Integer>("INTEGER_RANGE");
+      new FieldType<>("INTEGER_RANGE");
 
   /** A single integer-valued field. */
   public static final FieldType<Long> LONG =
-      new FieldType<Long>("LONG");
+      new FieldType<>("LONG");
 
   /** A single date/time-valued field. */
   public static final FieldType<Timestamp> TIMESTAMP =
-      new FieldType<Timestamp>("TIMESTAMP");
+      new FieldType<>("TIMESTAMP");
 
   /** A string field searched using exact-match semantics. */
   public static final FieldType<String> EXACT =
-      new FieldType<String>("EXACT");
+      new FieldType<>("EXACT");
 
   /** A string field searched using prefix. */
   public static final FieldType<String> PREFIX =
-      new FieldType<String>("PREFIX");
+      new FieldType<>("PREFIX");
 
   /** A string field searched using fuzzy-match semantics. */
   public static final FieldType<String> FULL_TEXT =
-      new FieldType<String>("FULL_TEXT");
+      new FieldType<>("FULL_TEXT");
 
   /** A field that is only stored as raw bytes and cannot be queried. */
   public static final FieldType<byte[]> STORED_ONLY =
-      new FieldType<byte[]>("STORED_ONLY");
+      new FieldType<>("STORED_ONLY");
 
   private final String name;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexConfig.java
new file mode 100644
index 0000000..1857e55
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexConfig.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * Implementation-specific configuration for secondary indexes.
+ * <p>
+ * Contains configuration that is tied to a specific index implementation but is
+ * otherwise global, i.e. not tied to a specific {@link ChangeIndex} and schema
+ * version.
+ */
+@AutoValue
+public abstract class IndexConfig {
+  public static IndexConfig createDefault() {
+    return create(Integer.MAX_VALUE);
+  }
+
+  public static IndexConfig create(int maxLimit) {
+    checkArgument(maxLimit > 0, "maxLimit must be positive: %s", maxLimit);
+    return new AutoValue_IndexConfig(maxLimit);
+  }
+
+  public abstract int maxLimit();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexExecutor.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexExecutor.java
index 0a96d1d..eb97fdc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexExecutor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexExecutor.java
@@ -17,6 +17,7 @@
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
 import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.server.git.QueueProvider.QueueType;
 import com.google.inject.BindingAnnotation;
 
 import java.lang.annotation.Retention;
@@ -28,4 +29,5 @@
 @Retention(RUNTIME)
 @BindingAnnotation
 public @interface IndexExecutor {
+  QueueType value();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
index 67d0fef..41df287 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.index;
 
+import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
+import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
+
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.gerrit.lifecycle.LifecycleModule;
@@ -21,7 +24,6 @@
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.query.change.BasicChangeRewrites;
 import com.google.gerrit.server.query.change.ChangeQueryRewriter;
-import com.google.inject.AbstractModule;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Provides;
@@ -48,16 +50,20 @@
   }
 
   private final int threads;
-  private final ListeningExecutorService indexExecutor;
+  private final ListeningExecutorService interactiveExecutor;
+  private final ListeningExecutorService batchExecutor;
 
   public IndexModule(int threads) {
     this.threads = threads;
-    this.indexExecutor = null;
+    this.interactiveExecutor = null;
+    this.batchExecutor = null;
   }
 
-  public IndexModule(ListeningExecutorService indexExecutor) {
+  public IndexModule(ListeningExecutorService interactiveExecutor,
+      ListeningExecutorService batchExecutor) {
     this.threads = -1;
-    this.indexExecutor = indexExecutor;
+    this.interactiveExecutor = interactiveExecutor;
+    this.batchExecutor = batchExecutor;
   }
 
   @Override
@@ -67,49 +73,61 @@
     bind(IndexCollection.class);
     listener().to(IndexCollection.class);
     factory(ChangeIndexer.Factory.class);
-
-    if (indexExecutor != null) {
-      bind(ListeningExecutorService.class)
-          .annotatedWith(IndexExecutor.class)
-          .toInstance(indexExecutor);
-    } else {
-      install(new IndexExecutorModule(threads));
-    }
   }
 
   @Provides
+  @Singleton
   ChangeIndexer getChangeIndexer(
+      @IndexExecutor(INTERACTIVE) ListeningExecutorService executor,
       ChangeIndexer.Factory factory,
       IndexCollection indexes) {
-    return factory.create(indexes);
+    // Bind default indexer to interactive executor; callers who need a
+    // different executor can use the factory directly.
+    return factory.create(executor, indexes);
   }
 
-  private static class IndexExecutorModule extends AbstractModule {
-    private final int threads;
-
-    private IndexExecutorModule(int threads) {
-      this.threads = threads;
+  @Provides
+  @Singleton
+  @IndexExecutor(INTERACTIVE)
+  ListeningExecutorService getInteractiveIndexExecutor(
+      @GerritServerConfig Config config,
+      WorkQueue workQueue) {
+    if (interactiveExecutor != null) {
+      return interactiveExecutor;
     }
-
-    @Override
-    public void configure() {
+    int threads = this.threads;
+    if (threads <= 0) {
+      threads = config.getInt("index", null, "threads", 0);
     }
-
-    @Provides
-    @Singleton
-    @IndexExecutor
-    ListeningExecutorService getIndexExecutor(
-        @GerritServerConfig Config config,
-        WorkQueue workQueue) {
-      int threads = this.threads;
-      if (threads <= 0) {
-        threads = config.getInt("index", null, "threads", 0);
-      }
-      if (threads <= 0) {
-        return MoreExecutors.sameThreadExecutor();
-      }
-      return MoreExecutors.listeningDecorator(
-          workQueue.createQueue(threads, "index"));
+    if (threads <= 0) {
+      threads =
+          config.getInt("changeMerge", null, "interactiveThreadPoolSize", 0);
     }
+    if (threads <= 0) {
+      return MoreExecutors.newDirectExecutorService();
+    }
+    return MoreExecutors.listeningDecorator(
+        workQueue.createQueue(threads, "Index-Interactive"));
+  }
+
+  @Provides
+  @Singleton
+  @IndexExecutor(BATCH)
+  ListeningExecutorService getBatchIndexExecutor(
+      @IndexExecutor(INTERACTIVE) ListeningExecutorService interactive,
+      @GerritServerConfig Config config,
+      WorkQueue workQueue) {
+    if (batchExecutor != null) {
+      return batchExecutor;
+    }
+    int threads = config.getInt("index", null, "batchThreads", 0);
+    if (threads <= 0) {
+      threads = config.getInt("changeMerge", null, "threadPoolSize", 0);
+    }
+    if (threads <= 0) {
+      return interactive;
+    }
+    return MoreExecutors.listeningDecorator(
+        workQueue.createQueue(threads, "Index-Batch"));
   }
 }
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 e451f60..7fbddfb 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
@@ -14,9 +14,8 @@
 
 package com.google.gerrit.server.index;
 
-import static com.google.gerrit.common.data.GlobalCapability.DEFAULT_MAX_QUERY_LIMIT;
+import static com.google.common.base.Preconditions.checkArgument;
 
-import com.google.common.base.Objects;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Change;
@@ -29,9 +28,9 @@
 import com.google.gerrit.server.query.change.AndSource;
 import com.google.gerrit.server.query.change.BasicChangeRewrites;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeQueryRewriter;
 import com.google.gerrit.server.query.change.ChangeStatusPredicate;
+import com.google.gerrit.server.query.change.LimitPredicate;
 import com.google.gerrit.server.query.change.OrSource;
 import com.google.inject.Inject;
 
@@ -129,16 +128,14 @@
   }
 
   @Override
-  public Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int start)
-      throws QueryParseException {
+  public Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int start,
+      int limit) throws QueryParseException {
+    checkArgument(limit > 0, "limit must be positive: %s", limit);
     ChangeIndex index = indexes.getSearchIndex();
     in = basicRewrites.rewrite(in);
-    int limit = Objects.firstNonNull(
-        ChangeQueryBuilder.getLimit(in), DEFAULT_MAX_QUERY_LIMIT);
     // Increase the limit rather than skipping, since we don't know how many
     // skipped results would have been filtered out by the enclosing AndSource.
     limit += start;
-    limit = Math.max(limit, 1);
 
     Predicate<ChangeData> out = rewriteImpl(in, index, limit);
     if (in == out || out instanceof IndexPredicate) {
@@ -168,6 +165,9 @@
       ChangeIndex index, int limit) throws QueryParseException {
     if (isIndexPredicate(in, index)) {
       return in;
+    } else if (in instanceof LimitPredicate) {
+      // Replace any limits with the limit provided by the caller.
+      return new LimitPredicate(limit);
     } else if (!isRewritePossible(in)) {
       return null; // magic to indicate "in" cannot be rewritten
     }
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 09d66e5..34d37d8 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,18 +14,15 @@
 
 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.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
 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;
 
@@ -44,40 +41,6 @@
 public class IndexedChangeQuery extends Predicate<ChangeData>
     implements ChangeDataSource, Paginated {
 
-  /**
-   * 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 ChangeIndex index;
   private final int limit;
 
@@ -163,27 +126,13 @@
   }
 
   @Override
-  public ResultSet<ChangeData> restart(ChangeData last) throws OrmException {
-    pred = replaceSortKeyPredicates(pred, last.change().getSortKey());
-    try {
-      source = index.getSource(pred, 0, limit);
-    } catch (QueryParseException e) {
-      // Don't need to show this exception to the user; the only thing that
-      // changed about pred was its SortKeyPredicates, and any other QPEs
-      // that might happen should have already thrown from the constructor.
-      throw new OrmException(e);
-    }
-    return read();
-  }
-
-  @Override
   public ResultSet<ChangeData> restart(int start) throws OrmException {
     try {
       source = index.getSource(pred, start, limit);
     } catch (QueryParseException e) {
       // Don't need to show this exception to the user; the only thing that
-      // changed about pred was its SortKeyPredicates, and any other QPEs
-      // that might happen should have already thrown from the constructor.
+      // changed about pred was its start, and any other QPEs that might happen
+      // should have already thrown from the constructor.
       throw new OrmException(e);
     }
     return read();
@@ -225,7 +174,7 @@
 
   @Override
   public String toString() {
-    return Objects.toStringHelper("index")
+    return MoreObjects.toStringHelper("index")
         .add("p", pred)
         .add("limit", limit)
         .toString();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java
new file mode 100644
index 0000000..982f897
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java
@@ -0,0 +1,142 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import static com.google.gerrit.server.query.change.ChangeData.asChanges;
+
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.QueueProvider.QueueType;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+public class ReindexAfterUpdate implements GitReferenceUpdatedListener {
+  private static final Logger log = LoggerFactory
+      .getLogger(ReindexAfterUpdate.class);
+
+  private final OneOffRequestContext requestContext;
+  private final Provider<InternalChangeQuery> queryProvider;
+  private final ChangeIndexer.Factory indexerFactory;
+  private final IndexCollection indexes;
+  private final ListeningExecutorService executor;
+
+  @Inject
+  ReindexAfterUpdate(
+      OneOffRequestContext requestContext,
+      Provider<InternalChangeQuery> queryProvider,
+      ChangeIndexer.Factory indexerFactory,
+      IndexCollection indexes,
+      @IndexExecutor(QueueType.BATCH) ListeningExecutorService executor) {
+    this.requestContext = requestContext;
+    this.queryProvider = queryProvider;
+    this.indexerFactory = indexerFactory;
+    this.indexes = indexes;
+    this.executor = executor;
+  }
+
+  @Override
+  public void onGitReferenceUpdated(final Event event) {
+    Futures.transform(
+        executor.submit(new GetChanges(event)),
+        new AsyncFunction<List<Change>, List<Void>>() {
+          @Override
+          public ListenableFuture<List<Void>> apply(List<Change> changes) {
+            List<ListenableFuture<Void>> result =
+                Lists.newArrayListWithCapacity(changes.size());
+            for (Change c : changes) {
+              result.add(executor.submit(new Index(event, c.getId())));
+            }
+            return Futures.allAsList(result);
+          }
+        });
+  }
+
+  private abstract class Task<V> implements Callable<V> {
+    protected Event event;
+
+    protected Task(Event event) {
+      this.event = event;
+    }
+
+    @Override
+    public final V call() throws Exception {
+      try (ManualRequestContext ctx = requestContext.open()) {
+        return impl(ctx);
+      } catch (Exception e) {
+        log.error("Failed to reindex changes after " + event, e);
+        throw e;
+      }
+    }
+
+    protected abstract V impl(RequestContext ctx) throws Exception;
+  }
+
+  private class GetChanges extends Task<List<Change>> {
+    private GetChanges(Event event) {
+      super(event);
+    }
+
+    @Override
+    protected List<Change> impl(RequestContext ctx) throws OrmException {
+      String ref = event.getRefName();
+      Project.NameKey project = new Project.NameKey(event.getProjectName());
+      if (ref.equals(RefNames.REFS_CONFIG)) {
+        return asChanges(queryProvider.get().byProjectOpen(project));
+      } else {
+        return asChanges(queryProvider.get().byBranchOpen(
+            new Branch.NameKey(project, ref)));
+      }
+    }
+  }
+
+  private class Index extends Task<Void> {
+    private final Change.Id id;
+
+    Index(Event event, Change.Id id) {
+      super(event);
+      this.id = id;
+    }
+
+    @Override
+    protected Void impl(RequestContext ctx) throws OrmException, IOException {
+      // Reload change, as some time may have passed since GetChanges.
+      ReviewDb db = ctx.getReviewDbProvider().get();
+      Change c = db.changes().get(id);
+      indexerFactory.create(executor, indexes).index(db, c);
+      return null;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
index 0de1379..c0eb276 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
@@ -16,7 +16,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Predicates;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableMap;
@@ -50,19 +50,16 @@
     }
   }
 
-  private final boolean release;
   private final ImmutableMap<String, FieldDef<T, ?>> fields;
   private int version;
 
-  protected Schema(boolean release, Iterable<FieldDef<T, ?>> fields) {
-    this(0, release, fields);
+  protected Schema(Iterable<FieldDef<T, ?>> fields) {
+    this(0, fields);
   }
 
   @VisibleForTesting
-  public Schema(int version, boolean release,
-      Iterable<FieldDef<T, ?>> fields) {
+  public Schema(int version, Iterable<FieldDef<T, ?>> fields) {
     this.version = version;
-    this.release = release;
     ImmutableMap.Builder<String, FieldDef<T, ?>> b = ImmutableMap.builder();
     for (FieldDef<T, ?> f : fields) {
       b.put(f.getName(), f);
@@ -70,10 +67,6 @@
     this.fields = b.build();
   }
 
-  public final boolean isRelease() {
-    return release;
-  }
-
   public final int getVersion() {
     return version;
   }
@@ -123,7 +116,7 @@
 
   @Override
   public String toString() {
-    return Objects.toStringHelper(this)
+    return MoreObjects.toStringHelper(this)
         .addValue(fields.keySet())
         .toString();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
similarity index 81%
rename from gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
index fac1c55..c0522d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.index;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
 import static org.eclipse.jgit.lib.RefDatabase.ALL;
 
 import com.google.common.base.Stopwatch;
@@ -27,12 +29,11 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.MergeabilityChecker;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.ChangeCache;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.MultiProgressMonitor;
@@ -65,6 +66,7 @@
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.PrintWriter;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
@@ -75,9 +77,9 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 
-public class ChangeBatchIndexer {
+public class SiteIndexer {
   private static final Logger log =
-      LoggerFactory.getLogger(ChangeBatchIndexer.class);
+      LoggerFactory.getLogger(SiteIndexer.class);
 
   public static class Result {
     private final long elapsedNanos;
@@ -110,44 +112,59 @@
   }
 
   private final SchemaFactory<ReviewDb> schemaFactory;
+  private final ChangeCache changeCache;
   private final ChangeData.Factory changeDataFactory;
   private final GitRepositoryManager repoManager;
   private final ListeningExecutorService executor;
   private final ChangeIndexer.Factory indexerFactory;
-  private final MergeabilityChecker mergeabilityChecker;
   private final ThreeWayMergeStrategy mergeStrategy;
 
+  private int numChanges = -1;
+  private OutputStream progressOut = NullOutputStream.INSTANCE;
+  private PrintWriter verboseWriter =
+      new PrintWriter(NullOutputStream.INSTANCE);
+
   @Inject
-  ChangeBatchIndexer(SchemaFactory<ReviewDb> schemaFactory,
+  SiteIndexer(SchemaFactory<ReviewDb> schemaFactory,
+      ChangeCache changeCache,
       ChangeData.Factory changeDataFactory,
       GitRepositoryManager repoManager,
-      @IndexExecutor ListeningExecutorService executor,
+      @IndexExecutor(BATCH) ListeningExecutorService executor,
       ChangeIndexer.Factory indexerFactory,
-      @GerritServerConfig Config config,
-      @Nullable MergeabilityChecker mergeabilityChecker) {
+      @GerritServerConfig Config config) {
     this.schemaFactory = schemaFactory;
+    this.changeCache = changeCache;
     this.changeDataFactory = changeDataFactory;
     this.repoManager = repoManager;
     this.executor = executor;
     this.indexerFactory = indexerFactory;
-    this.mergeabilityChecker = mergeabilityChecker;
     this.mergeStrategy = MergeUtil.getMergeStrategy(config);
   }
 
-  public Result indexAll(ChangeIndex index, Iterable<Project.NameKey> projects,
-      int numProjects, int numChanges, OutputStream progressOut,
-      OutputStream verboseOut) {
-    if (progressOut == null) {
-      progressOut = NullOutputStream.INSTANCE;
-    }
-    PrintWriter verboseWriter = verboseOut != null ? new PrintWriter(verboseOut)
-        : null;
+  public SiteIndexer setNumChanges(int num) {
+    numChanges = num;
+    return this;
+  }
 
+  public SiteIndexer setProgressOut(OutputStream out) {
+    progressOut = checkNotNull(out);
+    return this;
+  }
+
+  public SiteIndexer setVerboseOut(OutputStream out) {
+    verboseWriter = new PrintWriter(checkNotNull(out));
+    return this;
+  }
+
+  public Result indexAll(ChangeIndex index,
+      Iterable<Project.NameKey> projects) {
     Stopwatch sw = Stopwatch.createStarted();
     final MultiProgressMonitor mpm =
         new MultiProgressMonitor(progressOut, "Reindexing changes");
     final Task projTask = mpm.beginSubTask("projects",
-        numProjects >= 0 ? numProjects : MultiProgressMonitor.UNKNOWN);
+        (projects instanceof Collection)
+          ? ((Collection<?>) projects).size()
+          : MultiProgressMonitor.UNKNOWN);
     final Task doneTask = mpm.beginSubTask(null,
         numChanges >= 0 ? numChanges : MultiProgressMonitor.UNKNOWN);
     final Task failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
@@ -156,9 +173,8 @@
     final AtomicBoolean ok = new AtomicBoolean(true);
 
     for (final Project.NameKey project : projects) {
-      updateMergeable(project);
       final ListenableFuture<?> future = executor.submit(reindexProject(
-          indexerFactory.create(index), project, doneTask, failedTask,
+          indexerFactory.create(executor, index), project, doneTask, failedTask,
           verboseWriter));
       futures.add(future);
       future.addListener(new Runnable() {
@@ -166,13 +182,13 @@
         public void run() {
           try {
             future.get();
-          } catch (InterruptedException e) {
-            fail(project, e);
-          } catch (ExecutionException e) {
+          } catch (ExecutionException | InterruptedException e) {
             fail(project, e);
           } catch (RuntimeException e) {
             failAndThrow(project, e);
           } catch (Error e) {
+            // Can't join with RuntimeException because "RuntimeException |
+            // Error" becomes Throwable, which messes with signatures.
             failAndThrow(project, e);
           } finally {
             projTask.update(1);
@@ -193,7 +209,7 @@
           fail(project, e);
           throw e;
         }
-      }, MoreExecutors.sameThreadExecutor());
+      }, MoreExecutors.directExecutor());
     }
 
     try {
@@ -212,18 +228,6 @@
     return new Result(sw, ok.get(), doneTask.getCount(), failedTask.getCount());
   }
 
-  private boolean updateMergeable(Project.NameKey project) {
-    if (mergeabilityChecker != null) {
-      try {
-        mergeabilityChecker.newCheck().addProject(project).run();
-      } catch (Exception e) {
-        log.error("Error in mergeability checker", e);
-        return false;
-      }
-    }
-    return true;
-  }
-
   private Callable<Void> reindexProject(final ChangeIndexer indexer,
       final Project.NameKey project, final Task done, final Task failed,
       final PrintWriter verboseWriter) {
@@ -237,7 +241,7 @@
           repo = repoManager.openRepository(project);
           Map<String, Ref> refs = repo.getRefDatabase().getRefs(ALL);
           db = schemaFactory.open();
-          for (Change c : db.changes().byProject(project)) {
+          for (Change c : changeCache.get(project)) {
             Ref r = refs.get(c.currentPatchSetId().toRefName());
             if (r != null) {
               byId.put(r.getObjectId(), changeDataFactory.create(db, c));
@@ -268,7 +272,7 @@
     };
   }
 
-  public static class ProjectIndexer implements Callable<Void> {
+  private static class ProjectIndexer implements Callable<Void> {
     private final ChangeIndexer indexer;
     private final ThreeWayMergeStrategy mergeStrategy;
     private final Multimap<ObjectId, ChangeData> byId;
@@ -319,41 +323,36 @@
           getPathsAndIndex(id);
         }
       } finally {
-        walk.release();
+        walk.close();
       }
       return null;
     }
 
     private void getPathsAndIndex(ObjectId b) throws Exception {
       List<ChangeData> cds = Lists.newArrayList(byId.get(b));
-      try {
+      try (DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
         RevCommit bCommit = walk.parseCommit(b);
         RevTree bTree = bCommit.getTree();
         RevTree aTree = aFor(bCommit, walk);
-        DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE);
-        try {
-          df.setRepository(repo);
-          if (!cds.isEmpty()) {
-            List<String> paths = (aTree != null)
-                ? getPaths(df.scan(aTree, bTree))
-                : Collections.<String>emptyList();
-            Iterator<ChangeData> cdit = cds.iterator();
-            for (ChangeData cd ; cdit.hasNext(); cdit.remove()) {
-              cd = cdit.next();
-              try {
-                cd.setCurrentFilePaths(paths);
-                indexer.index(cd);
-                done.update(1);
-                if (verboseWriter != null) {
-                  verboseWriter.println("Reindexed change " + cd.getId());
-                }
-              } catch (Exception e) {
-                fail("Failed to index change " + cd.getId(), true, e);
+        df.setRepository(repo);
+        if (!cds.isEmpty()) {
+          List<String> paths = (aTree != null)
+              ? getPaths(df.scan(aTree, bTree))
+              : Collections.<String>emptyList();
+          Iterator<ChangeData> cdit = cds.iterator();
+          for (ChangeData cd ; cdit.hasNext(); cdit.remove()) {
+            cd = cdit.next();
+            try {
+              cd.setCurrentFilePaths(paths);
+              indexer.index(cd);
+              done.update(1);
+              if (verboseWriter != null) {
+                verboseWriter.println("Reindexed change " + cd.getId());
               }
+            } catch (Exception e) {
+              fail("Failed to index change " + cd.getId(), true, e);
             }
           }
-        } finally {
-          df.release();
         }
       } catch (Exception e) {
         fail("Failed to index commit " + b.name(), false, e);
@@ -392,13 +391,10 @@
     }
 
     private ObjectId emptyTree() throws IOException {
-      ObjectInserter oi = repo.newObjectInserter();
-      try {
+      try (ObjectInserter oi = repo.newObjectInserter()) {
         ObjectId id = oi.insert(Constants.OBJ_TREE, new byte[] {});
         oi.flush();
         return id;
-      } finally {
-        oi.release();
       }
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java
index f611052..8ba7df9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java
@@ -14,12 +14,7 @@
 
 package com.google.gerrit.server.index;
 
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.gerrit.server.index.ChangeField.UPDATED;
-
 import com.google.gerrit.server.query.QueryParseException;
-import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtjsonrpc.common.JavaSqlTimestampHelper;
 
 import java.sql.Timestamp;
@@ -27,22 +22,6 @@
 
 // TODO: Migrate this to IntegerRangePredicate
 public abstract class TimestampRangePredicate<I> extends IndexPredicate<I> {
-  @SuppressWarnings({"deprecation", "unchecked"})
-  protected static FieldDef<ChangeData, Timestamp> updatedField(
-      Schema<ChangeData> schema) {
-    if (schema == null) {
-      return ChangeField.LEGACY_UPDATED;
-    }
-    FieldDef<ChangeData, ?> f = schema.getFields().get(UPDATED.getName());
-    if (f == null) {
-      f = schema.getFields().get(ChangeField.LEGACY_UPDATED.getName());
-      checkNotNull(f, "schema missing updated field, found: %s", schema);
-    }
-    checkArgument(f.getType() == FieldType.TIMESTAMP,
-        "expected %s to be TIMESTAMP, found %s", f.getName(), f.getType());
-    return (FieldDef<ChangeData, Timestamp>) f;
-  }
-
   protected static Timestamp parse(String value) throws QueryParseException {
     try {
       return JavaSqlTimestampHelper.parseTimestamp(value);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
index 8387f51..adcf242 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
@@ -24,6 +24,7 @@
 public class AbandonedSender extends ReplyToChangeSender {
   public static interface Factory extends
       ReplyToChangeSender.Factory<AbandonedSender> {
+    @Override
     AbandonedSender create(Change change);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
index d38c5a3..ac23455 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
@@ -72,6 +72,7 @@
     emailOnlyAuthors = false;
   }
 
+  @Override
   public void setFrom(final Account.Id id) {
     super.setFrom(id);
 
@@ -94,6 +95,7 @@
   }
 
   /** Format the message body by calling {@link #appendText(String)}. */
+  @Override
   protected void format() throws EmailException {
     formatChange();
     appendText(velocifyFile("ChangeFooter.vm"));
@@ -114,11 +116,16 @@
   /** Format the message body by calling {@link #appendText(String)}. */
   protected abstract void formatChange() throws EmailException;
 
-  /** Format the message footer by calling {@link #appendText(String)}. */
+  /**
+   * Format the message footer by calling {@link #appendText(String)}.
+   *
+   * @throws EmailException if an error occurred.
+   */
   protected void formatFooter() throws EmailException {
   }
 
   /** Setup the message headers and envelope (TO, CC, BCC). */
+  @Override
   protected void init() throws EmailException {
     if (args.projectCache != null) {
       projectState = args.projectCache.get(change.getProject());
@@ -324,12 +331,14 @@
     }
   }
 
+  @Override
   protected void add(final RecipientType rt, final Account.Id to) {
     if (! emailOnlyAuthors || authors.contains(to)) {
       super.add(rt, to);
     }
   }
 
+  @Override
   protected boolean isVisibleTo(final Account.Id to) throws OrmException {
     return projectState == null
         || projectState.controlFor(args.identifiedUserFactory.create(to))
@@ -387,28 +396,28 @@
 
     TemporaryBuffer.Heap buf =
         new TemporaryBuffer.Heap(args.settings.maximumDiffSize);
-    DiffFormatter fmt = new DiffFormatter(buf);
-    Repository git;
-    try {
-      git = args.server.openRepository(change.getProject());
-    } catch (IOException e) {
-      log.error("Cannot open repository to format patch", e);
-      return "";
-    }
-    try {
-      fmt.setRepository(git);
-      fmt.setDetectRenames(true);
-      fmt.format(patchList.getOldId(), patchList.getNewId());
-      return RawParseUtils.decode(buf.toByteArray());
-    } catch (IOException e) {
-      if (JGitText.get().inMemoryBufferLimitExceeded.equals(e.getMessage())) {
+    try (DiffFormatter fmt = new DiffFormatter(buf)) {
+      Repository git;
+      try {
+        git = args.server.openRepository(change.getProject());
+      } catch (IOException e) {
+        log.error("Cannot open repository to format patch", e);
         return "";
       }
-      log.error("Cannot format patch", e);
-      return "";
-    } finally {
-      fmt.release();
-      git.close();
+      try {
+        fmt.setRepository(git);
+        fmt.setDetectRenames(true);
+        fmt.format(patchList.getOldId(), patchList.getNewId());
+        return RawParseUtils.decode(buf.toByteArray());
+      } catch (IOException e) {
+        if (JGitText.get().inMemoryBufferLimitExceeded.equals(e.getMessage())) {
+          return "";
+        }
+        log.error("Cannot format patch", e);
+        return "";
+      } finally {
+        git.close();
+      }
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index 2beb49f..b587791 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.common.errors.EmailException;
@@ -24,6 +25,7 @@
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.patch.PatchFile;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
@@ -53,13 +55,16 @@
 
   private final NotifyHandling notify;
   private List<PatchLineComment> inlineComments = Collections.emptyList();
+  private final PatchLineCommentsUtil plcUtil;
 
   @Inject
   public CommentSender(EmailArguments ea,
       @Assisted NotifyHandling notify,
-      @Assisted Change c) {
+      @Assisted Change c,
+      PatchLineCommentsUtil plcUtil) {
     super(ea, c, "comment");
     this.notify = notify;
+    this.plcUtil = plcUtil;
   }
 
   public void setPatchLineComments(final List<PatchLineComment> plc)
@@ -116,7 +121,7 @@
         try {
           patchList = getPatchList();
         } catch (PatchListNotAvailableException e) {
-          patchList = null;
+          log.error("Failed to get patch list", e);
         }
       }
 
@@ -232,17 +237,19 @@
 
   private void appendQuotedParent(StringBuilder out, PatchLineComment child) {
     if (child.getParentUuid() != null) {
-      PatchLineComment parent;
+      Optional<PatchLineComment> parent;
+      PatchLineComment.Key key = new PatchLineComment.Key(
+          child.getKey().getParentKey(),
+          child.getParentUuid());
       try {
-        parent = args.db.get().patchComments().get(
-            new PatchLineComment.Key(
-                child.getKey().getParentKey(),
-                child.getParentUuid()));
+        parent = plcUtil.get(args.db.get(), changeData.notes(), key);
       } catch (OrmException e) {
-        parent = null;
+        log.warn("Could not find the parent of this comment: "
+            + child.toString());
+        parent = Optional.absent();
       }
-      if (parent != null) {
-        String msg = parent.getMessage().trim();
+      if (parent.isPresent()) {
+        String msg = parent.get().getMessage().trim();
         if (msg.length() > 75) {
           msg = msg.substring(0, 75);
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
index ebf5ac7..c1ab58a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.ApprovalsUtil;
@@ -35,6 +36,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.ssh.SshAdvertisedAddresses;
+import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -62,11 +64,12 @@
   final AllProjectsName allProjectsName;
   final List<String> sshAddresses;
 
-  final ChangeQueryBuilder.Factory queryBuilder;
+  final ChangeQueryBuilder queryBuilder;
   final Provider<ReviewDb> db;
   final ChangeData.Factory changeDataFactory;
   final RuntimeInstance velocityRuntime;
   final EmailSettings settings;
+  final DynamicSet<OutgoingEmailValidationListener> outgoingEmailValidationListeners;
 
   @Inject
   EmailArguments(GitRepositoryManager server, ProjectCache projectCache,
@@ -83,12 +86,13 @@
       @AnonymousCowardName String anonymousCowardName,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
       AllProjectsName allProjectsName,
-      ChangeQueryBuilder.Factory queryBuilder,
+      ChangeQueryBuilder queryBuilder,
       Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       RuntimeInstance velocityRuntime,
       EmailSettings settings,
-      @SshAdvertisedAddresses List<String> sshAddresses) {
+      @SshAdvertisedAddresses List<String> sshAddresses,
+      DynamicSet<OutgoingEmailValidationListener> outgoingEmailValidationListeners) {
     this.server = server;
     this.projectCache = projectCache;
     this.groupBackend = groupBackend;
@@ -112,5 +116,6 @@
     this.velocityRuntime = velocityRuntime;
     this.settings = settings;
     this.sshAddresses = sshAddresses;
+    this.outgoingEmailValidationListeners = outgoingEmailValidationListeners;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
index 26cccb8..c6e59eb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import com.google.common.collect.Multimap;
+import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.errors.NoSuchAccountException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -31,8 +32,6 @@
 import java.util.Set;
 
 public class MailUtil {
-  private static final FooterKey REVIEWED_BY = new FooterKey("Reviewed-by");
-  private static final FooterKey TESTED_BY = new FooterKey("Tested-by");
 
   public static MailRecipients getRecipientsFromFooters(
       final AccountResolver accountResolver, final PatchSet ps,
@@ -77,8 +76,8 @@
   private static boolean isReviewer(final FooterLine candidateFooterLine) {
     return candidateFooterLine.matches(FooterKey.SIGNED_OFF_BY)
         || candidateFooterLine.matches(FooterKey.ACKED_BY)
-        || candidateFooterLine.matches(REVIEWED_BY)
-        || candidateFooterLine.matches(TESTED_BY);
+        || candidateFooterLine.matches(FooterConstants.REVIEWED_BY)
+        || candidateFooterLine.matches(FooterConstants.TESTED_BY);
   }
 
   public static class MailRecipients {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index 49acda8..8a3133f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -20,6 +20,8 @@
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.mail.EmailHeader.AddressList;
+import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
+import com.google.gerrit.server.validators.ValidationException;
 import com.google.gwtorm.server.OrmException;
 
 import org.apache.commons.lang.StringUtils;
@@ -121,14 +123,32 @@
         }
       }
 
-      args.emailSender.send(smtpFromAddress, smtpRcptTo, headers, body.toString());
+      OutgoingEmailValidationListener.Args va = new OutgoingEmailValidationListener.Args();
+      va.messageClass = messageClass;
+      va.smtpFromAddress = smtpFromAddress;
+      va.smtpRcptTo = smtpRcptTo;
+      va.headers = headers;
+      va.body = body.toString();
+      for (OutgoingEmailValidationListener validator : args.outgoingEmailValidationListeners) {
+        try {
+          validator.validateOutgoingEmail(va);
+        } catch (ValidationException e) {
+          return;
+        }
+      }
+
+      args.emailSender.send(va.smtpFromAddress, va.smtpRcptTo, va.headers, va.body);
     }
   }
 
   /** Format the message body by calling {@link #appendText(String)}. */
   protected abstract void format() throws EmailException;
 
-  /** Setup the message headers and envelope (TO, CC, BCC). */
+  /**
+   * Setup the message headers and envelope (TO, CC, BCC).
+   *
+   * @throws EmailException if an error occurred.
+   */
   protected void init() throws EmailException {
     setupVelocityContext();
 
@@ -315,6 +335,11 @@
     }
   }
 
+  /**
+   * @param to account.
+   * @throws OrmException
+   * @return whether this email is visible to the given account.
+   */
   protected boolean isVisibleTo(final Account.Id to) throws OrmException {
     return true;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java
index 76b896a..53efa1e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java
@@ -16,7 +16,6 @@
 
 import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
 
-import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -29,12 +28,10 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.MailUtil.MailRecipients;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
-import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -65,14 +62,12 @@
 
   @Inject
   public PatchSetNotificationSender(Provider<ReviewDb> db,
-      ChangeHooks hooks,
       GitRepositoryManager repoManager,
       PatchSetInfoFactory patchSetInfoFactory,
       ApprovalsUtil approvalsUtil,
       AccountResolver accountResolver,
       CreateChangeSender.Factory createChangeSenderFactory,
-      ReplacePatchSetSender.Factory replacePatchSetFactory,
-      ChangeIndexer indexer) {
+      ReplacePatchSetSender.Factory replacePatchSetFactory) {
     this.db = db;
     this.repoManager = repoManager;
     this.patchSetInfoFactory = patchSetInfoFactory;
@@ -86,16 +81,12 @@
       final boolean newChange, final IdentifiedUser currentUser,
       final Change updatedChange, final PatchSet updatedPatchSet,
       final LabelTypes labelTypes)
-      throws OrmException, IOException, PatchSetInfoNotAvailableException {
-    final Repository git = repoManager.openRepository(updatedChange.getProject());
-    try {
-      final RevWalk revWalk = new RevWalk(git);
+      throws OrmException, IOException {
+    try (Repository git = repoManager.openRepository(updatedChange.getProject())) {
       final RevCommit commit;
-      try {
+      try (RevWalk revWalk = new RevWalk(git)) {
         commit = revWalk.parseCommit(ObjectId.fromString(
             updatedPatchSet.getRevision().get()));
-      } finally {
-        revWalk.release();
       }
       final PatchSetInfo info = patchSetInfoFactory.get(commit, updatedPatchSet.getId());
       final List<FooterLine> footerLines = commit.getFooterLines();
@@ -139,8 +130,6 @@
           log.error("Cannot send email for new patch set " + updatedPatchSet.getId(), e);
         }
       }
-    } finally {
-      git.close();
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
index 872b84a..63709c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
@@ -68,16 +68,16 @@
 
     for (AccountProjectWatch w : args.db.get().accountProjectWatches()
         .byProject(project)) {
-      if (w.isNotify(type)) {
+      if (add(matching, w, type)) {
+        // We only want to prevent matching All-Projects if this filter hits
         projectWatchers.add(w.getAccountId());
-        add(matching, w);
       }
     }
 
     for (AccountProjectWatch w : args.db.get().accountProjectWatches()
         .byProject(args.allProjectsName)) {
-      if (!projectWatchers.contains(w.getAccountId()) && w.isNotify(type)) {
-        add(matching, w);
+      if (!projectWatchers.contains(w.getAccountId())) {
+        add(matching, w, type);
       }
     }
 
@@ -85,7 +85,7 @@
       for (NotifyConfig nc : state.getConfig().getNotifyConfigs()) {
         if (nc.isNotify(type)) {
           try {
-            add(matching, nc, state.getProject().getNameKey());
+            add(matching, nc);
           } catch (QueryParseException e) {
             log.warn(String.format(
                 "Project %s has invalid notify %s filter \"%s\"",
@@ -121,7 +121,7 @@
     }
   }
 
-  private void add(Watchers matching, NotifyConfig nc, Project.NameKey project)
+  private void add(Watchers matching, NotifyConfig nc)
       throws OrmException, QueryParseException {
     for (GroupReference ref : nc.getGroups()) {
       CurrentUser user = new SingleGroupUser(args.capabilityControlFactory,
@@ -174,18 +174,24 @@
     }
   }
 
-  private void add(Watchers matching, AccountProjectWatch w)
+  private boolean add(Watchers matching, AccountProjectWatch w, NotifyType type)
       throws OrmException {
     IdentifiedUser user =
         args.identifiedUserFactory.create(args.db, w.getAccountId());
 
     try {
       if (filterMatch(user, w.getFilter())) {
-        matching.bcc.accounts.add(w.getAccountId());
+        // If we are set to notify on this type, add the user.
+        // Otherwise, still return true to stop notifications for this user.
+        if (w.isNotify(type)) {
+          matching.bcc.accounts.add(w.getAccountId());
+        }
+        return true;
       }
     } catch (QueryParseException e) {
       // Ignore broken filter expressions.
     }
+    return false;
   }
 
   private boolean filterMatch(CurrentUser user, String filter)
@@ -194,9 +200,9 @@
     Predicate<ChangeData> p = null;
 
     if (user == null) {
-      qb = args.queryBuilder.create(args.anonymousUser);
+      qb = args.queryBuilder.asUser(args.anonymousUser);
     } else {
-      qb = args.queryBuilder.create(user);
+      qb = args.queryBuilder.asUser(user);
       p = qb.is_visible();
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
index 7f464f7..4f65ab4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
@@ -24,6 +24,7 @@
 public class RestoredSender extends ReplyToChangeSender {
   public static interface Factory extends
       ReplyToChangeSender.Factory<RestoredSender> {
+    @Override
     RestoredSender create(Change change);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
index 92fffde..d74eaeb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
@@ -46,6 +46,7 @@
     emailRegistrationToken = config.getEmailRegistrationToken();
   }
 
+  @Override
   public String encode(Account.Id accountId, String emailAddress) {
     try {
       String payload = String.format("%s:%s", accountId, emailAddress);
@@ -59,6 +60,7 @@
     }
   }
 
+  @Override
   public ParsedToken decode(String tokenString) throws InvalidTokenException {
     ValidToken token;
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
index b1c5955..2f8f75d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.server.mail;
 
 import com.google.common.primitives.Ints;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -86,8 +86,7 @@
     }
 
     smtpEncryption =
-        ConfigUtil.getEnum(cfg, "sendemail", null, "smtpencryption",
-            Encryption.NONE);
+        cfg.getEnum("sendemail", null, "smtpencryption", Encryption.NONE);
     sslVerify = cfg.getBoolean("sendemail", null, "sslverify", true);
 
     final int defaultPort;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java
index 32098d5..ace1f5b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java
@@ -37,6 +37,7 @@
     this.site = site;
   }
 
+  @Override
   public RuntimeInstance get() {
     String rl = "resource.loader";
     String pkg = "org.apache.velocity.runtime.resource.loader";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/DefaultFileExtensionRegistry.java b/gerrit-server/src/main/java/com/google/gerrit/server/mime/DefaultFileExtensionRegistry.java
similarity index 98%
rename from gerrit-server/src/main/java/com/google/gerrit/server/DefaultFileExtensionRegistry.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/mime/DefaultFileExtensionRegistry.java
index 61bd39e..8c6bb3e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/DefaultFileExtensionRegistry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mime/DefaultFileExtensionRegistry.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+package com.google.gerrit.server.mime;
 
 import com.google.common.collect.ImmutableMap;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/FileTypeRegistry.java b/gerrit-server/src/main/java/com/google/gerrit/server/mime/FileTypeRegistry.java
similarity index 97%
rename from gerrit-server/src/main/java/com/google/gerrit/server/FileTypeRegistry.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/mime/FileTypeRegistry.java
index 83f87f9..43d53f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/FileTypeRegistry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mime/FileTypeRegistry.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+package com.google.gerrit.server.mime;
 
 import eu.medsea.mimeutil.MimeType;
 import eu.medsea.mimeutil.MimeUtil2;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mime/MimeUtil2Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/mime/MimeUtil2Module.java
new file mode 100644
index 0000000..61b01af8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mime/MimeUtil2Module.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mime;
+
+import com.google.gerrit.server.util.HostPlatform;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+import eu.medsea.mimeutil.MimeUtil2;
+import eu.medsea.mimeutil.detector.ExtensionMimeDetector;
+import eu.medsea.mimeutil.detector.MagicMimeMimeDetector;
+
+public class MimeUtil2Module extends AbstractModule {
+  @Override
+  protected void configure() {
+  }
+
+  @Provides
+  @Singleton
+  MimeUtil2 provideMimeUtil2() {
+    MimeUtil2 m = new MimeUtil2();
+    m.registerMimeDetector(ExtensionMimeDetector.class.getName());
+    m.registerMimeDetector(MagicMimeMimeDetector.class.getName());
+    if (HostPlatform.isWin32()) {
+      m.registerMimeDetector("eu.medsea.mimeutil.detector.WindowsRegistryMimeDetector");
+    }
+    m.registerMimeDetector(DefaultFileExtensionRegistry.class.getName());
+    return m;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java b/gerrit-server/src/main/java/com/google/gerrit/server/mime/MimeUtilFileTypeRegistry.java
similarity index 88%
rename from gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/mime/MimeUtilFileTypeRegistry.java
index 5263c6b..2387200 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mime/MimeUtilFileTypeRegistry.java
@@ -12,10 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server;
+package com.google.gerrit.server.mime;
 
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.util.HostPlatform;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -43,22 +42,12 @@
       LoggerFactory.getLogger(MimeUtilFileTypeRegistry.class);
 
   private final Config cfg;
-  private MimeUtil2 mimeUtil;
+  private final MimeUtil2 mimeUtil;
 
   @Inject
-  MimeUtilFileTypeRegistry(@GerritServerConfig final Config gsc) {
+  MimeUtilFileTypeRegistry(@GerritServerConfig Config gsc, MimeUtil2 mu2) {
     cfg = gsc;
-    mimeUtil = new MimeUtil2();
-    register("eu.medsea.mimeutil.detector.ExtensionMimeDetector");
-    register("eu.medsea.mimeutil.detector.MagicMimeMimeDetector");
-    if (HostPlatform.isWin32()) {
-      register("eu.medsea.mimeutil.detector.WindowsRegistryMimeDetector");
-    }
-    register(DefaultFileExtensionRegistry.class.getName());
-  }
-
-  private void register(String name) {
-    mimeUtil.registerMimeDetector(name);
+    mimeUtil = mu2;
   }
 
   /**
@@ -92,6 +81,7 @@
     return mimeType.getSpecificity();
   }
 
+  @Override
   @SuppressWarnings("unchecked")
   public MimeType getMimeType(final String path, final byte[] content) {
     Set<MimeType> mimeTypes = new HashSet<>();
@@ -122,6 +112,7 @@
     return types.get(0);
   }
 
+  @Override
   public boolean isSafeInline(final MimeType type) {
     if (MimeUtil2.UNKNOWN_MIME_TYPE.equals(type)) {
       // Most browsers perform content type sniffing when they get told
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index 37094fd..80d4504 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -21,49 +21,79 @@
 import com.google.gwtorm.server.OrmException;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
 
 /** View of contents at a single ref related to some change. **/
 public abstract class AbstractChangeNotes<T> extends VersionedMetaData {
-  private boolean loaded;
   protected final GitRepositoryManager repoManager;
-  private final Change change;
+  protected final NotesMigration migration;
+  private final Change.Id changeId;
 
-  AbstractChangeNotes(GitRepositoryManager repoManager, Change change) {
+  private boolean loaded;
+
+  AbstractChangeNotes(GitRepositoryManager repoManager,
+      NotesMigration migration, Change.Id changeId) {
     this.repoManager = repoManager;
-    this.change = new Change(change);
+    this.migration = migration;
+    this.changeId = changeId;
   }
 
   public Change.Id getChangeId() {
-    return change.getId();
-  }
-
-  public Change getChange() {
-    return change;
+    return changeId;
   }
 
   public T load() throws OrmException {
-    if (!loaded) {
-      Repository repo;
-      try {
-        repo = repoManager.openRepository(getProjectName());
-      } catch (IOException e) {
-        throw new OrmException(e);
-      }
-      try {
-        load(repo);
-        loaded = true;
-      } catch (ConfigInvalidException | IOException e) {
-        throw new OrmException(e);
-      } finally {
-        repo.close();
-      }
+    if (loaded) {
+      return self();
+    }
+    if (!migration.enabled()) {
+      loadDefaults();
+      return self();
+    }
+    Repository repo;
+    try {
+      repo = repoManager.openMetadataRepository(getProjectName());
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+    try {
+      load(repo);
+      loaded = true;
+    } catch (ConfigInvalidException | IOException e) {
+      throw new OrmException(e);
+    } finally {
+      repo.close();
     }
     return self();
   }
 
+  public ObjectId loadRevision() throws OrmException {
+    if (loaded) {
+      return getRevision();
+    } else if (!migration.enabled()) {
+      return null;
+    }
+    Repository repo;
+    try {
+      repo = repoManager.openMetadataRepository(getProjectName());
+      try {
+        Ref ref = repo.getRef(getRefName());
+        return ref != null ? ref.getObjectId() : null;
+      } finally {
+        repo.close();
+      }
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  /** Load default values for any instance variables when notedb is disabled. */
+  protected abstract void loadDefaults();
+
   /**
    * @return the NameKey for the project where the notes should be stored,
    *    which is not necessarily the same as the change's project.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index 0d637f5..fbca668 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.GERRIT_PLACEHOLDER_HOST;
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -26,10 +25,13 @@
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.VersionedMetaData;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.server.OrmException;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -43,6 +45,7 @@
   protected final GitRepositoryManager repoManager;
   protected final MetaDataUpdate.User updateFactory;
   protected final ChangeControl ctl;
+  protected final String anonymousCowardName;
   protected final PersonIdent serverIdent;
   protected final Date when;
   protected PatchSet.Id psId;
@@ -50,15 +53,22 @@
   AbstractChangeUpdate(NotesMigration migration,
       GitRepositoryManager repoManager,
       MetaDataUpdate.User updateFactory, ChangeControl ctl,
-      PersonIdent serverIdent, Date when) {
+      PersonIdent serverIdent,
+      String anonymousCowardName,
+      Date when) {
     this.migration = migration;
     this.repoManager = repoManager;
     this.updateFactory = updateFactory;
     this.ctl = ctl;
     this.serverIdent = serverIdent;
+    this.anonymousCowardName = anonymousCowardName;
     this.when = when;
   }
 
+  public ChangeNotes getChangeNotes() {
+    return ctl.getNotes();
+  }
+
   public Change getChange() {
     return ctl.getChange();
   }
@@ -71,6 +81,10 @@
     return (IdentifiedUser) ctl.getCurrentUser();
   }
 
+  public PatchSet.Id getPatchSetId() {
+    return psId;
+  }
+
   public void setPatchSetId(PatchSet.Id psId) {
     checkArgument(psId == null
         || psId.getParentKey().equals(getChange().getId()));
@@ -78,8 +92,8 @@
   }
 
   private void load() throws IOException {
-    if (migration.write() && getRevision() == null) {
-      Repository repo = repoManager.openRepository(getProjectName());
+    if (migration.writeChanges() && getRevision() == null) {
+      Repository repo = repoManager.openMetadataRepository(getProjectName());
       try {
         load(repo);
       } catch (ConfigInvalidException e) {
@@ -90,16 +104,27 @@
     }
   }
 
+  public void setInserter(ObjectInserter inserter) {
+    this.inserter = inserter;
+  }
+
   @Override
   public BatchMetaDataUpdate openUpdate(MetaDataUpdate update) throws IOException {
     throw new UnsupportedOperationException("use openUpdate()");
   }
 
   public BatchMetaDataUpdate openUpdate() throws IOException {
-    if (migration.write()) {
+    return openUpdateInBatch(null);
+  }
+
+  public BatchMetaDataUpdate openUpdateInBatch(BatchRefUpdate bru)
+      throws IOException {
+    if (migration.writeChanges()) {
       load();
       MetaDataUpdate md =
-          updateFactory.create(getProjectName(), getUser());
+          updateFactory.create(getProjectName(),
+              repoManager.openMetadataRepository(getProjectName()), getUser(),
+              bru);
       md.setAllowEmpty(true);
       return super.openUpdate(md);
     }
@@ -120,6 +145,11 @@
       }
 
       @Override
+      public void removeRef(String refName) {
+        // Do nothing.
+      }
+
+      @Override
       public RevCommit commit() {
         return null;
       }
@@ -147,15 +177,17 @@
   }
 
   protected PersonIdent newIdent(Account author, Date when) {
-    return new PersonIdent(
-        author.getFullName(),
-        author.getId().get() + "@" + GERRIT_PLACEHOLDER_HOST,
-        when, serverIdent.getTimeZone());
+    return ChangeNoteUtil.newIdent(author, when, serverIdent,
+        anonymousCowardName);
   }
 
+  /** Writes commit to a BatchMetaDataUpdate without committing the batch. */
+  public abstract void writeCommit(BatchMetaDataUpdate batch)
+      throws OrmException, IOException;
+
   /**
    * @return the NameKey for the project where the update will be stored,
    *    which is not necessarily the same as the change's project.
    */
-  abstract protected Project.NameKey getProjectName();
+  protected abstract Project.NameKey getProjectName();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
new file mode 100644
index 0000000..3b51bb4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -0,0 +1,319 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.notedb.CommentsInNotesUtil.getCommentPsId;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Table;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A single delta to apply atomically to a change.
+ * <p>
+ * This delta contains only draft comments on a single patch set of a change by
+ * a single author. This delta will become a single commit in the All-Users
+ * repository.
+ * <p>
+ * This class is not thread safe.
+ */
+public class ChangeDraftUpdate extends AbstractChangeUpdate {
+  public interface Factory {
+    ChangeDraftUpdate create(ChangeControl ctl, Date when);
+  }
+
+  private final AllUsersName draftsProject;
+  private final Account.Id accountId;
+  private final CommentsInNotesUtil commentsUtil;
+  private final ChangeNotes changeNotes;
+  private final DraftCommentNotes draftNotes;
+
+  private List<PatchLineComment> upsertComments;
+  private List<PatchLineComment> deleteComments;
+
+  @AssistedInject
+  private ChangeDraftUpdate(
+      @GerritPersonIdent PersonIdent serverIdent,
+      @AnonymousCowardName String anonymousCowardName,
+      GitRepositoryManager repoManager,
+      NotesMigration migration,
+      MetaDataUpdate.User updateFactory,
+      DraftCommentNotes.Factory draftNotesFactory,
+      AllUsersName allUsers,
+      CommentsInNotesUtil commentsUtil,
+      @Assisted ChangeControl ctl,
+      @Assisted Date when) throws OrmException {
+    super(migration, repoManager, updateFactory, ctl, serverIdent,
+        anonymousCowardName, when);
+    this.draftsProject = allUsers;
+    this.commentsUtil = commentsUtil;
+    checkState(ctl.getCurrentUser().isIdentifiedUser(),
+        "Current user must be identified");
+    IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
+    this.accountId = user.getAccountId();
+    this.changeNotes = getChangeNotes().load();
+    this.draftNotes = draftNotesFactory.create(ctl.getChange().getId(),
+        user.getAccountId());
+
+    this.upsertComments = Lists.newArrayList();
+    this.deleteComments = Lists.newArrayList();
+  }
+
+  public void insertComment(PatchLineComment c) throws OrmException {
+    verifyComment(c);
+    checkArgument(c.getStatus() == Status.DRAFT,
+        "Cannot insert a published comment into a ChangeDraftUpdate");
+    if (migration.readChanges()) {
+      checkArgument(!changeNotes.containsComment(c),
+          "A comment already exists with the same key,"
+          + " so the following comment cannot be inserted: %s", c);
+    }
+    upsertComments.add(c);
+  }
+
+  public void upsertComment(PatchLineComment c) {
+    verifyComment(c);
+    checkArgument(c.getStatus() == Status.DRAFT,
+        "Cannot upsert a published comment into a ChangeDraftUpdate");
+    upsertComments.add(c);
+  }
+
+  public void updateComment(PatchLineComment c) throws OrmException {
+    verifyComment(c);
+    checkArgument(c.getStatus() == Status.DRAFT,
+        "Cannot update a published comment into a ChangeDraftUpdate");
+    // Here, we check to see if this comment existed previously as a draft.
+    // However, this could cause a race condition if there is a delete and an
+    // update operation happening concurrently (or two deletes) and they both
+    // believe that the comment exists. If a delete happens first, then
+    // the update will fail. However, this is an acceptable risk since the
+    // caller wanted the comment deleted anyways, so the end result will be the
+    // same either way.
+    if (migration.readChanges()) {
+      checkArgument(draftNotes.load().containsComment(c),
+          "Cannot update this comment because it didn't exist previously");
+    }
+    upsertComments.add(c);
+  }
+
+  public void deleteComment(PatchLineComment c) throws OrmException {
+    verifyComment(c);
+    // See the comment above about potential race condition.
+    if (migration.readChanges()) {
+      checkArgument(draftNotes.load().containsComment(c),
+          "Cannot delete this comment because it didn't previously exist as a"
+          + " draft");
+    }
+    if (migration.writeChanges()) {
+      if (draftNotes.load().containsComment(c)) {
+        deleteComments.add(c);
+      }
+    }
+  }
+
+  /**
+   * Deletes a PatchLineComment from the list of drafts only if it existed
+   * previously as a draft. If it wasn't a draft previously, this is a no-op.
+   */
+  public void deleteCommentIfPresent(PatchLineComment c) throws OrmException {
+    if (draftNotes.load().containsComment(c)) {
+      verifyComment(c);
+      deleteComments.add(c);
+    }
+  }
+
+  private void verifyComment(PatchLineComment comment) {
+    checkState(psId != null,
+        "setPatchSetId must be called first");
+    checkArgument(getCommentPsId(comment).equals(psId),
+        "Comment on %s does not match configured patch set %s",
+        getCommentPsId(comment), psId);
+    if (migration.writeChanges()) {
+      checkArgument(comment.getRevId() != null);
+    }
+    checkArgument(comment.getAuthor().equals(accountId),
+        "The author for the following comment does not match the author of"
+        + " this ChangeDraftUpdate (%s): %s", accountId, comment);
+  }
+
+  /** @return the tree id for the updated tree */
+  private ObjectId storeCommentsInNotes(AtomicBoolean removedAllComments)
+      throws OrmException, IOException {
+    if (isEmpty()) {
+      return null;
+    }
+
+    NoteMap noteMap = draftNotes.load().getNoteMap();
+    if (noteMap == null) {
+      noteMap = NoteMap.newEmptyMap();
+    }
+
+    Table<PatchSet.Id, String, PatchLineComment> baseDrafts =
+        draftNotes.getDraftBaseComments();
+    Table<PatchSet.Id, String, PatchLineComment> psDrafts =
+        draftNotes.getDraftPsComments();
+
+    boolean draftsEmpty = baseDrafts.isEmpty() && psDrafts.isEmpty();
+
+    // There is no need to rewrite the note for one of the sides of the patch
+    // set if all of the modifications were made to the comments of one side,
+    // so we set these flags to potentially save that extra work.
+    boolean baseSideChanged = false;
+    boolean revisionSideChanged = false;
+
+    // We must define these RevIds so that if this update deletes all
+    // remaining comments on a given side, then we can remove that note.
+    // However, if this update doesn't delete any comments, it is okay for these
+    // to be null because they won't be used.
+    RevId baseRevId = null;
+    RevId psRevId = null;
+
+    for (PatchLineComment c : deleteComments) {
+      if (c.getSide() == (short) 0) {
+        baseSideChanged = true;
+        baseRevId = c.getRevId();
+        baseDrafts.remove(psId, c.getKey().get());
+      } else {
+        revisionSideChanged = true;
+        psRevId = c.getRevId();
+        psDrafts.remove(psId, c.getKey().get());
+      }
+    }
+
+    for (PatchLineComment c : upsertComments) {
+      if (c.getSide() == (short) 0) {
+        baseSideChanged = true;
+        baseDrafts.put(psId, c.getKey().get(), c);
+      } else {
+        revisionSideChanged = true;
+        psDrafts.put(psId, c.getKey().get(), c);
+      }
+    }
+
+    List<PatchLineComment> newBaseDrafts =
+        Lists.newArrayList(baseDrafts.row(psId).values());
+    List<PatchLineComment> newPsDrafts =
+        Lists.newArrayList(psDrafts.row(psId).values());
+
+    updateNoteMap(baseSideChanged, noteMap, newBaseDrafts,
+        baseRevId);
+    updateNoteMap(revisionSideChanged, noteMap, newPsDrafts,
+        psRevId);
+
+    removedAllComments.set(
+        baseDrafts.isEmpty() && psDrafts.isEmpty() && !draftsEmpty);
+
+    return noteMap.writeTree(inserter);
+  }
+
+  private void updateNoteMap(boolean changed, NoteMap noteMap,
+      List<PatchLineComment> comments, RevId commitId)
+      throws IOException {
+    if (changed) {
+      if (comments.isEmpty()) {
+        commentsUtil.removeNote(noteMap, commitId);
+      } else {
+        commentsUtil.writeCommentsToNoteMap(noteMap, comments, inserter);
+      }
+    }
+  }
+
+  public RevCommit commit() throws IOException {
+    BatchMetaDataUpdate batch = openUpdate();
+    try {
+      writeCommit(batch);
+      return batch.commit();
+    } catch (OrmException e) {
+      throw new IOException(e);
+    } finally {
+      batch.close();
+    }
+  }
+
+  @Override
+  public void writeCommit(BatchMetaDataUpdate batch)
+      throws OrmException, IOException {
+    CommitBuilder builder = new CommitBuilder();
+    if (migration.writeChanges()) {
+      AtomicBoolean removedAllComments = new AtomicBoolean();
+      ObjectId treeId = storeCommentsInNotes(removedAllComments);
+      if (treeId != null) {
+        if (removedAllComments.get()) {
+          batch.removeRef(getRefName());
+        } else {
+          builder.setTreeId(treeId);
+          batch.write(builder);
+        }
+      }
+    }
+  }
+
+  @Override
+  protected Project.NameKey getProjectName() {
+    return draftsProject;
+  }
+
+  @Override
+  protected String getRefName() {
+    return RefNames.refsDraftComments(accountId, getChange().getId());
+  }
+
+  @Override
+  protected boolean onSave(CommitBuilder commit) throws IOException,
+      ConfigInvalidException {
+    if (isEmpty()) {
+      return false;
+    }
+    commit.setAuthor(newIdent(getUser().getAccount(), when));
+    commit.setCommitter(new PersonIdent(serverIdent, when));
+    commit.setMessage(String.format("Comment on patch set %d", psId.get()));
+    return true;
+  }
+
+  private boolean isEmpty() {
+    return deleteComments.isEmpty()
+        && upsertComments.isEmpty();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index 7204735..c561a0d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -14,14 +14,20 @@
 
 package com.google.gerrit.server.notedb;
 
+import com.google.gerrit.common.data.AccountInfo;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.RefNames;
 
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.FooterKey;
 
+import java.util.Date;
+
 public class ChangeNoteUtil {
   static final String GERRIT_PLACEHOLDER_HOST = "gerrit";
 
+  static final FooterKey FOOTER_HASHTAGS = new FooterKey("Hashtags");
   static final FooterKey FOOTER_LABEL = new FooterKey("Label");
   static final FooterKey FOOTER_PATCH_SET = new FooterKey("Patch-set");
   static final FooterKey FOOTER_STATUS = new FooterKey("Status");
@@ -39,10 +45,18 @@
     r.append(m);
     r.append('/');
     r.append(n);
-    r.append("/meta");
+    r.append(RefNames.META_SUFFIX);
     return r.toString();
   }
 
+  static PersonIdent newIdent(Account author, Date when,
+      PersonIdent serverIdent, String anonymousCowardName) {
+    return new PersonIdent(
+        new AccountInfo(author).getName(anonymousCowardName),
+        author.getId().get() + "@" + GERRIT_PLACEHOLDER_HOST,
+        when, serverIdent.getTimeZone());
+  }
+
   private ChangeNoteUtil() {
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
index b242d7c..6d4f3a4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -14,29 +14,19 @@
 
 package com.google.gerrit.server.notedb;
 
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
-import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.GERRIT_PLACEHOLDER_HOST;
+import static com.google.gerrit.server.notedb.CommentsInNotesUtil.getCommentPsId;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Enums;
 import com.google.common.base.Function;
-import com.google.common.base.Optional;
-import com.google.common.base.Supplier;
-import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
-import com.google.common.collect.LinkedListMultimap;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Ordering;
 import com.google.common.collect.Table;
-import com.google.common.collect.Tables;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Account;
@@ -44,42 +34,31 @@
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
-import com.google.gerrit.reviewdb.client.PatchSet.Id;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.util.LabelVote;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.notes.NoteMap;
-import org.eclipse.jgit.revwalk.FooterKey;
-import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.util.RawParseUtils;
 
 import java.io.IOException;
-import java.nio.charset.Charset;
 import java.sql.Timestamp;
-import java.text.ParseException;
-import java.util.Collection;
-import java.util.Collections;
 import java.util.Comparator;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
 /** View of a single {@link Change} based on the log of its notes branch. */
 public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
-  private static final Ordering<PatchSetApproval> PSA_BY_TIME =
+  static final Ordering<PatchSetApproval> PSA_BY_TIME =
       Ordering.natural().onResultOf(
         new Function<PatchSetApproval, Timestamp>() {
           @Override
@@ -99,6 +78,7 @@
 
   public static Comparator<PatchLineComment> PatchLineCommentComparator =
       new Comparator<PatchLineComment>() {
+    @Override
     public int compare(PatchLineComment c1, PatchLineComment c2) {
       String filename1 = c1.getKey().getParentKey().get();
       String filename2 = c2.getKey().getParentKey().get();
@@ -134,367 +114,48 @@
   @Singleton
   public static class Factory {
     private final GitRepositoryManager repoManager;
+    private final NotesMigration migration;
+    private final AllUsersNameProvider allUsersProvider;
 
     @VisibleForTesting
     @Inject
-    public Factory(GitRepositoryManager repoManager) {
+    public Factory(GitRepositoryManager repoManager,
+        NotesMigration migration,
+        AllUsersNameProvider allUsersProvider) {
       this.repoManager = repoManager;
+      this.migration = migration;
+      this.allUsersProvider = allUsersProvider;
     }
 
     public ChangeNotes create(Change change) {
-      return new ChangeNotes(repoManager, change);
+      return new ChangeNotes(repoManager, migration, allUsersProvider, change);
     }
   }
 
-  private static class Parser {
-    private final Change.Id changeId;
-    private final ObjectId tip;
-    private final RevWalk walk;
-    private final Repository repo;
-    private final Map<PatchSet.Id,
-        Table<Account.Id, String, Optional<PatchSetApproval>>> approvals;
-    private final Map<Account.Id, ReviewerState> reviewers;
-    private final List<SubmitRecord> submitRecords;
-    private final Multimap<PatchSet.Id, ChangeMessage> changeMessages;
-    private final Multimap<Id, PatchLineComment> commentsForPs;
-    private final Multimap<PatchSet.Id, PatchLineComment> commentsForBase;
-    private NoteMap commentNoteMap;
-    private Change.Status status;
-
-    private Parser(Change change, ObjectId tip, RevWalk walk,
-        GitRepositoryManager repoManager) throws RepositoryNotFoundException,
-        IOException {
-      this.changeId = change.getId();
-      this.tip = tip;
-      this.walk = walk;
-      this.repo = repoManager.openRepository(getProjectName(change));
-      approvals = Maps.newHashMap();
-      reviewers = Maps.newLinkedHashMap();
-      submitRecords = Lists.newArrayListWithExpectedSize(1);
-      changeMessages = LinkedListMultimap.create();
-      commentsForPs = ArrayListMultimap.create();
-      commentsForBase = ArrayListMultimap.create();
-    }
-
-    private void parseAll() throws ConfigInvalidException, IOException, ParseException {
-      walk.markStart(walk.parseCommit(tip));
-      for (RevCommit commit : walk) {
-        parse(commit);
-      }
-      parseComments();
-      pruneReviewers();
-    }
-
-    private ImmutableListMultimap<PatchSet.Id, PatchSetApproval>
-        buildApprovals() {
-      Multimap<PatchSet.Id, PatchSetApproval> result =
-          ArrayListMultimap.create(approvals.keySet().size(), 3);
-      for (Table<?, ?, Optional<PatchSetApproval>> curr
-          : approvals.values()) {
-        for (PatchSetApproval psa : Optional.presentInstances(curr.values())) {
-          result.put(psa.getPatchSetId(), psa);
-        }
-      }
-      for (Collection<PatchSetApproval> v : result.asMap().values()) {
-        Collections.sort((List<PatchSetApproval>) v, PSA_BY_TIME);
-      }
-      return ImmutableListMultimap.copyOf(result);
-    }
-
-    private ImmutableListMultimap<PatchSet.Id, ChangeMessage> buildMessages() {
-      for (Collection<ChangeMessage> v : changeMessages.asMap().values()) {
-        Collections.sort((List<ChangeMessage>) v, MESSAGE_BY_TIME);
-      }
-      return ImmutableListMultimap.copyOf(changeMessages);
-    }
-
-    private void parse(RevCommit commit) throws ConfigInvalidException, IOException {
-      if (status == null) {
-        status = parseStatus(commit);
-      }
-      PatchSet.Id psId = parsePatchSetId(commit);
-      Account.Id accountId = parseIdent(commit);
-      parseChangeMessage(psId, accountId, commit);
-
-
-      if (submitRecords.isEmpty()) {
-        // Only parse the most recent set of submit records; any older ones are
-        // still there, but not currently used.
-        parseSubmitRecords(commit.getFooterLines(FOOTER_SUBMITTED_WITH));
-      }
-
-      for (String line : commit.getFooterLines(FOOTER_LABEL)) {
-        parseApproval(psId, accountId, commit, line);
-      }
-
-      for (ReviewerState state : ReviewerState.values()) {
-        for (String line : commit.getFooterLines(state.getFooterKey())) {
-          parseReviewer(state, line);
-        }
-      }
-    }
-
-    private Change.Status parseStatus(RevCommit commit)
-        throws ConfigInvalidException {
-      List<String> statusLines = commit.getFooterLines(FOOTER_STATUS);
-      if (statusLines.isEmpty()) {
-        return null;
-      } else if (statusLines.size() > 1) {
-        throw expectedOneFooter(FOOTER_STATUS, statusLines);
-      }
-      Optional<Change.Status> status = Enums.getIfPresent(
-          Change.Status.class, statusLines.get(0).toUpperCase());
-      if (!status.isPresent()) {
-        throw invalidFooter(FOOTER_STATUS, statusLines.get(0));
-      }
-      return status.get();
-    }
-
-    private PatchSet.Id parsePatchSetId(RevCommit commit)
-        throws ConfigInvalidException {
-      List<String> psIdLines = commit.getFooterLines(FOOTER_PATCH_SET);
-      if (psIdLines.size() != 1) {
-        throw expectedOneFooter(FOOTER_PATCH_SET, psIdLines);
-      }
-      Integer psId = Ints.tryParse(psIdLines.get(0));
-      if (psId == null) {
-        throw invalidFooter(FOOTER_PATCH_SET, psIdLines.get(0));
-      }
-      return new PatchSet.Id(changeId, psId);
-    }
-
-    private void parseChangeMessage(PatchSet.Id psId, Account.Id accountId,
-        RevCommit commit) {
-      byte[] raw = commit.getRawBuffer();
-      int size = raw.length;
-      Charset enc = RawParseUtils.parseEncoding(raw);
-
-      int subjectStart = RawParseUtils.commitMessage(raw, 0);
-      if (subjectStart < 0 || subjectStart >= size) {
-        return;
-      }
-
-      int subjectEnd = RawParseUtils.endOfParagraph(raw, subjectStart);
-      if (subjectEnd == size) {
-        return;
-      }
-
-      int changeMessageStart;
-
-      if (raw[subjectEnd] == '\n') {
-        changeMessageStart = subjectEnd + 2; //\n\n ends paragraph
-      } else if (raw[subjectEnd] == '\r') {
-        changeMessageStart = subjectEnd + 4; //\r\n\r\n ends paragraph
-      } else {
-        return;
-      }
-
-      int ptr = size - 1;
-      int changeMessageEnd = -1;
-      while(ptr > changeMessageStart) {
-        ptr = RawParseUtils.prevLF(raw, ptr, '\r');
-        if (ptr == -1) {
-          break;
-        }
-        if (raw[ptr] == '\n') {
-          changeMessageEnd = ptr - 1;
-          break;
-        } else if (raw[ptr] == '\r') {
-          changeMessageEnd = ptr - 3;
-          break;
-        }
-      }
-
-      if (ptr <= changeMessageStart) {
-        return;
-      }
-
-      String changeMsgString = RawParseUtils.decode(enc, raw,
-          changeMessageStart, changeMessageEnd + 1);
-      ChangeMessage changeMessage = new ChangeMessage(
-          new ChangeMessage.Key(psId.getParentKey(), commit.name()),
-          accountId,
-          new Timestamp(commit.getCommitterIdent().getWhen().getTime()),
-          psId);
-      changeMessage.setMessage(changeMsgString);
-      changeMessages.put(psId, changeMessage);
-    }
-
-    private void parseComments()
-        throws IOException, ConfigInvalidException, ParseException {
-      commentNoteMap = CommentsInNotesUtil.parseCommentsFromNotes(repo,
-          ChangeNoteUtil.changeRefName(changeId), walk, changeId,
-          commentsForBase, commentsForPs, Status.PUBLISHED);
-    }
-
-    private void parseApproval(PatchSet.Id psId, Account.Id accountId,
-        RevCommit commit, String line) throws ConfigInvalidException {
-      Table<Account.Id, String, Optional<PatchSetApproval>> curr =
-          approvals.get(psId);
-      if (curr == null) {
-        curr = Tables.newCustomTable(
-            Maps.<Account.Id, Map<String, Optional<PatchSetApproval>>>
-                newHashMapWithExpectedSize(2),
-            new Supplier<Map<String, Optional<PatchSetApproval>>>() {
-              @Override
-              public Map<String, Optional<PatchSetApproval>> get() {
-                return Maps.newLinkedHashMap();
-              }
-            });
-        approvals.put(psId, curr);
-      }
-
-      if (line.startsWith("-")) {
-        String label = line.substring(1);
-        if (!curr.contains(accountId, label)) {
-          curr.put(accountId, label, Optional.<PatchSetApproval> absent());
-        }
-      } else {
-        LabelVote l;
-        try {
-          l = LabelVote.parseWithEquals(line);
-        } catch (IllegalArgumentException e) {
-          ConfigInvalidException pe =
-              parseException("invalid %s: %s", FOOTER_LABEL, line);
-          pe.initCause(e);
-          throw pe;
-        }
-        if (!curr.contains(accountId, l.getLabel())) {
-          curr.put(accountId, l.getLabel(), Optional.of(new PatchSetApproval(
-              new PatchSetApproval.Key(
-                  psId,
-                  accountId,
-                  new LabelId(l.getLabel())),
-              l.getValue(),
-              new Timestamp(commit.getCommitterIdent().getWhen().getTime()))));
-        }
-      }
-    }
-
-    private void parseSubmitRecords(List<String> lines)
-        throws ConfigInvalidException {
-      SubmitRecord rec = null;
-
-      for (String line : lines) {
-        int c = line.indexOf(": ");
-        if (c < 0) {
-          rec = new SubmitRecord();
-          submitRecords.add(rec);
-          int s = line.indexOf(' ');
-          String statusStr = s >= 0 ? line.substring(0, s) : line;
-          Optional<SubmitRecord.Status> status =
-              Enums.getIfPresent(SubmitRecord.Status.class, statusStr);
-          checkFooter(status.isPresent(), FOOTER_SUBMITTED_WITH, line);
-          rec.status = status.get();
-          if (s >= 0) {
-            rec.errorMessage = line.substring(s);
-          }
-        } else {
-          checkFooter(rec != null, FOOTER_SUBMITTED_WITH, line);
-          SubmitRecord.Label label = new SubmitRecord.Label();
-          if (rec.labels == null) {
-            rec.labels = Lists.newArrayList();
-          }
-          rec.labels.add(label);
-
-          Optional<SubmitRecord.Label.Status> status = Enums.getIfPresent(
-              SubmitRecord.Label.Status.class, line.substring(0, c));
-          checkFooter(status.isPresent(), FOOTER_SUBMITTED_WITH, line);
-          label.status = status.get();
-          int c2 = line.indexOf(": ", c + 2);
-          if (c2 >= 0) {
-            label.label = line.substring(c + 2, c2);
-            PersonIdent ident =
-                RawParseUtils.parsePersonIdent(line.substring(c2 + 2));
-            checkFooter(ident != null, FOOTER_SUBMITTED_WITH, line);
-            label.appliedBy = parseIdent(ident);
-          } else {
-            label.label = line.substring(c + 2);
-          }
-        }
-      }
-    }
-
-    private Account.Id parseIdent(RevCommit commit)
-        throws ConfigInvalidException {
-      return parseIdent(commit.getAuthorIdent());
-    }
-
-    private Account.Id parseIdent(PersonIdent ident)
-        throws ConfigInvalidException {
-      String email = ident.getEmailAddress();
-      int at = email.indexOf('@');
-      if (at >= 0) {
-        String host = email.substring(at + 1, email.length());
-        Integer id = Ints.tryParse(email.substring(0, at));
-        if (id != null && host.equals(GERRIT_PLACEHOLDER_HOST)) {
-          return new Account.Id(id);
-        }
-      }
-      throw parseException("invalid identity, expected <id>@%s: %s",
-        GERRIT_PLACEHOLDER_HOST, email);
-    }
-
-    private void parseReviewer(ReviewerState state, String line)
-        throws ConfigInvalidException {
-      PersonIdent ident = RawParseUtils.parsePersonIdent(line);
-      if (ident == null) {
-        throw invalidFooter(state.getFooterKey(), line);
-      }
-      Account.Id accountId = parseIdent(ident);
-      if (!reviewers.containsKey(accountId)) {
-        reviewers.put(accountId, state);
-      }
-    }
-
-    private void pruneReviewers() {
-      Iterator<Map.Entry<Account.Id, ReviewerState>> rit =
-          reviewers.entrySet().iterator();
-      while (rit.hasNext()) {
-        Map.Entry<Account.Id, ReviewerState> e = rit.next();
-        if (e.getValue() == ReviewerState.REMOVED) {
-          rit.remove();
-          for (Table<Account.Id, ?, ?> curr : approvals.values()) {
-            curr.rowKeySet().remove(e.getKey());
-          }
-        }
-      }
-    }
-
-    private ConfigInvalidException expectedOneFooter(FooterKey footer,
-        List<String> actual) {
-      return parseException("missing or multiple %s: %s",
-          footer.getName(), actual);
-    }
-
-    private ConfigInvalidException invalidFooter(FooterKey footer,
-        String actual) {
-      return parseException("invalid %s: %s", footer.getName(), actual);
-    }
-
-    private void checkFooter(boolean expr, FooterKey footer, String actual)
-        throws ConfigInvalidException {
-      if (!expr) {
-        throw invalidFooter(footer, actual);
-      }
-    }
-
-    private ConfigInvalidException parseException(String fmt, Object... args) {
-      return ChangeNotes.parseException(changeId, fmt, args);
-    }
-  }
-
+  private final Change change;
   private ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals;
   private ImmutableSetMultimap<ReviewerState, Account.Id> reviewers;
+  private ImmutableList<Account.Id> allPastReviewers;
   private ImmutableList<SubmitRecord> submitRecords;
   private ImmutableListMultimap<PatchSet.Id, ChangeMessage> changeMessages;
   private ImmutableListMultimap<PatchSet.Id, PatchLineComment> commentsForBase;
   private ImmutableListMultimap<PatchSet.Id, PatchLineComment> commentsForPS;
+  private ImmutableSet<String> hashtags;
   NoteMap noteMap;
 
+  private final AllUsersName allUsers;
+  private DraftCommentNotes draftCommentNotes;
+
   @VisibleForTesting
-  public ChangeNotes(GitRepositoryManager repoManager, Change change) {
-    super(repoManager, change);
+  public ChangeNotes(GitRepositoryManager repoManager, NotesMigration migration,
+      AllUsersNameProvider allUsersProvider, Change change) {
+    super(repoManager, migration, change.getId());
+    this.allUsers = allUsersProvider.get();
+    this.change = new Change(change);
+  }
+
+  public Change getChange() {
+    return change;
   }
 
   public ImmutableListMultimap<PatchSet.Id, PatchSetApproval> getApprovals() {
@@ -506,6 +167,21 @@
   }
 
   /**
+   *
+   * @return a ImmutableSet of all hashtags for this change sorted in alphabetical order.
+   */
+  public ImmutableSet<String> getHashtags() {
+    return ImmutableSortedSet.copyOf(hashtags);
+  }
+
+  /**
+   * @return a list of all users who have ever been a reviewer on this change.
+   */
+  public ImmutableList<Account.Id> getAllPastReviewers() {
+    return allPastReviewers;
+  }
+
+  /**
    * @return submit records stored during the most recent submit; only for
    *     changes that were actually submitted.
    */
@@ -530,6 +206,55 @@
     return commentsForPS;
   }
 
+  public Table<PatchSet.Id, String, PatchLineComment> getDraftBaseComments(
+      Account.Id author) throws OrmException {
+    loadDraftComments(author);
+    return draftCommentNotes.getDraftBaseComments();
+  }
+
+  public Table<PatchSet.Id, String, PatchLineComment> getDraftPsComments(
+      Account.Id author) throws OrmException {
+    loadDraftComments(author);
+    return draftCommentNotes.getDraftPsComments();
+  }
+
+  /**
+   * If draft comments have already been loaded for this author, then they will
+   * not be reloaded. However, this method will load the comments if no draft
+   * comments have been loaded or if the caller would like the drafts for
+   * another author.
+   */
+  private void loadDraftComments(Account.Id author)
+      throws OrmException {
+    if (draftCommentNotes == null ||
+        !author.equals(draftCommentNotes.getAuthor())) {
+      draftCommentNotes = new DraftCommentNotes(repoManager, migration,
+          allUsers, getChangeId(), author);
+      draftCommentNotes.load();
+    }
+  }
+
+  public boolean containsComment(PatchLineComment c) throws OrmException {
+    if (containsCommentPublished(c)) {
+      return true;
+    }
+    loadDraftComments(c.getAuthor());
+    return draftCommentNotes.containsComment(c);
+  }
+
+  public boolean containsCommentPublished(PatchLineComment c) {
+    PatchSet.Id psId = getCommentPsId(c);
+    List<PatchLineComment> list = (c.getSide() == (short) 0)
+        ? getBaseComments().get(psId)
+        : getPatchSetComments().get(psId);
+    for (PatchLineComment l : list) {
+      if (c.getKey().equals(l.getKey())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   /** @return the NoteMap */
   NoteMap getNoteMap() {
     return noteMap;
@@ -547,10 +272,9 @@
       loadDefaults();
       return;
     }
-    RevWalk walk = new RevWalk(reader);
-    try {
-      Change change = getChange();
-      Parser parser = new Parser(change, rev, walk, repoManager);
+    try (RevWalk walk = new RevWalk(reader);
+        ChangeNotesParser parser =
+          new ChangeNotesParser(change, rev, walk, repoManager)) {
       parser.parseAll();
 
       if (parser.status != null) {
@@ -562,6 +286,11 @@
       commentsForPS = ImmutableListMultimap.copyOf(parser.commentsForPs);
       noteMap = parser.commentNoteMap;
 
+      if (parser.hashtags != null) {
+        hashtags = ImmutableSet.copyOf(parser.hashtags);
+      } else {
+        hashtags = ImmutableSet.of();
+      }
       ImmutableSetMultimap.Builder<ReviewerState, Account.Id> reviewers =
           ImmutableSetMultimap.builder();
       for (Map.Entry<Account.Id, ReviewerState> e
@@ -569,23 +298,21 @@
         reviewers.put(e.getValue(), e.getKey());
       }
       this.reviewers = reviewers.build();
+      this.allPastReviewers = ImmutableList.copyOf(parser.allPastReviewers);
 
       submitRecords = ImmutableList.copyOf(parser.submitRecords);
-    } catch (ParseException e1) {
-      // TODO(yyonas): figure out how to handle this exception
-      throw new IOException(e1);
-    } finally {
-      walk.release();
     }
   }
 
-  private void loadDefaults() {
+  @Override
+  protected void loadDefaults() {
     approvals = ImmutableListMultimap.of();
     reviewers = ImmutableSetMultimap.of();
     submitRecords = ImmutableList.of();
     changeMessages = ImmutableListMultimap.of();
     commentsForBase = ImmutableListMultimap.of();
     commentsForPS = ImmutableListMultimap.of();
+    hashtags = ImmutableSet.of();
   }
 
   @Override
@@ -594,7 +321,7 @@
         getClass().getSimpleName() + " is read-only");
   }
 
-  private static Project.NameKey getProjectName(Change change) {
+  static Project.NameKey getProjectName(Change change) {
     return change.getProject();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
new file mode 100644
index 0000000..b5b3c74
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -0,0 +1,436 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.GERRIT_PLACEHOLDER_HOST;
+
+import com.google.common.base.Enums;
+import com.google.common.base.Optional;
+import com.google.common.base.Splitter;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Table;
+import com.google.common.collect.Tables;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.LabelId;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.util.LabelVote;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.FooterKey;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.RawParseUtils;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+class ChangeNotesParser implements AutoCloseable {
+  final Map<Account.Id, ReviewerState> reviewers;
+  final List<Account.Id> allPastReviewers;
+  final List<SubmitRecord> submitRecords;
+  final Multimap<PatchSet.Id, PatchLineComment> commentsForPs;
+  final Multimap<PatchSet.Id, PatchLineComment> commentsForBase;
+  NoteMap commentNoteMap;
+  Change.Status status;
+  Set<String> hashtags;
+
+  private final Change.Id changeId;
+  private final ObjectId tip;
+  private final RevWalk walk;
+  private final Repository repo;
+  private final Map<PatchSet.Id,
+      Table<Account.Id, String, Optional<PatchSetApproval>>> approvals;
+  private final Multimap<PatchSet.Id, ChangeMessage> changeMessages;
+
+  ChangeNotesParser(Change change, ObjectId tip, RevWalk walk,
+      GitRepositoryManager repoManager) throws RepositoryNotFoundException,
+      IOException {
+    this.changeId = change.getId();
+    this.tip = tip;
+    this.walk = walk;
+    this.repo =
+        repoManager.openMetadataRepository(ChangeNotes.getProjectName(change));
+    approvals = Maps.newHashMap();
+    reviewers = Maps.newLinkedHashMap();
+    allPastReviewers = Lists.newArrayList();
+    submitRecords = Lists.newArrayListWithExpectedSize(1);
+    changeMessages = LinkedListMultimap.create();
+    commentsForPs = ArrayListMultimap.create();
+    commentsForBase = ArrayListMultimap.create();
+  }
+
+  @Override
+  public void close() {
+    repo.close();
+  }
+
+  void parseAll() throws ConfigInvalidException, IOException {
+    walk.markStart(walk.parseCommit(tip));
+    for (RevCommit commit : walk) {
+      parse(commit);
+    }
+    parseComments();
+    allPastReviewers.addAll(reviewers.keySet());
+    pruneReviewers();
+  }
+
+  ImmutableListMultimap<PatchSet.Id, PatchSetApproval>
+      buildApprovals() {
+    Multimap<PatchSet.Id, PatchSetApproval> result =
+        ArrayListMultimap.create(approvals.keySet().size(), 3);
+    for (Table<?, ?, Optional<PatchSetApproval>> curr
+        : approvals.values()) {
+      for (PatchSetApproval psa : Optional.presentInstances(curr.values())) {
+        result.put(psa.getPatchSetId(), psa);
+      }
+    }
+    for (Collection<PatchSetApproval> v : result.asMap().values()) {
+      Collections.sort((List<PatchSetApproval>) v, ChangeNotes.PSA_BY_TIME);
+    }
+    return ImmutableListMultimap.copyOf(result);
+  }
+
+  ImmutableListMultimap<PatchSet.Id, ChangeMessage> buildMessages() {
+    for (Collection<ChangeMessage> v : changeMessages.asMap().values()) {
+      Collections.sort((List<ChangeMessage>) v, ChangeNotes.MESSAGE_BY_TIME);
+    }
+    return ImmutableListMultimap.copyOf(changeMessages);
+  }
+
+  private void parse(RevCommit commit) throws ConfigInvalidException {
+    if (status == null) {
+      status = parseStatus(commit);
+    }
+    PatchSet.Id psId = parsePatchSetId(commit);
+    Account.Id accountId = parseIdent(commit);
+    parseChangeMessage(psId, accountId, commit);
+    parseHashtags(commit);
+
+
+    if (submitRecords.isEmpty()) {
+      // Only parse the most recent set of submit records; any older ones are
+      // still there, but not currently used.
+      parseSubmitRecords(commit.getFooterLines(FOOTER_SUBMITTED_WITH));
+    }
+
+    for (String line : commit.getFooterLines(FOOTER_LABEL)) {
+      parseApproval(psId, accountId, commit, line);
+    }
+
+    for (ReviewerState state : ReviewerState.values()) {
+      for (String line : commit.getFooterLines(state.getFooterKey())) {
+        parseReviewer(state, line);
+      }
+    }
+  }
+
+  private void parseHashtags(RevCommit commit) throws ConfigInvalidException {
+    // Commits are parsed in reverse order and only the last set of hashtags should be used.
+    if (hashtags != null) {
+      return;
+    }
+    List<String> hashtagsLines = commit.getFooterLines(FOOTER_HASHTAGS);
+    if (hashtagsLines.isEmpty()) {
+      return;
+    } else if (hashtagsLines.size() > 1) {
+      throw expectedOneFooter(FOOTER_HASHTAGS, hashtagsLines);
+    } else if (hashtagsLines.get(0).isEmpty()) {
+      hashtags = ImmutableSet.of();
+    } else {
+      hashtags = Sets.newHashSet(Splitter.on(',').split(hashtagsLines.get(0)));
+    }
+  }
+
+  private Change.Status parseStatus(RevCommit commit)
+      throws ConfigInvalidException {
+    List<String> statusLines = commit.getFooterLines(FOOTER_STATUS);
+    if (statusLines.isEmpty()) {
+      return null;
+    } else if (statusLines.size() > 1) {
+      throw expectedOneFooter(FOOTER_STATUS, statusLines);
+    }
+    Optional<Change.Status> status = Enums.getIfPresent(
+        Change.Status.class, statusLines.get(0).toUpperCase());
+    if (!status.isPresent()) {
+      throw invalidFooter(FOOTER_STATUS, statusLines.get(0));
+    }
+    return status.get();
+  }
+
+  private PatchSet.Id parsePatchSetId(RevCommit commit)
+      throws ConfigInvalidException {
+    List<String> psIdLines = commit.getFooterLines(FOOTER_PATCH_SET);
+    if (psIdLines.size() != 1) {
+      throw expectedOneFooter(FOOTER_PATCH_SET, psIdLines);
+    }
+    Integer psId = Ints.tryParse(psIdLines.get(0));
+    if (psId == null) {
+      throw invalidFooter(FOOTER_PATCH_SET, psIdLines.get(0));
+    }
+    return new PatchSet.Id(changeId, psId);
+  }
+
+  private void parseChangeMessage(PatchSet.Id psId, Account.Id accountId,
+      RevCommit commit) {
+    byte[] raw = commit.getRawBuffer();
+    int size = raw.length;
+    Charset enc = RawParseUtils.parseEncoding(raw);
+
+    int subjectStart = RawParseUtils.commitMessage(raw, 0);
+    if (subjectStart < 0 || subjectStart >= size) {
+      return;
+    }
+
+    int subjectEnd = RawParseUtils.endOfParagraph(raw, subjectStart);
+    if (subjectEnd == size) {
+      return;
+    }
+
+    int changeMessageStart;
+
+    if (raw[subjectEnd] == '\n') {
+      changeMessageStart = subjectEnd + 2; //\n\n ends paragraph
+    } else if (raw[subjectEnd] == '\r') {
+      changeMessageStart = subjectEnd + 4; //\r\n\r\n ends paragraph
+    } else {
+      return;
+    }
+
+    int ptr = size - 1;
+    int changeMessageEnd = -1;
+    while(ptr > changeMessageStart) {
+      ptr = RawParseUtils.prevLF(raw, ptr, '\r');
+      if (ptr == -1) {
+        break;
+      }
+      if (raw[ptr] == '\n') {
+        changeMessageEnd = ptr - 1;
+        break;
+      } else if (raw[ptr] == '\r') {
+        changeMessageEnd = ptr - 3;
+        break;
+      }
+    }
+
+    if (ptr <= changeMessageStart) {
+      return;
+    }
+
+    String changeMsgString = RawParseUtils.decode(enc, raw,
+        changeMessageStart, changeMessageEnd + 1);
+    ChangeMessage changeMessage = new ChangeMessage(
+        new ChangeMessage.Key(psId.getParentKey(), commit.name()),
+        accountId,
+        new Timestamp(commit.getCommitterIdent().getWhen().getTime()),
+        psId);
+    changeMessage.setMessage(changeMsgString);
+    changeMessages.put(psId, changeMessage);
+  }
+
+  private void parseComments()
+      throws IOException, ConfigInvalidException {
+    commentNoteMap = CommentsInNotesUtil.parseCommentsFromNotes(repo,
+        ChangeNoteUtil.changeRefName(changeId), walk, changeId,
+        commentsForBase, commentsForPs, PatchLineComment.Status.PUBLISHED);
+  }
+
+  private void parseApproval(PatchSet.Id psId, Account.Id accountId,
+      RevCommit commit, String line) throws ConfigInvalidException {
+    Table<Account.Id, String, Optional<PatchSetApproval>> curr =
+        approvals.get(psId);
+    if (curr == null) {
+      curr = Tables.newCustomTable(
+          Maps.<Account.Id, Map<String, Optional<PatchSetApproval>>>
+              newHashMapWithExpectedSize(2),
+          new Supplier<Map<String, Optional<PatchSetApproval>>>() {
+            @Override
+            public Map<String, Optional<PatchSetApproval>> get() {
+              return Maps.newLinkedHashMap();
+            }
+          });
+      approvals.put(psId, curr);
+    }
+
+    if (line.startsWith("-")) {
+      String label = line.substring(1);
+      if (!curr.contains(accountId, label)) {
+        curr.put(accountId, label, Optional.<PatchSetApproval> absent());
+      }
+    } else {
+      LabelVote l;
+      try {
+        l = LabelVote.parseWithEquals(line);
+      } catch (IllegalArgumentException e) {
+        ConfigInvalidException pe =
+            parseException("invalid %s: %s", FOOTER_LABEL, line);
+        pe.initCause(e);
+        throw pe;
+      }
+      if (!curr.contains(accountId, l.label())) {
+        curr.put(accountId, l.label(), Optional.of(new PatchSetApproval(
+            new PatchSetApproval.Key(
+                psId,
+                accountId,
+                new LabelId(l.label())),
+            l.value(),
+            new Timestamp(commit.getCommitterIdent().getWhen().getTime()))));
+      }
+    }
+  }
+
+  private void parseSubmitRecords(List<String> lines)
+      throws ConfigInvalidException {
+    SubmitRecord rec = null;
+
+    for (String line : lines) {
+      int c = line.indexOf(": ");
+      if (c < 0) {
+        rec = new SubmitRecord();
+        submitRecords.add(rec);
+        int s = line.indexOf(' ');
+        String statusStr = s >= 0 ? line.substring(0, s) : line;
+        Optional<SubmitRecord.Status> status =
+            Enums.getIfPresent(SubmitRecord.Status.class, statusStr);
+        checkFooter(status.isPresent(), FOOTER_SUBMITTED_WITH, line);
+        rec.status = status.get();
+        if (s >= 0) {
+          rec.errorMessage = line.substring(s);
+        }
+      } else {
+        checkFooter(rec != null, FOOTER_SUBMITTED_WITH, line);
+        SubmitRecord.Label label = new SubmitRecord.Label();
+        if (rec.labels == null) {
+          rec.labels = Lists.newArrayList();
+        }
+        rec.labels.add(label);
+
+        Optional<SubmitRecord.Label.Status> status = Enums.getIfPresent(
+            SubmitRecord.Label.Status.class, line.substring(0, c));
+        checkFooter(status.isPresent(), FOOTER_SUBMITTED_WITH, line);
+        label.status = status.get();
+        int c2 = line.indexOf(": ", c + 2);
+        if (c2 >= 0) {
+          label.label = line.substring(c + 2, c2);
+          PersonIdent ident =
+              RawParseUtils.parsePersonIdent(line.substring(c2 + 2));
+          checkFooter(ident != null, FOOTER_SUBMITTED_WITH, line);
+          label.appliedBy = parseIdent(ident);
+        } else {
+          label.label = line.substring(c + 2);
+        }
+      }
+    }
+  }
+
+  private Account.Id parseIdent(RevCommit commit)
+      throws ConfigInvalidException {
+    return parseIdent(commit.getAuthorIdent());
+  }
+
+  private Account.Id parseIdent(PersonIdent ident)
+      throws ConfigInvalidException {
+    String email = ident.getEmailAddress();
+    int at = email.indexOf('@');
+    if (at >= 0) {
+      String host = email.substring(at + 1, email.length());
+      Integer id = Ints.tryParse(email.substring(0, at));
+      if (id != null && host.equals(GERRIT_PLACEHOLDER_HOST)) {
+        return new Account.Id(id);
+      }
+    }
+    throw parseException("invalid identity, expected <id>@%s: %s",
+      GERRIT_PLACEHOLDER_HOST, email);
+  }
+
+  private void parseReviewer(ReviewerState state, String line)
+      throws ConfigInvalidException {
+    PersonIdent ident = RawParseUtils.parsePersonIdent(line);
+    if (ident == null) {
+      throw invalidFooter(state.getFooterKey(), line);
+    }
+    Account.Id accountId = parseIdent(ident);
+    if (!reviewers.containsKey(accountId)) {
+      reviewers.put(accountId, state);
+    }
+  }
+
+  private void pruneReviewers() {
+    Iterator<Map.Entry<Account.Id, ReviewerState>> rit =
+        reviewers.entrySet().iterator();
+    while (rit.hasNext()) {
+      Map.Entry<Account.Id, ReviewerState> e = rit.next();
+      if (e.getValue() == ReviewerState.REMOVED) {
+        rit.remove();
+        for (Table<Account.Id, ?, ?> curr : approvals.values()) {
+          curr.rowKeySet().remove(e.getKey());
+        }
+      }
+    }
+  }
+
+  private ConfigInvalidException expectedOneFooter(FooterKey footer,
+      List<String> actual) {
+    return parseException("missing or multiple %s: %s",
+        footer.getName(), actual);
+  }
+
+  private ConfigInvalidException invalidFooter(FooterKey footer,
+      String actual) {
+    return parseException("invalid %s: %s", footer.getName(), actual);
+  }
+
+  private void checkFooter(boolean expr, FooterKey footer, String actual)
+      throws ConfigInvalidException {
+    if (!expr) {
+      throw invalidFooter(footer, actual);
+    }
+  }
+
+  private ConfigInvalidException parseException(String fmt, Object... args) {
+    return ChangeNotes.parseException(changeId, fmt, args);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
new file mode 100644
index 0000000..76dfdc8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
@@ -0,0 +1,332 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
+import static com.google.gerrit.server.notedb.CommentsInNotesUtil.getCommentPsId;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+public class ChangeRebuilder {
+  private static final long TS_WINDOW_MS =
+      TimeUnit.MILLISECONDS.convert(1, TimeUnit.SECONDS);
+
+  private final Provider<ReviewDb> dbProvider;
+  private final ChangeControl.GenericFactory controlFactory;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final PatchListCache patchListCache;
+  private final ChangeUpdate.Factory updateFactory;
+  private final ChangeDraftUpdate.Factory draftUpdateFactory;
+
+  @Inject
+  ChangeRebuilder(Provider<ReviewDb> dbProvider,
+      ChangeControl.GenericFactory controlFactory,
+      IdentifiedUser.GenericFactory userFactory,
+      PatchListCache patchListCache,
+      ChangeUpdate.Factory updateFactory,
+      ChangeDraftUpdate.Factory draftUpdateFactory) {
+    this.dbProvider = dbProvider;
+    this.controlFactory = controlFactory;
+    this.userFactory = userFactory;
+    this.patchListCache = patchListCache;
+    this.updateFactory = updateFactory;
+    this.draftUpdateFactory = draftUpdateFactory;
+  }
+
+  public ListenableFuture<?> rebuildAsync(final Change change,
+      ListeningExecutorService executor, final BatchRefUpdate bru,
+      final BatchRefUpdate bruForDrafts, final Repository changeRepo,
+      final Repository allUsersRepo) {
+    return executor.submit(new Callable<Void>() {
+        @Override
+      public Void call() throws Exception {
+        rebuild(change, bru, bruForDrafts, changeRepo, allUsersRepo);
+        return null;
+      }
+    });
+  }
+
+  public void rebuild(Change change, BatchRefUpdate bru,
+      BatchRefUpdate bruForDrafts, Repository changeRepo,
+      Repository allUsersRepo) throws NoSuchChangeException, IOException,
+      OrmException {
+    deleteRef(change, changeRepo);
+    ReviewDb db = dbProvider.get();
+    Change.Id changeId = change.getId();
+
+    // We will rebuild all events, except for draft comments, in buckets based
+    // on author and timestamp. However, all draft comments for a given change
+    // and author will be written as one commit in the notedb.
+    List<Event> events = Lists.newArrayList();
+    Multimap<Account.Id, PatchLineCommentEvent> draftCommentEvents =
+        ArrayListMultimap.create();
+
+    for (PatchSet ps : db.patchSets().byChange(changeId)) {
+      events.add(new PatchSetEvent(ps));
+      for (PatchLineComment c : db.patchComments().byPatchSet(ps.getId())) {
+        PatchLineCommentEvent e =
+            new PatchLineCommentEvent(c, change, ps, patchListCache);
+        if (c.getStatus() == Status.PUBLISHED) {
+          events.add(e);
+        } else {
+          draftCommentEvents.put(c.getAuthor(), e);
+        }
+      }
+    }
+
+    for (PatchSetApproval psa : db.patchSetApprovals().byChange(changeId)) {
+      events.add(new ApprovalEvent(psa));
+    }
+
+
+    Collections.sort(events);
+    BatchMetaDataUpdate batch = null;
+    ChangeUpdate update = null;
+    for (Event e : events) {
+      if (!sameUpdate(e, update)) {
+        if (update != null) {
+          writeToBatch(batch, update, changeRepo);
+        }
+        IdentifiedUser user = userFactory.create(dbProvider, e.who);
+        update = updateFactory.create(
+            controlFactory.controlFor(change, user), e.when);
+        update.setPatchSetId(e.psId);
+        if (batch == null) {
+          batch = update.openUpdateInBatch(bru);
+        }
+      }
+      e.apply(update);
+    }
+    if (batch != null) {
+      if (update != null) {
+        writeToBatch(batch, update, changeRepo);
+      }
+
+      // Since the BatchMetaDataUpdates generated by all ChangeRebuilders on a
+      // given project are backed by the same BatchRefUpdate, we need to
+      // synchronize on the BatchRefUpdate. Therefore, since commit on a
+      // BatchMetaDataUpdate is the only method that modifies a BatchRefUpdate,
+      // we can just synchronize this call.
+      synchronized (bru) {
+        batch.commit();
+      }
+    }
+
+    for (Account.Id author : draftCommentEvents.keys()) {
+      IdentifiedUser user = userFactory.create(dbProvider, author);
+      ChangeDraftUpdate draftUpdate = null;
+      BatchMetaDataUpdate batchForDrafts = null;
+      for (PatchLineCommentEvent e : draftCommentEvents.get(author)) {
+        if (draftUpdate == null) {
+          draftUpdate = draftUpdateFactory.create(
+              controlFactory.controlFor(change, user), e.when);
+          draftUpdate.setPatchSetId(e.psId);
+          batchForDrafts = draftUpdate.openUpdateInBatch(bruForDrafts);
+        }
+        e.applyDraft(draftUpdate);
+      }
+      writeToBatch(batchForDrafts, draftUpdate, allUsersRepo);
+      synchronized(bruForDrafts) {
+        batchForDrafts.commit();
+      }
+    }
+  }
+
+  private void deleteRef(Change change, Repository changeRepo)
+      throws IOException {
+    String refName = ChangeNoteUtil.changeRefName(change.getId());
+    RefUpdate ru = changeRepo.updateRef(refName, true);
+    ru.setForceUpdate(true);
+    RefUpdate.Result result = ru.delete();
+    switch (result) {
+      case FORCED:
+      case NEW:
+      case NO_CHANGE:
+        break;
+      default:
+        throw new IOException(
+            String.format("Failed to delete ref %s: %s", refName, result));
+    }
+  }
+
+  private void writeToBatch(BatchMetaDataUpdate batch,
+      AbstractChangeUpdate update, Repository repo) throws IOException,
+      OrmException {
+    try (ObjectInserter inserter = repo.newObjectInserter()) {
+      update.setInserter(inserter);
+      update.writeCommit(batch);
+    }
+  }
+
+  private static long round(Date when) {
+    return when.getTime() / TS_WINDOW_MS;
+  }
+
+  private static boolean sameUpdate(Event event, ChangeUpdate update) {
+    return update != null
+        && round(event.when) == round(update.getWhen())
+        && event.who.equals(update.getUser().getAccountId())
+        && event.psId.equals(update.getPatchSetId());
+  }
+
+  private abstract static class Event implements Comparable<Event> {
+    final PatchSet.Id psId;
+    final Account.Id who;
+    final Timestamp when;
+
+    protected Event(PatchSet.Id psId, Account.Id who, Timestamp when) {
+      this.psId = psId;
+      this.who = who;
+      this.when = when;
+    }
+
+    protected void checkUpdate(AbstractChangeUpdate update) {
+      checkState(Objects.equals(update.getPatchSetId(), psId),
+          "cannot apply event for %s to update for %s",
+          update.getPatchSetId(), psId);
+      checkState(when.getTime() - update.getWhen().getTime() <= TS_WINDOW_MS,
+          "event at %s outside update window starting at %s",
+          when, update.getWhen());
+      checkState(Objects.equals(update.getUser().getAccountId(), who),
+          "cannot apply event by %s to update by %s",
+          who, update.getUser().getAccountId());
+    }
+
+    abstract void apply(ChangeUpdate update) throws OrmException;
+
+    @Override
+    public int compareTo(Event other) {
+      return ComparisonChain.start()
+          // TODO(dborowitz): Smarter bucketing: pick a bucket start time T and
+          // include all events up to T + TS_WINDOW_MS but no further.
+          // Interleaving different authors complicates things.
+          .compare(round(when), round(other.when))
+          .compare(who.get(), other.who.get())
+          .compare(psId.get(), other.psId.get())
+          .result();
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this)
+          .add("psId", psId)
+          .add("who", who)
+          .add("when", when)
+          .toString();
+    }
+  }
+
+  private static class ApprovalEvent extends Event {
+    private PatchSetApproval psa;
+
+    ApprovalEvent(PatchSetApproval psa) {
+      super(psa.getPatchSetId(), psa.getAccountId(), psa.getGranted());
+      this.psa = psa;
+    }
+
+    @Override
+    void apply(ChangeUpdate update) {
+      checkUpdate(update);
+      update.putApproval(psa.getLabel(), psa.getValue());
+    }
+  }
+
+  private static class PatchSetEvent extends Event {
+    private final PatchSet ps;
+
+    PatchSetEvent(PatchSet ps) {
+      super(ps.getId(), ps.getUploader(), ps.getCreatedOn());
+      this.ps = ps;
+    }
+
+    @Override
+    void apply(ChangeUpdate update) {
+      checkUpdate(update);
+      if (ps.getPatchSetId() == 1) {
+        update.setSubject("Create change");
+      } else {
+        update.setSubject("Create patch set " + ps.getPatchSetId());
+      }
+    }
+  }
+
+  private static class PatchLineCommentEvent extends Event {
+    public final PatchLineComment c;
+    private final Change change;
+    private final PatchSet ps;
+    private final PatchListCache cache;
+
+    PatchLineCommentEvent(PatchLineComment c, Change change, PatchSet ps,
+        PatchListCache cache) {
+      super(getCommentPsId(c), c.getAuthor(), c.getWrittenOn());
+      this.c = c;
+      this.change = change;
+      this.ps = ps;
+      this.cache = cache;
+    }
+
+    @Override
+    void apply(ChangeUpdate update) throws OrmException {
+      checkUpdate(update);
+      if (c.getRevId() == null) {
+        setCommentRevId(c, cache, change, ps);
+      }
+      update.insertComment(c);
+    }
+
+    void applyDraft(ChangeDraftUpdate draftUpdate) throws OrmException {
+      if (c.getRevId() == null) {
+        setCommentRevId(c, cache, change, ps);
+      }
+      draftUpdate.insertComment(c);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index be3a44d..7302425 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
@@ -22,6 +23,7 @@
 import static com.google.gerrit.server.notedb.CommentsInNotesUtil.getCommentPsId;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
 import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
@@ -31,11 +33,12 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.project.ChangeControl;
@@ -58,14 +61,17 @@
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
- * A single delta to apply atomically to a change.
+ * A delta to apply to a change.
  * <p>
- * This delta becomes a single commit on the notes branch, so there are
- * limitations on the set of modifications that can be handled in a single
- * update. In particular, there is a single author and timestamp for each
- * update.
+ * This delta will become two unique commits: one in the AllUsers repo that will
+ * contain the draft comments on this change and one in the notes branch that
+ * will contain approvals, reviewers, change status, subject, submit records,
+ * the change message, and published comments. There are limitations on the set
+ * of modifications that can be handled in a single update. In particular, there
+ * is a single author and timestamp for each update.
  * <p>
  * This class is not thread-safe.
  */
@@ -87,35 +93,45 @@
   private final CommentsInNotesUtil commentsUtil;
   private List<PatchLineComment> commentsForBase;
   private List<PatchLineComment> commentsForPs;
+  private Set<String> hashtags;
   private String changeMessage;
+  private ChangeNotes notes;
+
+  private final ChangeDraftUpdate.Factory draftUpdateFactory;
+  private ChangeDraftUpdate draftUpdate;
 
   @AssistedInject
   private ChangeUpdate(
       @GerritPersonIdent PersonIdent serverIdent,
+      @AnonymousCowardName String anonymousCowardName,
       GitRepositoryManager repoManager,
       NotesMigration migration,
       AccountCache accountCache,
       MetaDataUpdate.User updateFactory,
+      ChangeDraftUpdate.Factory draftUpdateFactory,
       ProjectCache projectCache,
-      IdentifiedUser user,
       @Assisted ChangeControl ctl,
       CommentsInNotesUtil commentsUtil) {
-    this(serverIdent, repoManager, migration, accountCache, updateFactory,
+    this(serverIdent, anonymousCowardName, repoManager, migration, accountCache,
+        updateFactory, draftUpdateFactory,
         projectCache, ctl, serverIdent.getWhen(), commentsUtil);
   }
 
   @AssistedInject
   private ChangeUpdate(
       @GerritPersonIdent PersonIdent serverIdent,
+      @AnonymousCowardName String anonymousCowardName,
       GitRepositoryManager repoManager,
       NotesMigration migration,
       AccountCache accountCache,
       MetaDataUpdate.User updateFactory,
+      ChangeDraftUpdate.Factory draftUpdateFactory,
       ProjectCache projectCache,
       @Assisted ChangeControl ctl,
       @Assisted Date when,
       CommentsInNotesUtil commentsUtil) {
-    this(serverIdent, repoManager, migration, accountCache, updateFactory, ctl,
+    this(serverIdent, anonymousCowardName, repoManager, migration, accountCache,
+        updateFactory, draftUpdateFactory, ctl,
         when,
         projectCache.get(getProjectName(ctl)).getLabelTypes().nameComparator(),
         commentsUtil);
@@ -128,15 +144,19 @@
   @AssistedInject
   private ChangeUpdate(
       @GerritPersonIdent PersonIdent serverIdent,
+      @AnonymousCowardName String anonymousCowardName,
       GitRepositoryManager repoManager,
       NotesMigration migration,
       AccountCache accountCache,
       MetaDataUpdate.User updateFactory,
+      ChangeDraftUpdate.Factory draftUpdateFactory,
       @Assisted ChangeControl ctl,
       @Assisted Date when,
       @Assisted Comparator<String> labelNameComparator,
       CommentsInNotesUtil commentsUtil) {
-    super(migration, repoManager, updateFactory, ctl, serverIdent, when);
+    super(migration, repoManager, updateFactory, ctl, serverIdent,
+        anonymousCowardName, when);
+    this.draftUpdateFactory = draftUpdateFactory;
     this.accountCache = accountCache;
     this.commentsUtil = commentsUtil;
     this.approvals = Maps.newTreeMap(labelNameComparator);
@@ -174,20 +194,152 @@
     this.changeMessage = changeMessage;
   }
 
-  public void putComment(PatchLineComment comment) {
-    checkArgument(psId != null,
-        "setPatchSetId must be called before putComment");
-    checkArgument(getCommentPsId(comment).equals(psId),
-        "Comment on %s doesn't match previous patch set %s",
-        getCommentPsId(comment), psId);
-    checkArgument(comment.getRevId() != null);
-    if (comment.getSide() == 0) {
-      commentsForBase.add(comment);
+  public void insertComment(PatchLineComment comment) throws OrmException {
+    if (comment.getStatus() == Status.DRAFT) {
+      insertDraftComment(comment);
     } else {
-      commentsForPs.add(comment);
+      insertPublishedComment(comment);
     }
   }
 
+  public void upsertComment(PatchLineComment comment) throws OrmException {
+    if (comment.getStatus() == Status.DRAFT) {
+      upsertDraftComment(comment);
+    } else {
+      deleteDraftCommentIfPresent(comment);
+      upsertPublishedComment(comment);
+    }
+  }
+
+  public void updateComment(PatchLineComment comment) throws OrmException {
+    if (comment.getStatus() == Status.DRAFT) {
+      updateDraftComment(comment);
+    } else {
+      deleteDraftCommentIfPresent(comment);
+      updatePublishedComment(comment);
+    }
+  }
+
+  public void deleteComment(PatchLineComment comment) throws OrmException {
+    if (comment.getStatus() == Status.DRAFT) {
+      deleteDraftComment(comment);
+    } else {
+      throw new IllegalArgumentException("Cannot delete a published comment.");
+    }
+  }
+
+  private void insertPublishedComment(PatchLineComment c) throws OrmException {
+    verifyComment(c);
+    if (notes == null) {
+      notes = getChangeNotes().load();
+    }
+    if (migration.readChanges()) {
+      checkArgument(!notes.containsComment(c),
+          "A comment already exists with the same key as the following comment,"
+          + " so we cannot insert this comment: %s", c);
+    }
+    if (c.getSide() == 0) {
+      commentsForBase.add(c);
+    } else {
+      commentsForPs.add(c);
+    }
+  }
+
+  private void insertDraftComment(PatchLineComment c) throws OrmException {
+    createDraftUpdateIfNull(c);
+    draftUpdate.insertComment(c);
+  }
+
+  private void upsertPublishedComment(PatchLineComment c) throws OrmException {
+    verifyComment(c);
+    if (notes == null) {
+      notes = getChangeNotes().load();
+    }
+    // This could allow callers to update a published comment if migration.write
+    // is on and migration.readComments is off because we will not be able to
+    // verify that the comment didn't already exist as a published comment
+    // since we don't have a ReviewDb.
+    if (migration.readChanges()) {
+      checkArgument(!notes.containsCommentPublished(c),
+          "Cannot update a comment that has already been published and saved");
+    }
+    if (c.getSide() == 0) {
+      commentsForBase.add(c);
+    } else {
+      commentsForPs.add(c);
+    }
+  }
+
+  private void upsertDraftComment(PatchLineComment c) {
+    createDraftUpdateIfNull(c);
+    draftUpdate.upsertComment(c);
+  }
+
+  private void updatePublishedComment(PatchLineComment c) throws OrmException {
+    verifyComment(c);
+    if (notes == null) {
+      notes = getChangeNotes().load();
+    }
+    // See comment above in upsertPublishedComment() about potential risk with
+    // this check.
+    if (migration.readChanges()) {
+      checkArgument(!notes.containsCommentPublished(c),
+          "Cannot update a comment that has already been published and saved");
+    }
+    if (c.getSide() == 0) {
+      commentsForBase.add(c);
+    } else {
+      commentsForPs.add(c);
+    }
+  }
+
+  private void updateDraftComment(PatchLineComment c) throws OrmException {
+    createDraftUpdateIfNull(c);
+    draftUpdate.updateComment(c);
+  }
+
+  private void deleteDraftComment(PatchLineComment c) throws OrmException {
+    createDraftUpdateIfNull(c);
+    draftUpdate.deleteComment(c);
+  }
+
+  private void deleteDraftCommentIfPresent(PatchLineComment c)
+      throws OrmException {
+    createDraftUpdateIfNull(c);
+    draftUpdate.deleteCommentIfPresent(c);
+  }
+
+  private void createDraftUpdateIfNull(PatchLineComment c) {
+    if (draftUpdate == null) {
+      draftUpdate = draftUpdateFactory.create(ctl, when);
+      if (psId != null) {
+        draftUpdate.setPatchSetId(psId);
+      } else {
+        draftUpdate.setPatchSetId(getCommentPsId(c));
+      }
+    }
+  }
+
+  private void verifyComment(PatchLineComment c) {
+    checkArgument(psId != null,
+        "setPatchSetId must be called first");
+    checkArgument(getCommentPsId(c).equals(psId),
+        "Comment on %s doesn't match previous patch set %s",
+        getCommentPsId(c), psId);
+    checkArgument(c.getRevId() != null);
+    checkArgument(c.getStatus() == Status.PUBLISHED,
+        "Cannot add a draft comment to a ChangeUpdate. Use a ChangeDraftUpdate"
+        + " for draft comments");
+    checkArgument(c.getAuthor().equals(getUser().getAccountId()),
+        "The author for the following comment does not match the author of"
+        + " this ChangeDraftUpdate (%s): %s", getUser().getAccountId(), c);
+
+  }
+
+  public void setHashtags(Set<String> hashtags) {
+    this.hashtags = hashtags;
+  }
+
   public void putReviewer(Account.Id reviewer, ReviewerState type) {
     checkArgument(type != ReviewerState.REMOVED, "invalid ReviewerType");
     reviewers.put(reviewer, type);
@@ -235,14 +387,10 @@
   public RevCommit commit() throws IOException {
     BatchMetaDataUpdate batch = openUpdate();
     try {
-      CommitBuilder builder = new CommitBuilder();
-      if (migration.write()) {
-        ObjectId treeId = storeCommentsInNotes();
-        if (treeId != null) {
-          builder.setTreeId(treeId);
-        }
+      writeCommit(batch);
+      if (draftUpdate != null) {
+        draftUpdate.commit();
       }
-      batch.write(builder);
       RevCommit c = batch.commit();
       return c;
     } catch (OrmException e) {
@@ -253,6 +401,19 @@
   }
 
   @Override
+  public void writeCommit(BatchMetaDataUpdate batch) throws OrmException,
+      IOException {
+    CommitBuilder builder = new CommitBuilder();
+    if (migration.writeChanges()) {
+      ObjectId treeId = storeCommentsInNotes();
+      if (treeId != null) {
+        builder.setTreeId(treeId);
+      }
+    }
+    batch.write(this, builder);
+  }
+
+  @Override
   protected String getRefName() {
     return ChangeNoteUtil.changeRefName(getChange().getId());
   }
@@ -285,6 +446,10 @@
       addFooter(msg, FOOTER_STATUS, status.name().toLowerCase());
     }
 
+    if (hashtags != null) {
+      addFooter(msg, FOOTER_HASHTAGS, Joiner.on(",").join(hashtags));
+    }
+
     for (Map.Entry<Account.Id, ReviewerState> e : reviewers.entrySet()) {
       Account account = accountCache.get(e.getKey()).getAccount();
       PersonIdent ident = newIdent(account, when);
@@ -297,8 +462,8 @@
       if (!e.getValue().isPresent()) {
         addFooter(msg, FOOTER_LABEL, '-', e.getKey());
       } else {
-        addFooter(msg, FOOTER_LABEL,
-            new LabelVote(e.getKey(), e.getValue().get()).formatWithEquals());
+        addFooter(msg, FOOTER_LABEL, LabelVote.create(
+            e.getKey(), e.getValue().get()).formatWithEquals());
       }
     }
 
@@ -338,12 +503,14 @@
 
   private boolean isEmpty() {
     return approvals.isEmpty()
-        && reviewers.isEmpty()
+        && changeMessage == null
         && commentsForBase.isEmpty()
         && commentsForPs.isEmpty()
+        && reviewers.isEmpty()
         && status == null
+        && subject == null
         && submitRecords == null
-        && changeMessage == null;
+        && hashtags == null;
   }
 
   private static StringBuilder addFooter(StringBuilder sb, FooterKey footer) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java
index ede979fe..34ecbc0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java
@@ -18,11 +18,12 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.GERRIT_PLACEHOLDER_HOST;
 import static com.google.gerrit.server.notedb.ChangeNotes.parseException;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.CommentRange;
@@ -33,13 +34,14 @@
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -48,11 +50,11 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.GitDateFormatter;
+import org.eclipse.jgit.util.GitDateFormatter.Format;
 import org.eclipse.jgit.util.GitDateParser;
 import org.eclipse.jgit.util.MutableInteger;
 import org.eclipse.jgit.util.QuotedString;
 import org.eclipse.jgit.util.RawParseUtils;
-import org.eclipse.jgit.util.GitDateFormatter.Format;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -69,6 +71,7 @@
  * Utility functions to parse PatchLineComments out of a note byte array and
  * store a list of PatchLineComments in the form of a note (in a byte array).
  **/
+@Singleton
 public class CommentsInNotesUtil {
   private static final String AUTHOR = "Author";
   private static final String BASE_PATCH_SET = "Base-for-patch-set";
@@ -79,6 +82,7 @@
   private static final String PATCH_SET = "Patch-set";
   private static final String REVISION = "Revision";
   private static final String UUID = "UUID";
+  private static final int MAX_NOTE_SZ = 25 << 20;
 
   public static NoteMap parseCommentsFromNotes(Repository repo, String refName,
       RevWalk walk, Change.Id changeId,
@@ -90,14 +94,16 @@
     if (ref == null) {
       return null;
     }
+
+    ObjectReader reader = walk.getObjectReader();
     RevCommit commit = walk.parseCommit(ref.getObjectId());
-    NoteMap noteMap = NoteMap.read(walk.getObjectReader(), commit);
+    NoteMap noteMap = NoteMap.read(reader, commit);
 
     for (Note note: noteMap) {
-      byte[] bytes = walk.getObjectReader().open(
-          note.getData(), Constants.OBJ_BLOB).getBytes();
+      byte[] bytes =
+          reader.open(note.getData(), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
       List<PatchLineComment> result = parseNote(bytes, changeId, status);
-      if ((result == null) || (result.isEmpty())) {
+      if (result == null || result.isEmpty()) {
         continue;
       }
       PatchSet.Id psId = result.get(0).getKey().getParentKey().getParentKey();
@@ -166,7 +172,7 @@
       throw parseException(changeId, "could not parse %s", FILE);
     }
 
-    CommentRange range = parseCommentRange(note, curr, changeId);
+    CommentRange range = parseCommentRange(note, curr);
     if (range == null) {
       throw parseException(changeId, "could not parse %s", COMMENT_RANGE);
     }
@@ -221,17 +227,19 @@
    *    contains a whole comment range, then we return a CommentRange with all
    *    fields set. If the line is not correctly formatted, return null.
    */
-  private static CommentRange parseCommentRange(byte[] note, MutableInteger ptr,
-      Change.Id changeId) throws ConfigInvalidException {
+  private static CommentRange parseCommentRange(byte[] note, MutableInteger ptr) {
     CommentRange range = new CommentRange(-1, -1, -1, -1);
 
     int startLine = RawParseUtils.parseBase10(note, ptr.value, ptr);
     if (startLine == 0) {
-      return null;
+      range.setEndLine(0);
+      ptr.value += 1;
+      return range;
     }
 
     if (note[ptr.value] == '\n') {
       range.setEndLine(startLine);
+      ptr.value += 1;
       return range;
     } else if (note[ptr.value] == ':') {
       range.setStartLine(startLine);
@@ -368,7 +376,7 @@
 
   private PersonIdent newIdent(Account author, Date when) {
     return new PersonIdent(
-        author.getFullName(),
+        new AccountInfo(author).getName(anonymousCowardName),
         author.getId().get() + "@" + GERRIT_PLACEHOLDER_HOST,
         when, serverIdent.getTimeZone());
   }
@@ -389,7 +397,7 @@
   }
 
   private void appendHeaderField(PrintWriter writer,
-      String field, String value) throws IOException {
+      String field, String value) {
     writer.print(field);
     writer.print(": ");
     writer.print(value);
@@ -410,13 +418,15 @@
 
   private final AccountCache accountCache;
   private final PersonIdent serverIdent;
+  private final String anonymousCowardName;
 
-  @VisibleForTesting
   @Inject
   public CommentsInNotesUtil(AccountCache accountCache,
-      @GerritPersonIdent PersonIdent serverIdent) {
+      @GerritPersonIdent PersonIdent serverIdent,
+      @AnonymousCowardName String anonymousCowardName) {
     this.accountCache = accountCache;
     this.serverIdent = serverIdent;
+    this.anonymousCowardName = anonymousCowardName;
   }
 
   /**
@@ -432,8 +442,7 @@
    *            for no comments.
    * @return the note. Null if there are no comments in the list.
    */
-  public byte[] buildNote(List<PatchLineComment> comments)
-      throws OrmException, IOException {
+  public byte[] buildNote(List<PatchLineComment> comments) {
     ByteArrayOutputStream buf = new ByteArrayOutputStream();
     OutputStreamWriter streamWriter = new OutputStreamWriter(buf, UTF_8);
     PrintWriter writer = new PrintWriter(streamWriter);
@@ -454,11 +463,11 @@
       checkArgument(psId.equals(currentPsId),
           "All comments being added must all have the same PatchSet.Id. The"
           + "comment below does not have the same PatchSet.Id as the others "
-          + "(%d).\n%s", psId.toString(), c.toString());
+          + "(%s).\n%s", psId.toString(), c.toString());
       checkArgument(side == c.getSide(),
           "All comments being added must all have the same side. The"
           + "comment below does not have the same side as the others "
-          + "(%d).\n%s", side, c.toString());
+          + "(%s).\n%s", side, c.toString());
       String commentFilename =
           QuotedString.GIT_PATH.quote(c.getKey().getParentKey().getFileName());
 
@@ -517,15 +526,13 @@
 
   public void writeCommentsToNoteMap(NoteMap noteMap,
       List<PatchLineComment> allComments, ObjectInserter inserter)
-        throws OrmException, IOException {
+        throws IOException {
     checkArgument(!allComments.isEmpty(),
         "No comments to write; to delete, use removeNoteFromNoteMap().");
-    ObjectId commitOID =
+    ObjectId commit =
         ObjectId.fromString(allComments.get(0).getRevId().get());
     Collections.sort(allComments, ChangeNotes.PatchLineCommentComparator);
-    byte[] note = buildNote(allComments);
-    ObjectId noteId = inserter.insert(Constants.OBJ_BLOB, note, 0, note.length);
-    noteMap.set(commitOID, noteId);
+    noteMap.set(commit, inserter.insert(OBJ_BLOB, buildNote(allComments)));
   }
 
   public void removeNote(NoteMap noteMap, RevId commitId)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
new file mode 100644
index 0000000..20d6c4a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -0,0 +1,171 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static com.google.gerrit.server.notedb.CommentsInNotesUtil.getCommentPsId;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Table;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+
+/**
+ * View of the draft comments for a single {@link Change} based on the log of
+ * its drafts branch.
+ */
+public class DraftCommentNotes extends AbstractChangeNotes<DraftCommentNotes> {
+  @Singleton
+  public static class Factory {
+    private final GitRepositoryManager repoManager;
+    private final NotesMigration migration;
+    private final AllUsersName draftsProject;
+
+    @VisibleForTesting
+    @Inject
+    public Factory(GitRepositoryManager repoManager,
+        NotesMigration migration,
+        AllUsersNameProvider allUsers) {
+      this.repoManager = repoManager;
+      this.migration = migration;
+      this.draftsProject = allUsers.get();
+    }
+
+    public DraftCommentNotes create(Change.Id changeId, Account.Id accountId) {
+      return new DraftCommentNotes(repoManager, migration, draftsProject,
+          changeId, accountId);
+    }
+  }
+
+  private final AllUsersName draftsProject;
+  private final Account.Id author;
+
+  private final Table<PatchSet.Id, String, PatchLineComment> draftBaseComments;
+  private final Table<PatchSet.Id, String, PatchLineComment> draftPsComments;
+  private NoteMap noteMap;
+
+  DraftCommentNotes(GitRepositoryManager repoManager, NotesMigration migration,
+      AllUsersName draftsProject, Change.Id changeId, Account.Id author) {
+    super(repoManager, migration, changeId);
+    this.draftsProject = draftsProject;
+    this.author = author;
+
+    this.draftBaseComments = HashBasedTable.create();
+    this.draftPsComments = HashBasedTable.create();
+  }
+
+  public NoteMap getNoteMap() {
+    return noteMap;
+  }
+
+  public Account.Id getAuthor() {
+    return author;
+  }
+
+  /**
+   * @return a defensive copy of the table containing all draft comments
+   *    on this change with side == 0. The row key is the comment's PatchSet.Id,
+   *    the column key is the comment's UUID, and the value is the comment.
+   */
+  public Table<PatchSet.Id, String, PatchLineComment>
+      getDraftBaseComments() {
+    return HashBasedTable.create(draftBaseComments);
+  }
+
+  /**
+   * @return a defensive copy of the table containing all draft comments
+   *    on this change with side == 1. The row key is the comment's PatchSet.Id,
+   *    the column key is the comment's UUID, and the value is the comment.
+   */
+  public Table<PatchSet.Id, String, PatchLineComment>
+      getDraftPsComments() {
+    return HashBasedTable.create(draftPsComments);
+  }
+
+  public boolean containsComment(PatchLineComment c) {
+    Table<PatchSet.Id, String, PatchLineComment> t =
+        c.getSide() == (short) 0
+        ? getDraftBaseComments()
+        : getDraftPsComments();
+    return t.contains(getCommentPsId(c), c.getKey().get());
+  }
+
+  @Override
+  protected String getRefName() {
+    return RefNames.refsDraftComments(author, getChangeId());
+  }
+
+  @Override
+  protected void onLoad() throws IOException, ConfigInvalidException {
+    ObjectId rev = getRevision();
+    if (rev == null) {
+      return;
+    }
+
+    try (RevWalk walk = new RevWalk(reader);
+        DraftCommentNotesParser parser = new DraftCommentNotesParser(
+          getChangeId(), walk, rev, repoManager, draftsProject, author)) {
+      parser.parseDraftComments();
+
+      buildCommentTable(draftBaseComments, parser.draftBaseComments);
+      buildCommentTable(draftPsComments, parser.draftPsComments);
+      noteMap = parser.noteMap;
+    }
+  }
+
+  @Override
+  protected boolean onSave(CommitBuilder commit) throws IOException,
+      ConfigInvalidException {
+    throw new UnsupportedOperationException(
+        getClass().getSimpleName() + " is read-only");
+  }
+
+  @Override
+  protected void loadDefaults() {
+    // Do nothing; tables are final and initialized in constructor.
+  }
+
+  @Override
+  protected Project.NameKey getProjectName() {
+    return draftsProject;
+  }
+
+  private void buildCommentTable(
+      Table<PatchSet.Id, String, PatchLineComment> commentTable,
+      Multimap<PatchSet.Id, PatchLineComment> allComments) {
+    for (PatchLineComment c : allComments.values()) {
+      commentTable.put(getCommentPsId(c), c.getKey().get(), c);
+    }
+  }
+
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotesParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotesParser.java
new file mode 100644
index 0000000..4b3fbdf
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotesParser.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+
+class DraftCommentNotesParser implements AutoCloseable {
+  final Multimap<PatchSet.Id, PatchLineComment> draftBaseComments;
+  final Multimap<PatchSet.Id, PatchLineComment> draftPsComments;
+  NoteMap noteMap;
+
+  private final Change.Id changeId;
+  private final ObjectId tip;
+  private final RevWalk walk;
+  private final Repository repo;
+  private final Account.Id author;
+
+  DraftCommentNotesParser(Change.Id changeId, RevWalk walk, ObjectId tip,
+      GitRepositoryManager repoManager, AllUsersName draftsProject,
+      Account.Id author) throws RepositoryNotFoundException, IOException {
+    this.changeId = changeId;
+    this.walk = walk;
+    this.tip = tip;
+    this.repo = repoManager.openMetadataRepository(draftsProject);
+    this.author = author;
+
+    draftBaseComments = ArrayListMultimap.create();
+    draftPsComments = ArrayListMultimap.create();
+  }
+
+  @Override
+  public void close() {
+    repo.close();
+  }
+
+  void parseDraftComments() throws IOException, ConfigInvalidException {
+    walk.markStart(walk.parseCommit(tip));
+    noteMap = CommentsInNotesUtil.parseCommentsFromNotes(repo,
+        RefNames.refsDraftComments(author, changeId),
+        walk, changeId, draftBaseComments,
+        draftPsComments, PatchLineComment.Status.DRAFT);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
index 174997c..4193fe4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
@@ -20,5 +20,6 @@
   @Override
   public void configure() {
     factory(ChangeUpdate.Factory.class);
+    factory(ChangeDraftUpdate.Factory.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
index 36f685d..e6d9ff8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
@@ -14,61 +14,103 @@
 
 package com.google.gerrit.server.notedb;
 
-import com.google.common.annotations.VisibleForTesting;
+import static com.google.common.base.Preconditions.checkArgument;
+
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.Config;
 
+import java.util.HashSet;
+import java.util.Set;
+
 /**
- * Holds the current state of the notes DB migration.
+ * Holds the current state of the NoteDb migration.
  * <p>
- * During a transitional period, different subsets of the former gwtorm DB
- * functionality may be enabled on the site, possibly only for reading or
- * writing.
+ * The migration will proceed one root entity type at a time. A <em>root
+ * entity</em> is an entity stored in ReviewDb whose key's
+ * {@code getParentKey()} method returns null. For an example of the entity
+ * hierarchy rooted at Change, see the diagram in
+ * {@code com.google.gerrit.reviewdb.client.Change}.
+ * <p>
+ * During a transitional period, each root entity group from ReviewDb may be
+ * either <em>written to</em> or <em>both written to and read from</em> NoteDb.
+ * <p>
+ * This class controls the state of the migration according to options in
+ * {@code gerrit.config}. In general, any changes to these options should only
+ * be made by adventurous administrators, who know what they're doing, on
+ * non-production data, for the purposes of testing the NoteDb implementation.
+ * Changing options quite likely requires re-running {@code RebuildNoteDb}. For
+ * these reasons, the options remain undocumented.
  */
 @Singleton
 public class NotesMigration {
-  @VisibleForTesting
-  static NotesMigration allEnabled() {
-    Config cfg = new Config();
-    cfg.setBoolean("notedb", null, "write", true);
-    cfg.setBoolean("notedb", "patchSetApprovals", "read", true);
-    cfg.setBoolean("notedb", "changeMessages", "read", true);
-    cfg.setBoolean("notedb", "publishedComments", "read", true);
-    return new NotesMigration(cfg);
+  private static enum Table {
+    CHANGES;
+
+    private String key() {
+      return name().toLowerCase();
+    }
   }
 
-  private final boolean write;
-  private final boolean readPatchSetApprovals;
-  private final boolean readChangeMessages;
-  private final boolean readPublishedComments;
+  private static final String NOTEDB = "notedb";
+  private static final String READ = "read";
+  private static final String WRITE = "write";
+
+  private static void checkConfig(Config cfg) {
+    Set<String> keys = new HashSet<>();
+    for (Table t : Table.values()) {
+      keys.add(t.key());
+    }
+    for (String t : cfg.getSubsections(NOTEDB)) {
+      checkArgument(keys.contains(t.toLowerCase()),
+          "invalid notedb table: %s", t);
+      for (String key : cfg.getNames(NOTEDB, t)) {
+        String lk = key.toLowerCase();
+        checkArgument(lk.equals(WRITE) || lk.equals(READ),
+            "invalid notedb key: %s.%s", t, key);
+      }
+      boolean write = cfg.getBoolean(NOTEDB, t, WRITE, false);
+      boolean read = cfg.getBoolean(NOTEDB, t, READ, false);
+      checkArgument(!(read && !write),
+          "must have write enabled when read enabled: %s", t);
+    }
+  }
+
+  public static NotesMigration allEnabled() {
+    return new NotesMigration(allEnabledConfig());
+  }
+
+  public static Config allEnabledConfig() {
+    Config cfg = new Config();
+    for (Table t : Table.values()) {
+      cfg.setBoolean(NOTEDB, t.key(), WRITE, true);
+      cfg.setBoolean(NOTEDB, t.key(), READ, true);
+    }
+    return cfg;
+  }
+
+  private final boolean writeChanges;
+  private final boolean readChanges;
 
   @Inject
   NotesMigration(@GerritServerConfig Config cfg) {
-    write = cfg.getBoolean("notedb", null, "write", false);
-    readPatchSetApprovals =
-        cfg.getBoolean("notedb", "patchSetApprovals", "read", false);
-    readChangeMessages =
-        cfg.getBoolean("notedb", "changeMessages", "read", false);
-    readPublishedComments =
-        cfg.getBoolean("notedb", "publishedComments", "read", false);
+    checkConfig(cfg);
+    writeChanges = cfg.getBoolean(NOTEDB, Table.CHANGES.key(), WRITE, false);
+    readChanges = cfg.getBoolean(NOTEDB, Table.CHANGES.key(), READ, false);
   }
 
-  public boolean write() {
-    return write;
+  public boolean enabled() {
+    return writeChanges()
+        || readChanges();
   }
 
-  public boolean readPatchSetApprovals() {
-    return readPatchSetApprovals;
+  public boolean writeChanges() {
+    return writeChanges;
   }
 
-  public boolean readChangeMessages() {
-    return readChangeMessages;
-  }
-
-  public boolean readPublishedComments() {
-    return readPublishedComments;
+  public boolean readChanges() {
+    return readChanges;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
index 009b492..ffcce14 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -188,10 +188,11 @@
               int nb = lf + 1;
               int p = 0;
               while (p < ae - ab) {
-                if (cmp.equals(a, ab + p, a, ab + p))
+                if (cmp.equals(a, ab + p, a, ab + p)) {
                   p++;
-                else
+                } else {
                   break;
+                }
               }
               if (p == ae - ab) {
                 ab = nb;
@@ -224,10 +225,11 @@
               int nb = lf + 1;
               int p = 0;
               while (p < be - bb) {
-                if (cmp.equals(b, bb + p, b, bb + p))
+                if (cmp.equals(b, bb + p, b, bb + p)) {
                   p++;
-                else
+                } else {
                   break;
+                }
               }
               if (p == be - bb) {
                 bb = nb;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
index 81f4352..8740a6b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
@@ -48,18 +48,17 @@
     this.repo = repo;
     this.entry = patchList.get(fileName);
 
-    final ObjectReader reader = repo.newObjectReader();
-    try {
-      final RevWalk rw = new RevWalk(reader);
+    try (ObjectReader reader = repo.newObjectReader();
+        RevWalk rw = new RevWalk(reader)) {
       final RevCommit bCommit = rw.parseCommit(patchList.getNewId());
 
       if (Patch.COMMIT_MSG.equals(fileName)) {
         if (patchList.isAgainstParent()) {
           a = Text.EMPTY;
         } else {
-          a = Text.forCommit(repo, reader, patchList.getOldId());
+          a = Text.forCommit(reader, patchList.getOldId());
         }
-        b = Text.forCommit(repo, reader, bCommit);
+        b = Text.forCommit(reader, bCommit);
 
         aTree = null;
         bTree = null;
@@ -74,8 +73,6 @@
         }
         bTree = bCommit.getTree();
       }
-    } finally {
-      reader.release();
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
index 7d44912..4fff619 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
@@ -150,12 +150,13 @@
     while (low < high) {
       final int mid = (low + high) >>> 1;
       final int cmp = patches[mid].getNewName().compareTo(fileName);
-      if (cmp < 0)
+      if (cmp < 0) {
         low = mid + 1;
-      else if (cmp == 0)
+      } else if (cmp == 0) {
         return mid;
-      else
+      } else {
         high = mid;
+      }
     }
     return -(low + 1);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
index 31f5e96..e7c56be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
@@ -24,9 +24,9 @@
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
 
 import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gerrit.reviewdb.client.Patch.PatchType;
+import com.google.gerrit.reviewdb.client.PatchSet;
 
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.lib.Constants;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
index ff03840..9867b11 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
@@ -22,8 +22,8 @@
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
 
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.reviewdb.client.Project;
 
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
index 94050d7..40a51aa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -87,7 +87,6 @@
   private final ExecutorService diffExecutor;
   private final long timeoutMillis;
 
-
   @Inject
   PatchListLoader(GitRepositoryManager mgr,
       PatchListCache plc,
@@ -134,9 +133,9 @@
   private PatchList readPatchList(final PatchListKey key, final Repository repo)
       throws IOException, PatchListNotAvailableException {
     final RawTextComparator cmp = comparatorFor(key.getWhitespace());
-    final ObjectReader reader = repo.newObjectReader();
-    try {
-      final RevWalk rw = new RevWalk(reader);
+    try (ObjectReader reader = repo.newObjectReader();
+        RevWalk rw = new RevWalk(reader);
+        DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
       final RevCommit b = rw.parseCommit(key.getNewId());
       final RevObject a = aFor(key, repo, rw, b);
 
@@ -145,7 +144,7 @@
         // This is a merge commit, compared to its ancestor.
         //
         final PatchListEntry[] entries = new PatchListEntry[1];
-        entries[0] = newCommitMessage(cmp, repo, reader, null, b);
+        entries[0] = newCommitMessage(cmp, reader, null, b);
         return new PatchList(a, b, true, entries);
       }
 
@@ -156,7 +155,6 @@
       RevTree aTree = rw.parseTree(a);
       RevTree bTree = b.getTree();
 
-      DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE);
       df.setRepository(repo);
       df.setDiffComparator(cmp);
       df.setDetectRenames(true);
@@ -176,7 +174,7 @@
           : null;
       int cnt = diffEntries.size();
       List<PatchListEntry> entries = new ArrayList<>();
-      entries.add(newCommitMessage(cmp, repo, reader, //
+      entries.add(newCommitMessage(cmp, reader,
           againstParent ? null : aCommit, b));
       for (int i = 0; i < cnt; i++) {
         DiffEntry diffEntry = diffEntries.get(i);
@@ -188,8 +186,6 @@
       }
       return new PatchList(a, b, againstParent,
           entries.toArray(new PatchListEntry[entries.size()]));
-    } finally {
-      reader.release();
     }
   }
 
@@ -232,7 +228,7 @@
   }
 
   private PatchListEntry newCommitMessage(final RawTextComparator cmp,
-      final Repository db, final ObjectReader reader,
+      final ObjectReader reader,
       final RevCommit aCommit, final RevCommit bCommit) throws IOException {
     StringBuilder hdr = new StringBuilder();
 
@@ -253,8 +249,8 @@
     hdr.append("+++ b/").append(Patch.COMMIT_MSG).append("\n");
 
     Text aText =
-        aCommit != null ? Text.forCommit(db, reader, aCommit) : Text.EMPTY;
-    Text bText = Text.forCommit(db, reader, bCommit);
+        aCommit != null ? Text.forCommit(reader, aCommit) : Text.EMPTY;
+    Text bText = Text.forCommit(reader, bCommit);
 
     byte[] rawHdr = hdr.toString().getBytes("UTF-8");
     RawText aRawText = new RawText(aText.getContent());
@@ -327,8 +323,7 @@
     }
 
     ResolveMerger m = (ResolveMerger) mergeStrategy.newMerger(repo, true);
-    final ObjectInserter ins = repo.newObjectInserter();
-    try {
+    try (ObjectInserter ins = repo.newObjectInserter()) {
       DirCache dc = DirCache.newInCore();
       m.setDirCache(dc);
       m.setObjectInserter(new ObjectInserter.Filter() {
@@ -453,19 +448,14 @@
       }
 
       return rw.lookupTree(treeId);
-    } finally {
-      ins.release();
     }
   }
 
   private static ObjectId emptyTree(final Repository repo) throws IOException {
-    ObjectInserter oi = repo.newObjectInserter();
-    try {
+    try (ObjectInserter oi = repo.newObjectInserter()) {
       ObjectId id = oi.insert(Constants.OBJ_TREE, new byte[] {});
       oi.flush();
       return id;
-    } finally {
-      oi.release();
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index 0c98ccf..ea427eb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -20,12 +20,12 @@
 import com.google.gerrit.prettify.common.EditList;
 import com.google.gerrit.prettify.common.SparseFileContent;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
-import com.google.gerrit.server.FileTypeRegistry;
+import com.google.gerrit.server.mime.FileTypeRegistry;
 import com.google.inject.Inject;
 
 import eu.medsea.mimeutil.MimeType;
@@ -119,7 +119,7 @@
     try {
       return build(content, comments, history);
     } finally {
-      reader.release();
+      reader.close();
     }
   }
 
@@ -213,7 +213,8 @@
         content.getHeaderLines(), diffPrefs, a.dst, b.dst, edits,
         a.displayMethod, b.displayMethod, a.mimeType.toString(),
         b.mimeType.toString(), comments, history, hugeFile,
-        intralineDifferenceIsPossible, intralineFailure, intralineTimeout);
+        intralineDifferenceIsPossible, intralineFailure, intralineTimeout,
+        content.getPatchType() == Patch.PatchType.BINARY);
   }
 
   private static boolean isModify(PatchListEntry content) {
@@ -436,7 +437,7 @@
             displayMethod = DisplayMethod.NONE;
           } else {
             id = within;
-            src = Text.forCommit(db, reader, within);
+            src = Text.forCommit(reader, within);
             srcContent = src.getContent();
             if (src == Text.EMPTY) {
               mode = FileMode.MISSING;
@@ -513,9 +514,10 @@
       if (path == null || within == null) {
         return null;
       }
-      final RevWalk rw = new RevWalk(reader);
-      final RevTree tree = rw.parseTree(within);
-      return TreeWalk.forPath(reader, path, tree);
+      try (RevWalk rw = new RevWalk(reader)) {
+        final RevTree tree = rw.parseTree(within);
+        return TreeWalk.forPath(reader, path, tree);
+      }
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index b4337cf..3bdb6b2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -14,27 +14,32 @@
 
 package com.google.gerrit.server.patch;
 
+import com.google.common.base.Optional;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
-import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
+import com.google.gerrit.server.edit.ChangeEdit;
+import com.google.gerrit.server.edit.ChangeEditUtil;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.LargeObjectException;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -80,6 +85,8 @@
   private final PatchSet.Id psa;
   private final PatchSet.Id psb;
   private final AccountDiffPreference diffPrefs;
+  private final ChangeEditUtil editReader;
+  private Optional<ChangeEdit> edit;
 
   private final Change.Id changeId;
   private boolean loadHistory = true;
@@ -99,6 +106,7 @@
       final PatchListCache patchListCache, final ReviewDb db,
       final AccountInfoCacheFactory.Factory aicFactory,
       PatchLineCommentsUtil plcUtil,
+      ChangeEditUtil editReader,
       @Assisted ChangeControl control,
       @Assisted final String fileName,
       @Assisted("patchSetA") @Nullable final PatchSet.Id patchSetA,
@@ -111,6 +119,7 @@
     this.control = control;
     this.aicFactory = aicFactory;
     this.plcUtil = plcUtil;
+    this.editReader = editReader;
 
     this.fileName = fileName;
     this.psa = patchSetA;
@@ -130,7 +139,8 @@
 
   @Override
   public PatchScript call() throws OrmException, NoSuchChangeException,
-      LargeObjectException {
+      LargeObjectException, AuthException,
+      InvalidChangeOperationException, IOException {
     validatePatchSetId(psa);
     validatePatchSetId(psb);
 
@@ -197,12 +207,16 @@
   }
 
   private ObjectId toObjectId(final ReviewDb db, final PatchSet.Id psId)
-      throws OrmException, NoSuchChangeException {
+      throws OrmException, NoSuchChangeException, AuthException,
+      NoSuchChangeException, IOException {
     if (!changeId.equals(psId.getParentKey())) {
       throw new NoSuchChangeException(changeId);
     }
 
-    final PatchSet ps = db.patchSets().get(psId);
+    if (psId.get() == 0) {
+      return getEditRev();
+    }
+    PatchSet ps = db.patchSets().get(psId);
     if (ps == null || ps.getRevision() == null
         || ps.getRevision().get() == null) {
       throw new NoSuchChangeException(changeId);
@@ -216,6 +230,15 @@
     }
   }
 
+  private ObjectId getEditRev() throws AuthException,
+      NoSuchChangeException, IOException {
+    edit = editReader.byChange(change);
+    if (edit.isPresent()) {
+      return edit.get().getRef().getObjectId();
+    }
+    throw new NoSuchChangeException(change.getId());
+  }
+
   private void validatePatchSetId(final PatchSet.Id psId)
       throws NoSuchChangeException {
     if (psId == null) { // OK, means use base;
@@ -262,9 +285,15 @@
         history.add(p);
         byKey.put(p.getKey(), p);
       }
+      if (edit != null && edit.isPresent()) {
+        final Patch p = new Patch(new Patch.Key(
+            new PatchSet.Id(psb.getParentKey(), 0), fileName));
+        history.add(p);
+        byKey.put(p.getKey(), p);
+      }
     }
 
-    if (loadComments) {
+    if (loadComments && edit == null) {
       final AccountInfoCacheFactory aic = aicFactory.create();
       comments = new CommentDetail(psa, psb);
       switch (changeType) {
@@ -338,7 +367,8 @@
   private void loadDrafts(final Map<Patch.Key, Patch> byKey,
       final AccountInfoCacheFactory aic, final Account.Id me, final String file)
       throws OrmException {
-    for (PatchLineComment c : db.patchComments().draftByChangeFileAuthor(changeId, file, me)) {
+    for (PatchLineComment c :
+        plcUtil.draftByChangeFileAuthor(db, control.getNotes(), file, me)) {
       if (comments.include(c)) {
         aic.want(me);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
index 340300a..1e4ffce 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
@@ -85,17 +85,12 @@
     } catch (IOException e) {
       throw new PatchSetInfoNotAvailableException(e);
     }
-    try {
-      final RevWalk rw = new RevWalk(repo);
-      try {
-        final RevCommit src =
-            rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get()));
-        PatchSetInfo info = get(src, patchSet.getId());
-        info.setParents(toParentInfos(src.getParents(), rw));
-        return info;
-      } finally {
-        rw.release();
-      }
+    try (RevWalk rw = new RevWalk(repo)) {
+      final RevCommit src =
+          rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get()));
+      PatchSetInfo info = get(src, patchSet.getId());
+      info.setParents(toParentInfos(src.getParents(), rw));
+      return info;
     } catch (IOException e) {
       throw new PatchSetInfoNotAvailableException(e);
     } finally {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java
index f12b02b..882e25f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java
@@ -21,7 +21,6 @@
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.storage.pack.PackConfig;
@@ -44,46 +43,46 @@
   public static final byte[] NO_BYTES = {};
   public static final Text EMPTY = new Text(NO_BYTES);
 
-  public static Text forCommit(Repository db, ObjectReader reader,
-      AnyObjectId commitId) throws IOException {
-    RevWalk rw = new RevWalk(reader);
-    RevCommit c;
-    if (commitId instanceof RevCommit) {
-      c = (RevCommit) commitId;
-    } else {
-      c = rw.parseCommit(commitId);
-    }
-
-    StringBuilder b = new StringBuilder();
-    switch (c.getParentCount()) {
-      case 0:
-        break;
-      case 1: {
-        RevCommit p = c.getParent(0);
-        rw.parseBody(p);
-        b.append("Parent:     ");
-        b.append(reader.abbreviate(p, 8).name());
-        b.append(" (");
-        b.append(p.getShortMessage());
-        b.append(")\n");
-        break;
+  public static Text forCommit(ObjectReader reader, AnyObjectId commitId) throws IOException {
+    try (RevWalk rw = new RevWalk(reader)) {
+      RevCommit c;
+      if (commitId instanceof RevCommit) {
+        c = (RevCommit) commitId;
+      } else {
+        c = rw.parseCommit(commitId);
       }
-      default:
-        for (int i = 0; i < c.getParentCount(); i++) {
-          RevCommit p = c.getParent(i);
+
+      StringBuilder b = new StringBuilder();
+      switch (c.getParentCount()) {
+        case 0:
+          break;
+        case 1: {
+          RevCommit p = c.getParent(0);
           rw.parseBody(p);
-          b.append(i == 0 ? "Merge Of:   " : "            ");
+          b.append("Parent:     ");
           b.append(reader.abbreviate(p, 8).name());
           b.append(" (");
           b.append(p.getShortMessage());
           b.append(")\n");
+          break;
         }
+        default:
+          for (int i = 0; i < c.getParentCount(); i++) {
+            RevCommit p = c.getParent(i);
+            rw.parseBody(p);
+            b.append(i == 0 ? "Merge Of:   " : "            ");
+            b.append(reader.abbreviate(p, 8).name());
+            b.append(" (");
+            b.append(p.getShortMessage());
+            b.append(")\n");
+          }
+      }
+      appendPersonIdent(b, "Author", c.getAuthorIdent());
+      appendPersonIdent(b, "Commit", c.getCommitterIdent());
+      b.append("\n");
+      b.append(c.getFullMessage());
+      return new Text(b.toString().getBytes("UTF-8"));
     }
-    appendPersonIdent(b, "Author", c.getAuthorIdent());
-    appendPersonIdent(b, "Commit", c.getCommitterIdent());
-    b.append("\n");
-    b.append(c.getFullMessage());
-    return new Text(b.toString().getBytes("UTF-8"));
   }
 
   private static void appendPersonIdent(StringBuilder b, String field,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AbstractPreloadedPluginScanner.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AbstractPreloadedPluginScanner.java
index 0b128dd..e239654 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AbstractPreloadedPluginScanner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AbstractPreloadedPluginScanner.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.plugins;
 
+import static com.google.common.base.Preconditions.checkState;
+
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Sets;
 import com.google.gerrit.extensions.annotations.Export;
@@ -28,8 +30,6 @@
 import java.util.Set;
 import java.util.jar.Manifest;
 
-import static com.google.common.base.Preconditions.checkState;
-
 /**
  * Base plugin scanner for a set of pre-loaded classes.
  *
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
index 6f1204b..0eaddb3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
@@ -23,12 +23,17 @@
 import com.google.gerrit.extensions.annotations.Export;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.extensions.annotations.Listen;
+import com.google.gerrit.extensions.webui.JavaScriptPlugin;
 import com.google.gerrit.server.plugins.PluginContentScanner.ExtensionMetaData;
 import com.google.inject.AbstractModule;
 import com.google.inject.Module;
 import com.google.inject.Scopes;
 import com.google.inject.TypeLiteral;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.ParameterizedType;
 import java.util.Arrays;
@@ -36,12 +41,14 @@
 import java.util.Set;
 
 class AutoRegisterModules {
+  private static final Logger log = LoggerFactory.getLogger(AutoRegisterModules.class);
+
   private final String pluginName;
   private final PluginGuiceEnvironment env;
   private final PluginContentScanner scanner;
   private final ClassLoader classLoader;
   private final ModuleGenerator sshGen;
-  private final ModuleGenerator httpGen;
+  private final HttpModuleGenerator httpGen;
 
   private Set<Class<?>> sysSingletons;
   private Multimap<TypeLiteral<?>, Class<?>> sysListen;
@@ -58,32 +65,28 @@
     this.env = env;
     this.scanner = scanner;
     this.classLoader = classLoader;
-    this.sshGen = env.hasSshModule() ? env.newSshModuleGenerator() : null;
-    this.httpGen = env.hasHttpModule() ? env.newHttpModuleGenerator() : null;
+    this.sshGen = env.hasSshModule()
+        ? env.newSshModuleGenerator()
+        : new ModuleGenerator.NOP();
+    this.httpGen = env.hasHttpModule()
+        ? env.newHttpModuleGenerator()
+        : new HttpModuleGenerator.NOP();
   }
 
   AutoRegisterModules discover() throws InvalidPluginException {
     sysSingletons = Sets.newHashSet();
     sysListen = LinkedListMultimap.create();
 
-    if (sshGen != null) {
-      sshGen.setPluginName(pluginName);
-    }
-    if (httpGen != null) {
-      httpGen.setPluginName(pluginName);
-    }
+    sshGen.setPluginName(pluginName);
+    httpGen.setPluginName(pluginName);
 
     scan();
 
     if (!sysSingletons.isEmpty() || !sysListen.isEmpty()) {
       sysModule = makeSystemModule();
     }
-    if (sshGen != null) {
-      sshModule = sshGen.create();
-    }
-    if (httpGen != null) {
-      httpModule = httpGen.create();
-    }
+    sshModule = sshGen.create();
+    httpModule = httpGen.create();
     return this;
   }
 
@@ -117,6 +120,19 @@
     for (ExtensionMetaData listener : extensions.get(Listen.class)) {
       listen(listener);
     }
+    exportInitJs();
+  }
+
+  private void exportInitJs() {
+    try {
+      if (scanner.getEntry(JavaScriptPlugin.STATIC_INIT_JS).isPresent()) {
+        httpGen.export(JavaScriptPlugin.INIT_JS);
+      }
+    } catch (IOException e) {
+      log.warn(String.format("Cannot access %s from plugin %s: "
+          + "JavaScript auto-discovered plugin will not be registered",
+          JavaScriptPlugin.STATIC_INIT_JS, pluginName), e);
+    }
   }
 
   private void export(ExtensionMetaData def) throws InvalidPluginException {
@@ -138,14 +154,10 @@
     }
 
     if (is("org.apache.sshd.server.Command", clazz)) {
-      if (sshGen != null) {
-        sshGen.export(export, clazz);
-      }
+      sshGen.export(export, clazz);
     } else if (is("javax.servlet.http.HttpServlet", clazz)) {
-      if (httpGen != null) {
-        httpGen.export(export, clazz);
-        listen(clazz, clazz);
-      }
+      httpGen.export(export, clazz);
+      listen(clazz, clazz);
     } else {
       int cnt = sysListen.size();
       listen(clazz, clazz);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java
index 67ee7b4..7252617 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.securestore.SecureStore;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
@@ -116,6 +117,14 @@
   }
 
   @Inject
+  private SecureStore secureStore;
+
+  @Provides
+  SecureStore getSecureStore() {
+    return secureStore;
+  }
+
+  @Inject
   CopyConfigModule() {
   }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/HttpModuleGenerator.java
similarity index 65%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/plugins/HttpModuleGenerator.java
index 407b7c7..dd0ce67 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/HttpModuleGenerator.java
@@ -12,10 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.server.plugins;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+
+public interface HttpModuleGenerator extends ModuleGenerator {
+  void export(String javascript);
+
+  static class NOP extends ModuleGenerator.NOP
+      implements HttpModuleGenerator {
+    @Override
+    public void export(String javascript) {
+      // do nothing
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java
index df9ccf4..53f39f1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java
@@ -16,7 +16,7 @@
 
 import static com.google.gerrit.server.plugins.PluginLoader.asTemp;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 
@@ -60,7 +60,7 @@
   @Override
   public String getPluginName(File srcFile) {
     try {
-      return Objects.firstNonNull(getJarPluginName(srcFile),
+      return MoreObjects.firstNonNull(getJarPluginName(srcFile),
           PluginLoader.nameOf(srcFile));
     } catch (IOException e) {
       throw new IllegalArgumentException("Invalid plugin file " + srcFile
@@ -85,7 +85,7 @@
         File tmp = asTemp(in, tempNameFor(name), extension, tmpDir);
         return loadJarPlugin(name, srcFile, snapshot, tmp, description);
       }
-    } catch (IOException | ClassNotFoundException e) {
+    } catch (IOException e) {
       throw new InvalidPluginException("Cannot load Jar plugin " + srcFile, e);
     }
   }
@@ -119,8 +119,7 @@
 
   private ServerPlugin loadJarPlugin(String name, File srcJar,
       FileSnapshot snapshot, File tmp, PluginDescription description)
-      throws IOException, InvalidPluginException, MalformedURLException,
-      ClassNotFoundException {
+      throws IOException, InvalidPluginException, MalformedURLException {
     JarFile jarFile = new JarFile(tmp);
     boolean keep = false;
     try {
@@ -143,9 +142,10 @@
           new URLClassLoader(urls.toArray(new URL[urls.size()]),
               PluginLoader.parentFor(type));
 
+      JarScanner jarScanner = createJarScanner(srcJar);
       ServerPlugin plugin =
           new ServerPlugin(name, description.canonicalUrl, description.user,
-              srcJar, snapshot, new JarScanner(srcJar), description.dataDir,
+              srcJar, snapshot, jarScanner, description.dataDir,
               pluginLoader);
       plugin.setCleanupHandle(new CleanupHandle(tmp, jarFile));
       keep = true;
@@ -156,4 +156,13 @@
       }
     }
   }
+
+  private JarScanner createJarScanner(File srcJar)
+      throws InvalidPluginException {
+    try {
+      return new JarScanner(srcJar);
+    } catch (IOException e) {
+      throw new InvalidPluginException("Cannot scan plugin file " + srcJar, e);
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
index 31a7c98..a8600fe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.plugins;
 
-import static com.google.common.base.Objects.firstNonNull;
+import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.collect.Iterables.transform;
 
 import com.google.common.base.Function;
@@ -43,9 +43,11 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.annotation.Annotation;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Enumeration;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.jar.Attributes;
@@ -67,12 +69,8 @@
 
   private final JarFile jarFile;
 
-  public JarScanner(File srcFile) throws InvalidPluginException {
-    try {
-      this.jarFile = new JarFile(srcFile);
-    } catch (IOException e) {
-      throw new InvalidPluginException("Cannot scan plugin file " + srcFile, e);
-    }
+  public JarScanner(File srcFile) throws IOException {
+    this.jarFile = new JarFile(srcFile);
   }
 
   @Override
@@ -136,6 +134,41 @@
     return result.build();
   }
 
+  public List<String> findSubClassesOf(Class<?> superClass) throws IOException {
+    return findSubClassesOf(superClass.getName());
+  }
+
+  private List<String> findSubClassesOf(String superClass) throws IOException {
+    String name = superClass.replace('.', '/');
+
+    List<String> classes = new ArrayList<>();
+    Enumeration<JarEntry> e = jarFile.entries();
+    while (e.hasMoreElements()) {
+      JarEntry entry = e.nextElement();
+      if (skip(entry)) {
+        continue;
+      }
+
+      ClassData def = new ClassData(Collections.<String>emptySet());
+      try {
+        new ClassReader(read(jarFile, entry)).accept(def, SKIP_ALL);
+      } catch (RuntimeException err) {
+        PluginLoader.log.warn(String.format("Jar %s has invalid class file %s",
+            jarFile.getName(), entry.getName()), err);
+        continue;
+      }
+
+      if (name.equals(def.superName)) {
+        classes.addAll(findSubClassesOf(def.className));
+        if (def.isConcrete()) {
+          classes.add(def.className);
+        }
+      }
+    }
+
+    return classes;
+  }
+
   private static boolean skip(JarEntry entry) {
     if (!entry.getName().endsWith(".class")) {
       return true; // Avoid non-class resources.
@@ -164,8 +197,10 @@
   public static class ClassData extends ClassVisitor {
     int access;
     String className;
+    String superName;
     String annotationName;
     String annotationValue;
+    String[] interfaces;
     Iterable<String> exports;
 
     private ClassData(Iterable<String> exports) {
@@ -183,6 +218,7 @@
         String superName, String[] interfaces) {
       this.className = Type.getObjectType(name).getClassName();
       this.access = access;
+      this.superName = superName;
     }
 
     @Override
@@ -234,7 +270,7 @@
     }
   }
 
-  private static abstract class AbstractAnnotationVisitor extends
+  private abstract static class AbstractAnnotationVisitor extends
       AnnotationVisitor {
     AbstractAnnotationVisitor() {
       super(Opcodes.ASM4);
@@ -274,6 +310,7 @@
     return Collections.enumeration(Lists.transform(
         Collections.list(jarFile.entries()),
         new Function<JarEntry, PluginEntry>() {
+          @Override
           public PluginEntry apply(JarEntry jarEntry) {
             try {
               return resourceOf(jarEntry);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
index c5611dd..54f05f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
@@ -29,11 +29,7 @@
 
 import org.kohsuke.args4j.Option;
 
-import java.io.BufferedWriter;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
-import java.io.UnsupportedEncodingException;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
@@ -66,27 +62,12 @@
   }
 
   @Override
-  public Object apply(TopLevelResource resource)
-      throws UnsupportedEncodingException {
+  public Object apply(TopLevelResource resource) {
     format = OutputFormat.JSON;
     return display(null);
   }
 
-  public JsonElement display(OutputStream displayOutputStream)
-      throws UnsupportedEncodingException {
-    PrintWriter stdout = null;
-    if (displayOutputStream != null) {
-      try {
-        stdout = new PrintWriter(new BufferedWriter(
-            new OutputStreamWriter(displayOutputStream, "UTF-8")));
-      } catch (UnsupportedEncodingException e) {
-        throw new RuntimeException("JVM lacks UTF-8 encoding", e);
-      }
-    } else if (!format.isJson()) {
-      throw new IllegalStateException(
-          "Text output requires that a display OutputStream is provided.");
-    }
-
+  public JsonElement display(PrintWriter stdout) {
     Map<String, PluginInfo> output = Maps.newTreeMap();
     List<Plugin> plugins = Lists.newArrayList(pluginLoader.getPlugins(all));
     Collections.sort(plugins, new Comparator<Plugin>() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
index 2011e9d..ae8bb0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
@@ -26,4 +26,27 @@
   void listen(TypeLiteral<?> tl, Class<?> clazz);
 
   Module create() throws InvalidPluginException;
+
+  static class NOP implements ModuleGenerator {
+
+    @Override
+    public void setPluginName(String name) {
+      // do nothing
+    }
+
+    @Override
+    public void listen(TypeLiteral<?> tl, Class<?> clazz) {
+      // do nothing
+    }
+
+    @Override
+    public void export(Export export, Class<?> type) {
+      // do nothing
+    }
+
+    @Override
+    public Module create() throws InvalidPluginException {
+      return null;
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
index 606d5da..c13c533 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.plugins;
 
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
index 5b63a215..3fbbfa9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -40,6 +40,7 @@
 import com.google.inject.AbstractModule;
 import com.google.inject.Binding;
 import com.google.inject.Guice;
+import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Module;
@@ -56,7 +57,6 @@
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArrayList;
 
-import javax.inject.Inject;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
@@ -83,7 +83,7 @@
   private Module httpModule;
 
   private Provider<ModuleGenerator> sshGen;
-  private Provider<ModuleGenerator> httpGen;
+  private Provider<HttpModuleGenerator> httpGen;
 
   private Map<TypeLiteral<?>, DynamicItem<?>> sysItems;
   private Map<TypeLiteral<?>, DynamicItem<?>> sshItems;
@@ -149,13 +149,15 @@
     return sysModule;
   }
 
-  public void setCfgInjector(Injector cfgInjector) {
+  public void setDbCfgInjector(Injector dbInjector, Injector cfgInjector) {
+    final Module db = copy(dbInjector);
     final Module cm = copy(cfgInjector);
     final Module sm = copy(sysInjector);
     sysModule = new AbstractModule() {
       @Override
       protected void configure() {
         install(copyConfigModule);
+        install(db);
         install(cm);
         install(sm);
       }
@@ -187,7 +189,7 @@
 
   public void setHttpInjector(Injector injector) {
     httpModule = copy(injector);
-    httpGen = injector.getProvider(ModuleGenerator.class);
+    httpGen = injector.getProvider(HttpModuleGenerator.class);
     httpItems = dynamicItemsOf(injector);
     httpSets = dynamicSetsOf(injector);
     httpMaps = dynamicMapsOf(injector);
@@ -204,7 +206,7 @@
     return httpModule;
   }
 
-  ModuleGenerator newHttpModuleGenerator() {
+  HttpModuleGenerator newHttpModuleGenerator() {
     return httpGen.get();
   }
 
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 003da6c..b51359d 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
@@ -16,7 +16,7 @@
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Joiner;
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.LinkedHashMultimap;
@@ -71,8 +71,9 @@
   static final String PLUGIN_TMP_PREFIX = "plugin_";
   static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
 
-  public String getPluginName(File srcFile) throws IOException {
-    return Objects.firstNonNull(getGerritPluginName(srcFile), nameOf(srcFile));
+  public String getPluginName(File srcFile) {
+    return MoreObjects.firstNonNull(getGerritPluginName(srcFile),
+        nameOf(srcFile));
   }
 
   private final File pluginsDir;
@@ -158,7 +159,7 @@
 
     String fileName = originalName;
     File tmp = asTemp(in, ".next_" + fileName + "_", ".tmp", pluginsDir);
-    String name = Objects.firstNonNull(getGerritPluginName(tmp),
+    String name = MoreObjects.firstNonNull(getGerritPluginName(tmp),
         nameOf(fileName));
     if (!originalName.equals(name)) {
       log.warn(String.format("Plugin provides its own name: <%s>,"
@@ -213,7 +214,7 @@
     }
   }
 
-  synchronized private void unloadPlugin(Plugin plugin) {
+  private synchronized void unloadPlugin(Plugin plugin) {
     persistentCacheFactory.onStop(plugin);
     String name = plugin.getName();
     log.info(String.format("Unloading plugin %s", name));
@@ -404,7 +405,7 @@
     Iterator<Entry<String, File>> it = from.entrySet().iterator();
     while (it.hasNext()) {
       Entry<String,File> entry = it.next();
-      to.add(new AbstractMap.SimpleImmutableEntry<String, File>(
+      to.add(new AbstractMap.SimpleImmutableEntry<>(
           entry.getKey(), entry.getValue()));
     }
   }
@@ -545,7 +546,7 @@
   }
 
   private Plugin loadPlugin(String name, File srcPlugin, FileSnapshot snapshot)
-      throws IOException, ClassNotFoundException, InvalidPluginException {
+      throws InvalidPluginException {
     String pluginName = srcPlugin.getName();
     if (isJsPlugin(pluginName)) {
       return loadJsPlugin(name, srcPlugin, snapshot);
@@ -623,43 +624,37 @@
   public Multimap<String, File> prunePlugins(File pluginsDir) {
     List<File> pluginFiles = scanFilesInPluginsDirectory(pluginsDir);
     Multimap<String, File> map;
-    try {
-      map = asMultimap(pluginFiles);
-      for (String plugin : map.keySet()) {
-        Collection<File> files = map.asMap().get(plugin);
-        if (files.size() == 1) {
-          continue;
-        }
-        // retrieve enabled plugins
-        Iterable<File> enabled = filterDisabledPlugins(
-            files);
-        // If we have only one (the winner) plugin, nothing to do
-        if (!Iterables.skip(enabled, 1).iterator().hasNext()) {
-          continue;
-        }
-        File winner = Iterables.getFirst(enabled, null);
-        assert(winner != null);
-        // Disable all loser plugins by renaming their file names to
-        // "file.disabled" and replace the disabled files in the multimap.
-        Collection<File> elementsToRemove = Lists.newArrayList();
-        Collection<File> elementsToAdd = Lists.newArrayList();
-        for (File loser : Iterables.skip(enabled, 1)) {
-          log.warn(String.format("Plugin <%s> was disabled, because"
-               + " another plugin <%s>"
-               + " with the same name <%s> already exists",
-               loser, winner, plugin));
-          File disabledPlugin = new File(loser + ".disabled");
-          elementsToAdd.add(disabledPlugin);
-          elementsToRemove.add(loser);
-          loser.renameTo(disabledPlugin);
-        }
-        Iterables.removeAll(files, elementsToRemove);
-        Iterables.addAll(files, elementsToAdd);
+    map = asMultimap(pluginFiles);
+    for (String plugin : map.keySet()) {
+      Collection<File> files = map.asMap().get(plugin);
+      if (files.size() == 1) {
+        continue;
       }
-    } catch (IOException e) {
-      log.warn("Cannot prune plugin list",
-          e.getCause());
-      return LinkedHashMultimap.create();
+      // retrieve enabled plugins
+      Iterable<File> enabled = filterDisabledPlugins(
+          files);
+      // If we have only one (the winner) plugin, nothing to do
+      if (!Iterables.skip(enabled, 1).iterator().hasNext()) {
+        continue;
+      }
+      File winner = Iterables.getFirst(enabled, null);
+      assert(winner != null);
+      // Disable all loser plugins by renaming their file names to
+      // "file.disabled" and replace the disabled files in the multimap.
+      Collection<File> elementsToRemove = Lists.newArrayList();
+      Collection<File> elementsToAdd = Lists.newArrayList();
+      for (File loser : Iterables.skip(enabled, 1)) {
+        log.warn(String.format("Plugin <%s> was disabled, because"
+             + " another plugin <%s>"
+             + " with the same name <%s> already exists",
+             loser, winner, plugin));
+        File disabledPlugin = new File(loser + ".disabled");
+        elementsToAdd.add(disabledPlugin);
+        elementsToRemove.add(loser);
+        loser.renameTo(disabledPlugin);
+      }
+      Iterables.removeAll(files, elementsToRemove);
+      Iterables.addAll(files, elementsToAdd);
     }
     return map;
   }
@@ -693,7 +688,7 @@
     });
   }
 
-  public String getGerritPluginName(File srcFile) throws IOException {
+  public String getGerritPluginName(File srcFile) {
     String fileName = srcFile.getName();
     if (isJsPlugin(fileName)) {
       return fileName.substring(0, fileName.length() - 3);
@@ -704,8 +699,7 @@
     return null;
   }
 
-  private Multimap<String, File> asMultimap(List<File> plugins)
-      throws IOException {
+  private Multimap<String, File> asMultimap(List<File> plugins) {
     Multimap<String, File> map = LinkedHashMultimap.create();
     for (File srcFile : plugins) {
       map.put(getPluginName(srcFile), srcFile);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginResource.java
index 9572271..e7ebd56 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginResource.java
@@ -25,12 +25,12 @@
   private final Plugin plugin;
   private final String name;
 
-  PluginResource(Plugin plugin) {
+  public PluginResource(Plugin plugin) {
     this.plugin = plugin;
     this.name = plugin.getName();
   }
 
-  PluginResource(String name) {
+  public PluginResource(String name) {
     this.plugin = null;
     this.name = name;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
index ee5e90a..0b037fb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
@@ -69,7 +69,7 @@
   private Injector sysInjector;
   private Injector sshInjector;
   private Injector httpInjector;
-  private LifecycleManager manager;
+  private LifecycleManager serverManager;
   private List<ReloadableRegistrationHandle<?>> reloadableHandles;
 
   public ServerPlugin(String name,
@@ -140,12 +140,14 @@
     }
   }
 
+  @Override
   @Nullable
   public String getVersion() {
     Attributes main = manifest.getMainAttributes();
     return main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
   }
 
+  @Override
   boolean canReload() {
     Attributes main = manifest.getMainAttributes();
     String v = main.getValue("Gerrit-ReloadMode");
@@ -161,6 +163,7 @@
     }
   }
 
+  @Override
   void start(PluginGuiceEnvironment env) throws Exception {
     RequestContext oldContext = env.enter(this);
     try {
@@ -172,7 +175,7 @@
 
   private void startPlugin(PluginGuiceEnvironment env) throws Exception {
     Injector root = newRootInjector(env);
-    manager = new LifecycleManager();
+    serverManager = new LifecycleManager();
 
     AutoRegisterModules auto = null;
     if (sysModule == null && sshModule == null && httpModule == null) {
@@ -182,10 +185,10 @@
 
     if (sysModule != null) {
       sysInjector = root.createChildInjector(root.getInstance(sysModule));
-      manager.add(sysInjector);
+      serverManager.add(sysInjector);
     } else if (auto != null && auto.sysModule != null) {
       sysInjector = root.createChildInjector(auto.sysModule);
-      manager.add(sysInjector);
+      serverManager.add(sysInjector);
     } else {
       sysInjector = root;
     }
@@ -198,11 +201,11 @@
       if (sshModule != null) {
         modules.add(sysInjector.getInstance(sshModule));
         sshInjector = sysInjector.createChildInjector(modules);
-        manager.add(sshInjector);
+        serverManager.add(sshInjector);
       } else if (auto != null && auto.sshModule != null) {
         modules.add(auto.sshModule);
         sshInjector = sysInjector.createChildInjector(modules);
-        manager.add(sshInjector);
+        serverManager.add(sshInjector);
       }
     }
 
@@ -214,15 +217,15 @@
       if (httpModule != null) {
         modules.add(sysInjector.getInstance(httpModule));
         httpInjector = sysInjector.createChildInjector(modules);
-        manager.add(httpInjector);
+        serverManager.add(httpInjector);
       } else if (auto != null && auto.httpModule != null) {
         modules.add(auto.httpModule);
         httpInjector = sysInjector.createChildInjector(modules);
-        manager.add(httpInjector);
+        serverManager.add(httpInjector);
       }
     }
 
-    manager.start();
+    serverManager.start();
   }
 
   private Injector newRootInjector(final PluginGuiceEnvironment env) {
@@ -250,12 +253,14 @@
             public File get() {
               if (!ready) {
                 synchronized (dataDir) {
-                  if (!dataDir.exists() && !dataDir.mkdirs()) {
-                    throw new ProvisionException(String.format(
-                        "Cannot create %s for plugin %s",
-                        dataDir.getAbsolutePath(), getName()));
+                  if (!ready) {
+                    if (!dataDir.exists() && !dataDir.mkdirs()) {
+                      throw new ProvisionException(String.format(
+                          "Cannot create %s for plugin %s",
+                          dataDir.getAbsolutePath(), getName()));
+                    }
+                    ready = true;
                   }
-                  ready = true;
                 }
               }
               return dataDir;
@@ -266,44 +271,49 @@
     return Guice.createInjector(modules);
   }
 
+  @Override
   void stop(PluginGuiceEnvironment env) {
-    if (manager != null) {
+    if (serverManager != null) {
       RequestContext oldContext = env.enter(this);
       try {
-        manager.stop();
+        serverManager.stop();
       } finally {
         env.exit(oldContext);
       }
-      manager = null;
+      serverManager = null;
       sysInjector = null;
       sshInjector = null;
       httpInjector = null;
     }
   }
 
+  @Override
   public Injector getSysInjector() {
     return sysInjector;
   }
 
+  @Override
   @Nullable
   public Injector getSshInjector() {
     return sshInjector;
   }
 
+  @Override
   @Nullable
   public Injector getHttpInjector() {
     return httpInjector;
   }
 
+  @Override
   public void add(RegistrationHandle handle) {
-    if (manager != null) {
+    if (serverManager != null) {
       if (handle instanceof ReloadableRegistrationHandle) {
         if (reloadableHandles == null) {
           reloadableHandles = Lists.newArrayList();
         }
         reloadableHandles.add((ReloadableRegistrationHandle<?>) handle);
       }
-      manager.add(handle);
+      serverManager.add(handle);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
index fab1ed3..ef153ce 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.git.BanCommitResult;
-import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.project.BanCommit.Input;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -51,8 +50,12 @@
     }
   }
 
+  private final com.google.gerrit.server.git.BanCommit banCommit;
+
   @Inject
-  private com.google.gerrit.server.git.BanCommit banCommit;
+  BanCommit(com.google.gerrit.server.git.BanCommit banCommit) {
+    this.banCommit = banCommit;
+  }
 
   @Override
   public BanResultInfo apply(ProjectResource rsrc, Input input)
@@ -77,7 +80,7 @@
         r.ignored = transformCommits(result.getIgnoredObjectIds());
       } catch (PermissionDeniedException e) {
         throw new AuthException(e.getMessage());
-      } catch (MergeException | ConcurrentRefUpdateException e) {
+      } catch (ConcurrentRefUpdateException e) {
         throw new ResourceConflictException(e.getMessage(), e);
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java
index 6681d94..98531ce 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java
@@ -41,4 +41,8 @@
   public String getRef() {
     return branchInfo.ref;
   }
+
+  public String getRevision() {
+    return branchInfo.revision;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
index 3bf3522..e96d3a5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
@@ -16,12 +16,14 @@
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AcceptsCreate;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ChildCollection;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.project.ListBranches.BranchInfo;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.Constants;
@@ -34,12 +36,12 @@
     ChildCollection<ProjectResource, BranchResource>,
     AcceptsCreate<ProjectResource> {
   private final DynamicMap<RestView<BranchResource>> views;
-  private final ListBranches list;
+  private final Provider<ListBranches> list;
   private final CreateBranch.Factory createBranchFactory;
 
   @Inject
   BranchesCollection(DynamicMap<RestView<BranchResource>> views,
-      ListBranches list, CreateBranch.Factory createBranchFactory) {
+      Provider<ListBranches> list, CreateBranch.Factory createBranchFactory) {
     this.views = views;
     this.list = list;
     this.createBranchFactory = createBranchFactory;
@@ -47,18 +49,18 @@
 
   @Override
   public RestView<ProjectResource> list() {
-    return list;
+    return list.get();
   }
 
   @Override
   public BranchResource parse(ProjectResource parent, IdString id)
-      throws ResourceNotFoundException, IOException {
+      throws ResourceNotFoundException, IOException, BadRequestException {
     String branchName = id.get();
     if (!branchName.startsWith(Constants.R_REFS)
         && !branchName.equals(Constants.HEAD)) {
       branchName = Constants.R_HEADS + branchName;
     }
-    List<BranchInfo> branches = list.apply(parent);
+    List<BranchInfo> branches = list.get().apply(parent);
     for (BranchInfo b : branches) {
       if (branchName.equals(b.ref)) {
         return new BranchResource(parent.getControl(), b);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 1878c02..83db726 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -20,9 +20,6 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.RefConfigSection;
-import com.google.gerrit.common.data.SubmitRecord;
-import com.google.gerrit.common.data.SubmitTypeRecord;
-import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -39,28 +36,13 @@
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
-import com.googlecode.prolog_cafe.lang.IntegerTerm;
-import com.googlecode.prolog_cafe.lang.ListTerm;
-import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.StructureTerm;
-import com.googlecode.prolog_cafe.lang.SymbolTerm;
-import com.googlecode.prolog_cafe.lang.Term;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 
 
 /** Access control management for a user accessing a single change. */
 public class ChangeControl {
-  private static final Logger log = LoggerFactory
-      .getLogger(ChangeControl.class);
-
   public static class GenericFactory {
     private final ProjectControl.GenericFactory projectControl;
     private final Provider<ReviewDb> db;
@@ -71,6 +53,15 @@
       db = d;
     }
 
+    public ChangeControl controlFor(Change.Id changeId, CurrentUser user)
+        throws NoSuchChangeException, OrmException {
+      Change change = db.get().changes().get(changeId);
+      if (change == null) {
+        throw new NoSuchChangeException(changeId);
+      }
+      return controlFor(change, user);
+    }
+
     public ChangeControl controlFor(Change change, CurrentUser user)
         throws NoSuchChangeException {
       final Project.NameKey projectKey = change.getProject();
@@ -84,6 +75,15 @@
       }
     }
 
+    public ChangeControl validateFor(Change.Id changeId, CurrentUser user)
+        throws NoSuchChangeException, OrmException {
+      Change change = db.get().changes().get(changeId);
+      if (change == null) {
+        throw new NoSuchChangeException(changeId);
+      }
+      return validateFor(change, user);
+    }
+
     public ChangeControl validateFor(Change change, CurrentUser user)
         throws NoSuchChangeException, OrmException {
       ChangeControl c = controlFor(change, user);
@@ -94,77 +94,11 @@
     }
   }
 
-  public static class Factory {
-    private final ProjectControl.Factory projectControl;
-    private final Provider<ReviewDb> db;
-
-    @Inject
-    Factory(final ProjectControl.Factory p, final Provider<ReviewDb> d) {
-      projectControl = p;
-      db = d;
-    }
-
-    public ChangeControl controlFor(final Change.Id id)
-        throws NoSuchChangeException {
-      final Change change;
-      try {
-        change = db.get().changes().get(id);
-        if (change == null) {
-          throw new NoSuchChangeException(id);
-        }
-      } catch (OrmException e) {
-        throw new NoSuchChangeException(id, e);
-      }
-      return controlFor(change);
-    }
-
-    public ChangeControl controlFor(final Change change)
-        throws NoSuchChangeException {
-      try {
-        final Project.NameKey projectKey = change.getProject();
-        return projectControl.validateFor(projectKey).controlFor(change);
-      } catch (NoSuchProjectException e) {
-        throw new NoSuchChangeException(change.getId(), e);
-      }
-    }
-
-    public ChangeControl validateFor(final Change.Id id)
-        throws NoSuchChangeException, OrmException {
-      return validate(controlFor(id), db.get());
-    }
-
-    public ChangeControl validateFor(final Change change)
-        throws NoSuchChangeException, OrmException {
-      return validate(controlFor(change), db.get());
-    }
-
-    private static ChangeControl validate(final ChangeControl c, final ReviewDb db)
-        throws NoSuchChangeException, OrmException{
-      if (!c.isVisible(db)) {
-        throw new NoSuchChangeException(c.getChange().getId());
-      }
-      return c;
-    }
-  }
-
   public interface AssistedFactory {
     ChangeControl create(RefControl refControl, Change change);
     ChangeControl create(RefControl refControl, ChangeNotes notes);
   }
 
-  /**
-   * Exception thrown when the label term of a submit record
-   * unexpectedly didn't contain a user term.
-   */
-  private static class UserTermExpected extends Exception {
-    private static final long serialVersionUID = 1L;
-
-    public UserTermExpected(SubmitRecord.Label label) {
-      super(String.format("A label with the status %s must contain a user.",
-          label.toString()));
-    }
-  }
-
   private final ChangeData.Factory changeDataFactory;
   private final RefControl refControl;
   private final ChangeNotes notes;
@@ -390,8 +324,13 @@
     }
   }
 
-  public List<SubmitRecord> getSubmitRecords(ReviewDb db, PatchSet patchSet) {
-    return canSubmit(db, patchSet, null, false, true, false);
+  /** Can this user edit the hashtag name? */
+  public boolean canEditHashtags() {
+    return isOwner() // owner (aka creator) of the change can edit hashtags
+          || getRefControl().isOwner() // branch owner can edit hashtags
+          || getProjectControl().isOwner() // project owner can edit hashtags
+          || getCurrentUser().getCapabilities().canAdministrateServer() // site administers are god
+          || getRefControl().canEditHashtags(); // user can edit hashtag on a specific ref
   }
 
   public boolean canSubmit() {
@@ -402,306 +341,17 @@
     return getRefControl().canSubmitAs();
   }
 
-  public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet patchSet) {
-    return canSubmit(db, patchSet, null, false, false, false);
-  }
-
-  public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet patchSet,
-      @Nullable ChangeData cd, boolean fastEvalLabels, boolean allowClosed,
-      boolean allowDraft) {
-    if (!allowClosed && getChange().getStatus().isClosed()) {
-      SubmitRecord rec = new SubmitRecord();
-      rec.status = SubmitRecord.Status.CLOSED;
-      return Collections.singletonList(rec);
-    }
-
-    if (!patchSet.getId().equals(getChange().currentPatchSetId())) {
-      return ruleError("Patch set " + patchSet.getPatchSetId() + " is not current");
-    }
-
-    cd = changeData(db, cd);
-    if ((getChange().getStatus() == Change.Status.DRAFT || patchSet.isDraft())
-        && !allowDraft) {
-      return cannotSubmitDraft(db, patchSet, cd);
-    }
-
-    List<Term> results;
-    SubmitRuleEvaluator evaluator;
-    try {
-      evaluator = new SubmitRuleEvaluator(db, patchSet,
-          getProjectControl(),
-          this, getChange(), cd,
-          fastEvalLabels,
-          "locate_submit_rule", "can_submit",
-          "locate_submit_filter", "filter_submit_results");
-      results = evaluator.evaluate();
-    } catch (RuleEvalException e) {
-      return logRuleError(e.getMessage(), e);
-    }
-
-    if (results.isEmpty()) {
-      // This should never occur. A well written submit rule will always produce
-      // at least one result informing the caller of the labels that are
-      // required for this change to be submittable. Each label will indicate
-      // whether or not that is actually possible given the permissions.
-      log.error("Submit rule '" + evaluator.getSubmitRule() + "' for change "
-          + getChange().getId() + " of " + getProject().getName()
-          + " has no solution.");
-      return ruleError("Project submit rule has no solution");
-    }
-
-    return resultsToSubmitRecord(evaluator.getSubmitRule(), results);
-  }
-
   private boolean match(String destBranch, String refPattern) {
     return RefPatternMatcher.getMatcher(refPattern).match(destBranch,
         this.getRefControl().getCurrentUser().getUserName());
   }
 
-  private List<SubmitRecord> cannotSubmitDraft(ReviewDb db, PatchSet patchSet,
-      @Nullable ChangeData cd) {
-    try {
-      if (!isDraftVisible(db, cd)) {
-        return ruleError("Patch set " + patchSet.getPatchSetId() + " not found");
-      } else if (patchSet.isDraft()) {
-        return ruleError("Cannot submit draft patch sets");
-      } else {
-        return ruleError("Cannot submit draft changes");
-      }
-    } catch (OrmException err) {
-      return logRuleError("Cannot read patch set " + patchSet.getId(), err);
-    }
-  }
-
-  /**
-   * Convert the results from Prolog Cafe's format to Gerrit's common format.
-   *
-   * can_submit/1 terminates when an ok(P) record is found. Therefore walk
-   * the results backwards, using only that ok(P) record if it exists. This
-   * skips partial results that occur early in the output. Later after the loop
-   * the out collection is reversed to restore it to the original ordering.
-   */
-  public List<SubmitRecord> resultsToSubmitRecord(Term submitRule, List<Term> results) {
-    List<SubmitRecord> out = new ArrayList<>(results.size());
-    for (int resultIdx = results.size() - 1; 0 <= resultIdx; resultIdx--) {
-      Term submitRecord = results.get(resultIdx);
-      SubmitRecord rec = new SubmitRecord();
-      out.add(rec);
-
-      if (!submitRecord.isStructure() || 1 != submitRecord.arity()) {
-        return logInvalidResult(submitRule, submitRecord);
-      }
-
-      if ("ok".equals(submitRecord.name())) {
-        rec.status = SubmitRecord.Status.OK;
-
-      } else if ("not_ready".equals(submitRecord.name())) {
-        rec.status = SubmitRecord.Status.NOT_READY;
-
-      } else {
-        return logInvalidResult(submitRule, submitRecord);
-      }
-
-      // Unpack the one argument. This should also be a structure with one
-      // argument per label that needs to be reported on to the caller.
-      //
-      submitRecord = submitRecord.arg(0);
-
-      if (!submitRecord.isStructure()) {
-        return logInvalidResult(submitRule, submitRecord);
-      }
-
-      rec.labels = new ArrayList<>(submitRecord.arity());
-
-      for (Term state : ((StructureTerm) submitRecord).args()) {
-        if (!state.isStructure() || 2 != state.arity() || !"label".equals(state.name())) {
-          return logInvalidResult(submitRule, submitRecord);
-        }
-
-        SubmitRecord.Label lbl = new SubmitRecord.Label();
-        rec.labels.add(lbl);
-
-        lbl.label = state.arg(0).name();
-        Term status = state.arg(1);
-
-        try {
-          if ("ok".equals(status.name())) {
-            lbl.status = SubmitRecord.Label.Status.OK;
-            appliedBy(lbl, status);
-
-          } else if ("reject".equals(status.name())) {
-            lbl.status = SubmitRecord.Label.Status.REJECT;
-            appliedBy(lbl, status);
-
-          } else if ("need".equals(status.name())) {
-            lbl.status = SubmitRecord.Label.Status.NEED;
-
-          } else if ("may".equals(status.name())) {
-            lbl.status = SubmitRecord.Label.Status.MAY;
-
-          } else if ("impossible".equals(status.name())) {
-            lbl.status = SubmitRecord.Label.Status.IMPOSSIBLE;
-
-          } else {
-            return logInvalidResult(submitRule, submitRecord);
-          }
-        } catch (UserTermExpected e) {
-          return logInvalidResult(submitRule, submitRecord, e.getMessage());
-        }
-      }
-
-      if (rec.status == SubmitRecord.Status.OK) {
-        break;
-      }
-    }
-    Collections.reverse(out);
-
-    return out;
-  }
-
-  public SubmitTypeRecord getSubmitTypeRecord(ReviewDb db, PatchSet patchSet) {
-    return getSubmitTypeRecord(db, patchSet, null);
-  }
-
-  public SubmitTypeRecord getSubmitTypeRecord(ReviewDb db, PatchSet patchSet,
-      @Nullable ChangeData cd) {
-    cd = changeData(db, cd);
-    try {
-      if (getChange().getStatus() == Change.Status.DRAFT
-          && !isDraftVisible(db, cd)) {
-        return typeRuleError("Patch set " + patchSet.getPatchSetId()
-            + " not found");
-      }
-      if (patchSet.isDraft() && !isDraftVisible(db, cd)) {
-        return typeRuleError("Patch set " + patchSet.getPatchSetId()
-            + " not found");
-      }
-    } catch (OrmException err) {
-      return logTypeRuleError("Cannot read patch set " + patchSet.getId(),
-          err);
-    }
-
-    List<Term> results;
-    SubmitRuleEvaluator evaluator;
-    try {
-      evaluator = new SubmitRuleEvaluator(db, patchSet,
-          getProjectControl(), this, getChange(), cd,
-          false,
-          "locate_submit_type", "get_submit_type",
-          "locate_submit_type_filter", "filter_submit_type_results");
-      results = evaluator.evaluate();
-    } catch (RuleEvalException e) {
-      return logTypeRuleError(e.getMessage(), e);
-    }
-
-    if (results.isEmpty()) {
-      // Should never occur for a well written rule
-      log.error("Submit rule '" + evaluator.getSubmitRule() + "' for change "
-          + getChange().getId() + " of " + getProject().getName()
-          + " has no solution.");
-      return typeRuleError("Project submit rule has no solution");
-    }
-
-    Term typeTerm = results.get(0);
-    if (!typeTerm.isSymbol()) {
-      log.error("Submit rule '" + evaluator.getSubmitRule() + "' for change "
-          + getChange().getId() + " of " + getProject().getName()
-          + " did not return a symbol.");
-      return typeRuleError("Project submit rule has invalid solution");
-    }
-
-    String typeName = ((SymbolTerm)typeTerm).name();
-    try {
-      return SubmitTypeRecord.OK(
-          SubmitType.valueOf(typeName.toUpperCase()));
-    } catch (IllegalArgumentException e) {
-      return logInvalidType(evaluator.getSubmitRule(), typeName);
-    }
-  }
-
-  private List<SubmitRecord> logInvalidResult(Term rule, Term record, String reason) {
-    return logRuleError("Submit rule " + rule + " for change " + getChange().getId()
-        + " of " + getProject().getName() + " output invalid result: " + record
-        + (reason == null ? "" : ". Reason: " + reason));
-  }
-
-  private List<SubmitRecord> logInvalidResult(Term rule, Term record) {
-    return logInvalidResult(rule, record, null);
-  }
-
-  private List<SubmitRecord> logRuleError(String err, Exception e) {
-    log.error(err, e);
-    return ruleError("Error evaluating project rules, check server log");
-  }
-
-  private List<SubmitRecord> logRuleError(String err) {
-    log.error(err);
-    return ruleError("Error evaluating project rules, check server log");
-  }
-
-  private List<SubmitRecord> ruleError(String err) {
-    SubmitRecord rec = new SubmitRecord();
-    rec.status = SubmitRecord.Status.RULE_ERROR;
-    rec.errorMessage = err;
-    return Collections.singletonList(rec);
-  }
-
-  private SubmitTypeRecord logInvalidType(Term rule, String record) {
-    return logTypeRuleError("Submit type rule " + rule + " for change "
-        + getChange().getId() + " of " + getProject().getName()
-        + " output invalid result: " + record);
-  }
-
-  private SubmitTypeRecord logTypeRuleError(String err, Exception e) {
-    log.error(err, e);
-    return typeRuleError("Error evaluating project type rules, check server log");
-  }
-
-  private SubmitTypeRecord logTypeRuleError(String err) {
-    log.error(err);
-    return typeRuleError("Error evaluating project type rules, check server log");
-  }
-
-  private SubmitTypeRecord typeRuleError(String err) {
-    SubmitTypeRecord rec = new SubmitTypeRecord();
-    rec.status = SubmitTypeRecord.Status.RULE_ERROR;
-    rec.errorMessage = err;
-    return rec;
-  }
-
   private ChangeData changeData(ReviewDb db, @Nullable ChangeData cd) {
     return cd != null ? cd : changeDataFactory.create(db, this);
   }
 
-  private void appliedBy(SubmitRecord.Label label, Term status)
-      throws UserTermExpected {
-    if (status.isStructure() && status.arity() == 1) {
-      Term who = status.arg(0);
-      if (isUser(who)) {
-        label.appliedBy = new Account.Id(((IntegerTerm) who.arg(0)).intValue());
-      } else {
-        throw new UserTermExpected(label);
-      }
-    }
-  }
-
-  private boolean isDraftVisible(ReviewDb db, ChangeData cd)
+  public boolean isDraftVisible(ReviewDb db, ChangeData cd)
       throws OrmException {
     return isOwner() || isReviewer(db, cd) || getRefControl().canViewDrafts();
   }
-
-  private static boolean isUser(Term who) {
-    return who.isStructure()
-        && who.arity() == 1
-        && who.name().equals("user")
-        && who.arg(0).isInteger();
-  }
-
-  public static Term toListTerm(List<Term> terms) {
-    Term list = Prolog.Nil;
-    for (int i = terms.size() - 1; i >= 0; i--) {
-      list = new ListTerm(terms.get(i), list);
-    }
-    return list;
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeModifiedException.java
similarity index 71%
copy from gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeModifiedException.java
index cd07320..f3ca8b6 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeModifiedException.java
@@ -12,16 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd;
+package com.google.gerrit.server.project;
 
-public class GerritUiOptions {
-  private final boolean headless;
+public class ChangeModifiedException extends InvalidChangeOperationException {
+  private static final long serialVersionUID = 1L;
 
-  public GerritUiOptions(boolean headless) {
-    this.headless = headless;
-  }
-
-  public boolean enableDefaultUi() {
-    return !headless;
+  public ChangeModifiedException(String msg) {
+    super(msg);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java
index 2a386a4..f68acdd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java
@@ -24,7 +24,7 @@
 
   private final ProjectControl child;
 
-  ChildProjectResource(ProjectResource project, ProjectControl child) {
+  public ChildProjectResource(ProjectResource project, ProjectControl child) {
     super(project);
     this.child = child;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkInfo.java
index 4035c7e..a91e745 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkInfo.java
@@ -47,7 +47,7 @@
   public final String html;
   public final Boolean enabled; // null means true
 
-  public transient final String name;
+  public final transient String name;
 
   public CommentLinkInfo(String name, String match, String link, String html,
       Boolean enabled) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitResource.java
index fc9c807..36186a4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitResource.java
@@ -14,22 +14,28 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.inject.TypeLiteral;
 
 import org.eclipse.jgit.revwalk.RevCommit;
 
-public class CommitResource extends ProjectResource {
+public class CommitResource implements RestResource {
   public static final TypeLiteral<RestView<CommitResource>> COMMIT_KIND =
       new TypeLiteral<RestView<CommitResource>>() {};
 
+  private final ProjectResource project;
   private final RevCommit commit;
 
-  public CommitResource(ProjectControl control, RevCommit commit) {
-    super(control);
+  public CommitResource(ProjectResource project, RevCommit commit) {
+    this.project = project;
     this.commit = commit;
   }
 
+  public ProjectControl getProject() {
+    return project.getControl();
+  }
+
   public RevCommit getCommit() {
     return commit;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java
index f27dc6e..4879bb7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommitsCollection.java
@@ -19,8 +19,10 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -37,12 +39,15 @@
     ChildCollection<ProjectResource, CommitResource> {
   private final DynamicMap<RestView<CommitResource>> views;
   private final GitRepositoryManager repoManager;
+  private final Provider<ReviewDb> db;
 
   @Inject
   public CommitsCollection(DynamicMap<RestView<CommitResource>> views,
-      GitRepositoryManager repoManager) {
+      GitRepositoryManager repoManager,
+      Provider<ReviewDb> db) {
     this.views = views;
     this.repoManager = repoManager;
+    this.db = db;
   }
 
   @Override
@@ -60,25 +65,19 @@
       throw new ResourceNotFoundException(id);
     }
 
-    Repository repo = repoManager.openRepository(parent.getNameKey());
-    try {
-      RevWalk rw = new RevWalk(repo);
-      try {
-        RevCommit commit = rw.parseCommit(objectId);
-        if (!parent.getControl().canReadCommit(rw, commit)) {
-          throw new ResourceNotFoundException(id);
-        }
-        for (int i = 0; i < commit.getParentCount(); i++) {
-          rw.parseCommit(commit.getParent(i));
-        }
-        return new CommitResource(parent.getControl(), commit);
-      } catch (MissingObjectException | IncorrectObjectTypeException e) {
+    try (Repository repo = repoManager.openRepository(parent.getNameKey());
+        RevWalk rw = new RevWalk(repo)) {
+      RevCommit commit = rw.parseCommit(objectId);
+      rw.parseBody(commit);
+      if (!parent.getControl().canReadCommit(db.get(), rw, commit)) {
         throw new ResourceNotFoundException(id);
-      } finally {
-        rw.release();
       }
-    } finally {
-      repo.close();
+      for (int i = 0; i < commit.getParentCount(); i++) {
+        rw.parseBody(rw.parseCommit(commit.getParent(i)));
+      }
+      return new CommitResource(parent, commit);
+    } catch (MissingObjectException | IncorrectObjectTypeException e) {
+      throw new ResourceNotFoundException(id);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
index 358e023..1c6782c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
@@ -17,9 +17,9 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ActionInfo;
-import com.google.gerrit.extensions.common.InheritableBoolean;
-import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicMap.Entry;
 import com.google.gerrit.extensions.restapi.RestView;
@@ -43,10 +43,11 @@
   public InheritedBooleanInfo useContributorAgreements;
   public InheritedBooleanInfo useContentMerge;
   public InheritedBooleanInfo useSignedOffBy;
+  public InheritedBooleanInfo createNewChangeForAllNotInTarget;
   public InheritedBooleanInfo requireChangeId;
   public MaxObjectSizeLimitInfo maxObjectSizeLimit;
   public SubmitType submitType;
-  public com.google.gerrit.extensions.api.projects.ProjectState state;
+  public com.google.gerrit.extensions.client.ProjectState state;
   public Map<String, Map<String, ConfigParameterInfo>> pluginConfig;
   public Map<String, ActionInfo> actions;
 
@@ -68,17 +69,23 @@
     InheritedBooleanInfo useSignedOffBy = new InheritedBooleanInfo();
     InheritedBooleanInfo useContentMerge = new InheritedBooleanInfo();
     InheritedBooleanInfo requireChangeId = new InheritedBooleanInfo();
+    InheritedBooleanInfo createNewChangeForAllNotInTarget =
+        new InheritedBooleanInfo();
 
     useContributorAgreements.value = projectState.isUseContributorAgreements();
     useSignedOffBy.value = projectState.isUseSignedOffBy();
     useContentMerge.value = projectState.isUseContentMerge();
     requireChangeId.value = projectState.isRequireChangeID();
+    createNewChangeForAllNotInTarget.value =
+        projectState.isCreateNewChangeForAllNotInTarget();
 
     useContributorAgreements.configuredValue =
         p.getUseContributorAgreements();
     useSignedOffBy.configuredValue = p.getUseSignedOffBy();
     useContentMerge.configuredValue = p.getUseContentMerge();
     requireChangeId.configuredValue = p.getRequireChangeID();
+    createNewChangeForAllNotInTarget.configuredValue =
+        p.getCreateNewChangeForAllNotInTarget();
 
     ProjectState parentState = Iterables.getFirst(projectState
         .parents(), null);
@@ -88,12 +95,15 @@
       useSignedOffBy.inheritedValue = parentState.isUseSignedOffBy();
       useContentMerge.inheritedValue = parentState.isUseContentMerge();
       requireChangeId.inheritedValue = parentState.isRequireChangeID();
+      createNewChangeForAllNotInTarget.inheritedValue =
+          parentState.isCreateNewChangeForAllNotInTarget();
     }
 
     this.useContributorAgreements = useContributorAgreements;
     this.useSignedOffBy = useSignedOffBy;
     this.useContentMerge = useContentMerge;
     this.requireChangeId = requireChangeId;
+    this.createNewChangeForAllNotInTarget = createNewChangeForAllNotInTarget;
 
     MaxObjectSizeLimitInfo maxObjectSizeLimit = new MaxObjectSizeLimitInfo();
     maxObjectSizeLimit.value =
@@ -106,7 +116,7 @@
     this.maxObjectSizeLimit = maxObjectSizeLimit;
 
     this.submitType = p.getSubmitType();
-    this.state = p.getState() != com.google.gerrit.extensions.api.projects.ProjectState.ACTIVE ? p.getState() : null;
+    this.state = p.getState() != com.google.gerrit.extensions.client.ProjectState.ACTIVE ? p.getState() : null;
 
     this.commentlinks = Maps.newLinkedHashMap();
     for (CommentLinkInfo cl : projectState.getCommentLinks()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
index 4a3b415..ac40df0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -69,6 +70,7 @@
 
   private final Provider<IdentifiedUser>  identifiedUser;
   private final GitRepositoryManager repoManager;
+  private final Provider<ReviewDb> db;
   private final GitReferenceUpdated referenceUpdated;
   private final ChangeHooks hooks;
   private String ref;
@@ -76,10 +78,12 @@
   @Inject
   CreateBranch(Provider<IdentifiedUser> identifiedUser,
       GitRepositoryManager repoManager,
+      Provider<ReviewDb> db,
       GitReferenceUpdated referenceUpdated, ChangeHooks hooks,
       @Assisted String ref) {
     this.identifiedUser = identifiedUser;
     this.repoManager = repoManager;
+    this.db = db;
     this.referenceUpdated = referenceUpdated;
     this.hooks = hooks;
     this.ref = ref;
@@ -131,7 +135,8 @@
         }
       }
 
-      if (!refControl.canCreate(rw, object, true)) {
+      rw.reset();
+      if (!refControl.canCreate(db.get(), rw, object)) {
         throw new AuthException("Cannot create \"" + ref + "\"");
       }
 
@@ -163,6 +168,7 @@
               }
               refPrefix = getRefPrefix(refPrefix);
             }
+            //$FALL-THROUGH$
           default: {
             throw new IOException(result.name());
           }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index 37ab506..776a25e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -14,16 +14,16 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.errors.ProjectCreationFailedException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
-import com.google.gerrit.extensions.common.InheritableBoolean;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ProjectInfo;
-import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -114,16 +114,21 @@
       args.ownerIds = ownerIds;
     }
     args.contributorAgreements =
-        Objects.firstNonNull(input.useContributorAgreements,
+        MoreObjects.firstNonNull(input.useContributorAgreements,
             InheritableBoolean.INHERIT);
     args.signedOffBy =
-        Objects.firstNonNull(input.useSignedOffBy, InheritableBoolean.INHERIT);
+        MoreObjects.firstNonNull(input.useSignedOffBy,
+            InheritableBoolean.INHERIT);
     args.contentMerge =
         input.submitType == SubmitType.FAST_FORWARD_ONLY
-            ? InheritableBoolean.FALSE : Objects.firstNonNull(
-                input.useContentMerge, InheritableBoolean.INHERIT);
+            ? InheritableBoolean.FALSE : MoreObjects.firstNonNull(
+                input.useContentMerge,
+                InheritableBoolean.INHERIT);
+    args.newChangeForAllNotInTarget =
+        MoreObjects.firstNonNull(input.createNewChangeForAllNotInTarget,
+            InheritableBoolean.INHERIT);
     args.changeIdRequired =
-        Objects.firstNonNull(input.requireChangeId, InheritableBoolean.INHERIT);
+        MoreObjects.firstNonNull(input.requireChangeId, InheritableBoolean.INHERIT);
     try {
       args.maxObjectSizeLimit =
           ProjectConfig.validMaxObjectSizeLimit(input.maxObjectSizeLimit);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
index e937e0f..bbecb33 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.extensions.common.InheritableBoolean;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 
@@ -33,6 +33,7 @@
   public boolean permissionsOnly;
   public List<String> branch;
   public InheritableBoolean contentMerge;
+  public InheritableBoolean newChangeForAllNotInTarget;
   public InheritableBoolean changeIdRequired;
   public boolean createEmptyCommit;
   public String maxObjectSizeLimit;
@@ -42,6 +43,7 @@
     signedOffBy = InheritableBoolean.INHERIT;
     contentMerge = InheritableBoolean.INHERIT;
     changeIdRequired = InheritableBoolean.INHERIT;
+    newChangeForAllNotInTarget = InheritableBoolean.INHERIT;
     submitType = SubmitType.MERGE_IF_NECESSARY;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardResource.java
index 0e2db48..099350d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardResource.java
@@ -24,7 +24,7 @@
   public static final TypeLiteral<RestView<DashboardResource>> DASHBOARD_KIND =
       new TypeLiteral<RestView<DashboardResource>>() {};
 
-  static DashboardResource projectDefault(ProjectControl ctl) {
+  public static DashboardResource projectDefault(ProjectControl ctl) {
     return new DashboardResource(ctl, null, null, null, true);
   }
 
@@ -34,7 +34,7 @@
   private final Config config;
   private final boolean projectDefault;
 
-  DashboardResource(ProjectControl control,
+  public DashboardResource(ProjectControl control,
       String refName,
       String pathName,
       Config config,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
index 061e992..7822fa6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
@@ -17,7 +17,7 @@
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_DASHBOARDS;
 
 import com.google.common.base.Joiner;
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
@@ -49,7 +49,6 @@
 import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
-import java.io.UnsupportedEncodingException;
 import java.util.List;
 
 @Singleton
@@ -157,8 +156,7 @@
   }
 
   static DashboardInfo parse(Project definingProject, String refName,
-      String path, Config config, String project, boolean setDefault)
-      throws UnsupportedEncodingException {
+      String path, Config config, String project, boolean setDefault) {
     DashboardInfo info = new DashboardInfo(refName, path);
     info.project = project;
     info.definingProject = definingProject.getName();
@@ -173,7 +171,7 @@
     }
 
     UrlEncoded u = new UrlEncoded("/dashboard/");
-    u.put("title", Objects.firstNonNull(info.title, info.path));
+    u.put("title", MoreObjects.firstNonNull(info.title, info.path));
     if (info.foreach != null) {
       u.put("foreach", replace(project, info.foreach));
     }
@@ -194,7 +192,7 @@
   }
 
   private static String defaultOf(Project proj) {
-    final String defaultId = Objects.firstNonNull(
+    final String defaultId = MoreObjects.firstNonNull(
         proj.getLocalDefaultDashboard(),
         Strings.nullToEmpty(proj.getDefaultDashboard()));
     if (defaultId.startsWith(REFS_DASHBOARDS)) {
@@ -220,8 +218,7 @@
     String title;
     List<Section> sections = Lists.newArrayList();
 
-    DashboardInfo(String ref, String name)
-        throws UnsupportedEncodingException {
+    DashboardInfo(String ref, String name) {
       this.ref = ref;
       this.path = name;
       this.id = Joiner.on(':').join(Url.encode(ref), Url.encode(path));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
index 162a97d..4aba333 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
@@ -25,12 +25,14 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.DeleteBranch.Input;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.LockFailedException;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.slf4j.Logger;
@@ -41,6 +43,8 @@
 @Singleton
 public class DeleteBranch implements RestModifyView<BranchResource, Input>{
   private static final Logger log = LoggerFactory.getLogger(DeleteBranch.class);
+  private static final int MAX_LOCK_FAILURE_CALLS = 10;
+  private static final long SLEEP_ON_LOCK_FAILURE_MS = 15;
 
   static class Input {
   }
@@ -48,16 +52,19 @@
   private final Provider<IdentifiedUser> identifiedUser;
   private final GitRepositoryManager repoManager;
   private final Provider<ReviewDb> dbProvider;
+  private final Provider<InternalChangeQuery> queryProvider;
   private final GitReferenceUpdated referenceUpdated;
   private final ChangeHooks hooks;
 
   @Inject
   DeleteBranch(Provider<IdentifiedUser> identifiedUser,
       GitRepositoryManager repoManager, Provider<ReviewDb> dbProvider,
+      Provider<InternalChangeQuery> queryProvider,
       GitReferenceUpdated referenceUpdated, ChangeHooks hooks) {
     this.identifiedUser = identifiedUser;
     this.repoManager = repoManager;
     this.dbProvider = dbProvider;
+    this.queryProvider = queryProvider;
     this.referenceUpdated = referenceUpdated;
     this.hooks = hooks;
   }
@@ -68,8 +75,8 @@
     if (!rsrc.getControl().controlForRef(rsrc.getBranchKey()).canDelete()) {
       throw new AuthException("Cannot delete branch");
     }
-    if (dbProvider.get().changes().byBranchOpenAll(rsrc.getBranchKey())
-        .iterator().hasNext()) {
+    if (!queryProvider.get().setLimit(1)
+        .byBranchOpen(rsrc.getBranchKey()).isEmpty()) {
       throw new ResourceConflictException("branch " + rsrc.getBranchKey()
           + " has open changes");
     }
@@ -77,14 +84,28 @@
     Repository r = repoManager.openRepository(rsrc.getNameKey());
     try {
       RefUpdate.Result result;
-      RefUpdate u;
-      try {
-        u = r.updateRef(rsrc.getRef());
-        u.setForceUpdate(true);
-        result = u.delete();
-      } catch (IOException e) {
-        log.error("Cannot delete " + rsrc.getBranchKey(), e);
-        throw e;
+      RefUpdate u = r.updateRef(rsrc.getRef());
+      u.setForceUpdate(true);
+      int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
+      for (;;) {
+        try {
+          result = u.delete();
+        } catch (LockFailedException e) {
+          result = RefUpdate.Result.LOCK_FAILURE;
+        } catch (IOException e) {
+          log.error("Cannot delete " + rsrc.getBranchKey(), e);
+          throw e;
+        }
+        if (result == RefUpdate.Result.LOCK_FAILURE
+            && --remainingLockFailureCalls > 0) {
+          try {
+            Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS);
+          } catch (InterruptedException ie) {
+            // ignore
+          }
+        } else {
+          break;
+        }
       }
 
       switch (result) {
@@ -100,7 +121,7 @@
           break;
 
         case REJECTED_CURRENT_BRANCH:
-          log.warn("Cannot delete " + rsrc.getBranchKey() + ": " + result.name());
+          log.error("Cannot delete " + rsrc.getBranchKey() + ": " + result.name());
           throw new ResourceConflictException("cannot delete current branch");
 
         default:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
new file mode 100644
index 0000000..d6e93f0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
@@ -0,0 +1,178 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import static java.lang.String.format;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.DeleteBranches.Input;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.ReceiveCommand.Result;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.List;
+
+@Singleton
+class DeleteBranches implements RestModifyView<ProjectResource, Input> {
+  private static final Logger log = LoggerFactory.getLogger(DeleteBranches.class);
+
+  static class Input {
+    List<String> branches;
+
+    static Input init(Input in) {
+      if (in == null) {
+        in = new Input();
+      }
+      if (in.branches == null) {
+        in.branches = Lists.newArrayListWithCapacity(1);
+      }
+      return in;
+    }
+  }
+
+  private final Provider<IdentifiedUser> identifiedUser;
+  private final GitRepositoryManager repoManager;
+  private final Provider<ReviewDb> dbProvider;
+  private final Provider<InternalChangeQuery> queryProvider;
+  private final GitReferenceUpdated referenceUpdated;
+  private final ChangeHooks hooks;
+
+  @Inject
+  DeleteBranches(Provider<IdentifiedUser> identifiedUser,
+      GitRepositoryManager repoManager,
+      Provider<ReviewDb> dbProvider,
+      Provider<InternalChangeQuery> queryProvider,
+      GitReferenceUpdated referenceUpdated,
+      ChangeHooks hooks) {
+    this.identifiedUser = identifiedUser;
+    this.repoManager = repoManager;
+    this.dbProvider = dbProvider;
+    this.queryProvider = queryProvider;
+    this.referenceUpdated = referenceUpdated;
+    this.hooks = hooks;
+  }
+
+  @Override
+  public Response<?> apply(ProjectResource project, Input input)
+      throws OrmException, IOException, ResourceConflictException {
+    input = Input.init(input);
+    Repository r = repoManager.openRepository(project.getNameKey());
+    try {
+      BatchRefUpdate batchUpdate = r.getRefDatabase().newBatchUpdate();
+      for (String branch : input.branches) {
+        batchUpdate.addCommand(createDeleteCommand(project, r, branch));
+      }
+      try (RevWalk rw = new RevWalk(r)) {
+        batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
+      }
+      StringBuilder errorMessages = new StringBuilder();
+      for (ReceiveCommand command : batchUpdate.getCommands()) {
+        if (command.getResult() == Result.OK) {
+          postDeletion(project, command);
+        } else {
+          appendAndLogErrorMessage(errorMessages, command);
+        }
+      }
+      if (errorMessages.length() > 0) {
+        throw new ResourceConflictException(errorMessages.toString());
+      }
+    } finally {
+      r.close();
+    }
+    return Response.none();
+  }
+
+  private ReceiveCommand createDeleteCommand(ProjectResource project,
+      Repository r, String branch) throws OrmException, IOException {
+    Ref ref = r.getRefDatabase().getRef(branch);
+    ReceiveCommand command;
+    if (ref == null) {
+      command = new ReceiveCommand(ObjectId.zeroId(), ObjectId.zeroId(), branch);
+      command.setResult(Result.REJECTED_OTHER_REASON,
+          "it doesn't exist or you do not have permission to delete it");
+      return command;
+    }
+    command =
+        new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), ref.getName());
+    Branch.NameKey branchKey =
+        new Branch.NameKey(project.getNameKey(), ref.getName());
+    if (!project.getControl().controlForRef(branchKey).canDelete()) {
+      command.setResult(Result.REJECTED_OTHER_REASON,
+          "it doesn't exist or you do not have permission to delete it");
+    }
+    if (!queryProvider.get().setLimit(1).byBranchOpen(branchKey).isEmpty()) {
+      command.setResult(Result.REJECTED_OTHER_REASON, "it has open changes");
+    }
+    return command;
+  }
+
+  private void appendAndLogErrorMessage(StringBuilder errorMessages,
+      ReceiveCommand cmd) {
+    String msg = null;
+    switch (cmd.getResult()) {
+      case REJECTED_CURRENT_BRANCH:
+        msg = format("Cannot delete %s: it is the current branch",
+            cmd.getRefName());
+        break;
+      case REJECTED_OTHER_REASON:
+        msg = format("Cannot delete %s: %s", cmd.getRefName(), cmd.getMessage());
+        break;
+      default:
+        msg = format("Cannot delete %s: %s", cmd.getRefName(), cmd.getResult());
+        break;
+    }
+    log.error(msg);
+    errorMessages.append(msg);
+    errorMessages.append("\n");
+  }
+
+  private void postDeletion(ProjectResource project, ReceiveCommand cmd)
+      throws OrmException {
+    referenceUpdated.fire(project.getNameKey(), cmd.getRefName(),
+        cmd.getOldId(), cmd.getNewId());
+    Branch.NameKey branchKey =
+        new Branch.NameKey(project.getNameKey(), cmd.getRefName());
+    hooks.doRefUpdatedHook(branchKey, cmd.getOldId(), cmd.getNewId(),
+        identifiedUser.get().getAccount());
+    ResultSet<SubmoduleSubscription> submoduleSubscriptions =
+        dbProvider.get().submoduleSubscriptions().bySuperProject(branchKey);
+    dbProvider.get().submoduleSubscriptions().delete(submoduleSubscriptions);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
index c7c6675..47942be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
@@ -16,28 +16,29 @@
 
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.TypeLiteral;
 
+import org.eclipse.jgit.lib.ObjectId;
+
 public class FileResource implements RestResource {
   public static final TypeLiteral<RestView<FileResource>> FILE_KIND =
       new TypeLiteral<RestView<FileResource>>() {};
 
-  private final Project.NameKey project;
-  private final String rev;
+  private final ProjectControl project;
+  private final ObjectId rev;
   private final String path;
 
-  public FileResource(Project.NameKey project, String rev, String path) {
+  public FileResource(ProjectControl project, ObjectId rev, String path) {
     this.project = project;
     this.rev = rev;
     this.path = path;
   }
 
-  public Project.NameKey getProject() {
+  public ProjectControl getProject() {
     return project;
   }
 
-  public String getRev() {
+  public ObjectId getRev() {
     return rev;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java
index 2d0af29..d0460d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java
@@ -19,10 +19,11 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.server.project.BranchResource;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.lib.ObjectId;
+
 @Singleton
 public class FilesCollection implements
     ChildCollection<BranchResource, FileResource> {
@@ -40,7 +41,10 @@
 
   @Override
   public FileResource parse(BranchResource parent, IdString id) {
-    return new FileResource(parent.getNameKey(), parent.getRef(), id.get());
+    return new FileResource(
+        parent.getControl(),
+        ObjectId.fromString(parent.getRevision()),
+        id.get());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java
index 4dc4338..8e0aab8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java
@@ -40,8 +40,7 @@
   @Override
   public FileResource parse(CommitResource parent, IdString id)
       throws ResourceNotFoundException {
-    return new FileResource(parent.getNameKey(), parent.getCommit().getName(),
-        id.get());
+    return new FileResource(parent.getProject(), parent.getCommit(), id.get());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetCommit.java
index d43ea68..63cee1f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetCommit.java
@@ -15,14 +15,12 @@
 package com.google.gerrit.server.project;
 
 import com.google.gerrit.extensions.common.CommitInfo;
-import com.google.gerrit.extensions.common.GitPerson;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CommonConverters;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevCommit;
 
-import java.sql.Timestamp;
 import java.util.ArrayList;
 
 @Singleton
@@ -30,16 +28,14 @@
 
   @Override
   public CommitInfo apply(CommitResource rsrc) {
-    RevCommit c = rsrc.getCommit();
-    CommitInfo info = toCommitInfo(c);
-    return info;
+    return toCommitInfo(rsrc.getCommit());
   }
 
   private static CommitInfo toCommitInfo(RevCommit commit) {
     CommitInfo info = new CommitInfo();
     info.commit = commit.getName();
-    info.author = toGitPerson(commit.getAuthorIdent());
-    info.committer = toGitPerson(commit.getCommitterIdent());
+    info.author = CommonConverters.toGitPerson(commit.getAuthorIdent());
+    info.committer = CommonConverters.toGitPerson(commit.getCommitterIdent());
     info.subject = commit.getShortMessage();
     info.message = commit.getFullMessage();
     info.parents = new ArrayList<>(commit.getParentCount());
@@ -52,13 +48,4 @@
     }
     return info;
   }
-
-  private static GitPerson toGitPerson(PersonIdent ident) {
-    GitPerson gp = new GitPerson();
-    gp.name = ident.getName();
-    gp.email = ident.getEmailAddress();
-    gp.date = new Timestamp(ident.getWhen().getTime());
-    gp.tz = ident.getTimeZoneOffset();
-    return gp;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
index e0bc7e6..bb91097 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
@@ -17,13 +17,11 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.ProjectConfigEntry;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
@@ -40,8 +38,7 @@
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       PluginConfigFactory cfgFactory,
       AllProjectsNameProvider allProjects,
-      DynamicMap<RestView<ProjectResource>> views,
-      Provider<CurrentUser> currentUser) {
+      DynamicMap<RestView<ProjectResource>> views) {
     this.config = config;
     this.pluginConfigEntries = pluginConfigEntries;
     this.allProjects = allProjects;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetContent.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetContent.java
index 5a9221b..23e9e30 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetContent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetContent.java
@@ -17,25 +17,27 @@
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.change.FileContentUtil;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import java.io.IOException;
 
 @Singleton
 public class GetContent implements RestReadView<FileResource> {
-  private final Provider<com.google.gerrit.server.change.GetContent> getContent;
+  private final FileContentUtil fileContentUtil;
 
   @Inject
-  GetContent(Provider<com.google.gerrit.server.change.GetContent> getContent) {
-    this.getContent = getContent;
+  GetContent(FileContentUtil fileContentUtil) {
+    this.fileContentUtil = fileContentUtil;
   }
 
   @Override
   public BinaryResult apply(FileResource rsrc)
       throws ResourceNotFoundException, IOException {
-    return getContent.get().apply(rsrc.getProject(), rsrc.getRev(),
+    return fileContentUtil.getContent(
+        rsrc.getProject().getProjectState(),
+        rsrc.getRev(),
         rsrc.getPath());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
index f05ece4..2efd257 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
@@ -17,8 +17,10 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -34,20 +36,20 @@
 
 @Singleton
 public class GetHead implements RestReadView<ProjectResource> {
-
   private GitRepositoryManager repoManager;
+  private Provider<ReviewDb> db;
 
   @Inject
-  GetHead(GitRepositoryManager repoManager) {
+  GetHead(GitRepositoryManager repoManager,
+      Provider<ReviewDb> db) {
     this.repoManager = repoManager;
+    this.db = db;
   }
 
   @Override
   public String apply(ProjectResource rsrc) throws AuthException,
       ResourceNotFoundException, IOException {
-    Repository repo = null;
-    try {
-      repo = repoManager.openRepository(rsrc.getNameKey());
+    try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
       Ref head = repo.getRef(Constants.HEAD);
       if (head == null) {
         throw new ResourceNotFoundException(Constants.HEAD);
@@ -58,10 +60,9 @@
         }
         throw new AuthException("not allowed to see HEAD");
       } else if (head.getObjectId() != null) {
-        RevWalk rw = new RevWalk(repo);
-        try {
+        try (RevWalk rw = new RevWalk(repo)) {
           RevCommit commit = rw.parseCommit(head.getObjectId());
-          if (rsrc.getControl().canReadCommit(rw, commit)) {
+          if (rsrc.getControl().canReadCommit(db.get(), rw, commit)) {
             return head.getObjectId().name();
           }
           throw new AuthException("not allowed to see HEAD");
@@ -70,17 +71,11 @@
             return head.getObjectId().name();
           }
           throw new AuthException("not allowed to see HEAD");
-        } finally {
-          rw.release();
         }
       }
       throw new ResourceNotFoundException(Constants.HEAD);
     } catch (RepositoryNotFoundException e) {
       throw new ResourceNotFoundException(rsrc.getName());
-    } finally {
-      if (repo != null) {
-        repo.close();
-      }
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
index b6415b5..7e52381 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
@@ -20,12 +20,12 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CommonConverters;
 import com.google.gerrit.server.args4j.TimestampHandler;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.ReflogEntry;
 import org.eclipse.jgit.lib.ReflogReader;
 import org.eclipse.jgit.lib.Repository;
@@ -75,7 +75,7 @@
   public List<ReflogEntryInfo> apply(BranchResource rsrc) throws AuthException,
       ResourceNotFoundException, RepositoryNotFoundException, IOException {
     if (!rsrc.getControl().isOwner()) {
-      throw new AuthException("no project owner");
+      throw new AuthException("not project owner");
     }
 
     Repository repo = repoManager.openRepository(rsrc.getNameKey());
@@ -122,14 +122,7 @@
     public ReflogEntryInfo(ReflogEntry e) {
       oldId = e.getOldId().getName();
       newId = e.getNewId().getName();
-
-      PersonIdent ident = e.getWho();
-      who = new GitPerson();
-      who.name = ident.getName();
-      who.email = ident.getEmailAddress();
-      who.date = new Timestamp(ident.getWhen().getTime());
-      who.tz = ident.getTimeZoneOffset();
-
+      who = CommonConverters.toGitPerson(e.getWho());
       comment = e.getComment();
     }
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetTag.java
similarity index 62%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/project/GetTag.java
index 407b7c7..5b78e08 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetTag.java
@@ -12,10 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.server.project;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+import com.google.gerrit.extensions.common.TagInfo;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.inject.Singleton;
+
+@Singleton
+public class GetTag implements RestReadView<TagResource> {
+
+  @Override
+  public TagInfo apply(TagResource resource) {
+    return resource.getTagInfo();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
index b09e39f..a8eda97 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
@@ -14,50 +14,74 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
-import com.google.inject.Singleton;
 import com.google.inject.util.Providers;
 
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
+
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.kohsuke.args4j.Option;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
 
-@Singleton
 public class ListBranches implements RestReadView<ProjectResource> {
   private final GitRepositoryManager repoManager;
   private final DynamicMap<RestView<BranchResource>> branchViews;
+  private final WebLinks webLinks;
+
+  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of branches to list")
+  private int limit;
+
+  @Option(name = "--start", aliases = {"-s"}, metaVar = "CNT", usage = "number of branches to skip")
+  private int start;
+
+  @Option(name = "--match", aliases = {"-m"}, metaVar = "MATCH", usage = "match branches substring")
+  private String matchSubstring;
+
+  @Option(name = "--regex", aliases = {"-r"}, metaVar = "REGEX", usage = "match branches regex")
+  private String matchRegex;
 
   @Inject
   public ListBranches(GitRepositoryManager repoManager,
-      DynamicMap<RestView<BranchResource>> branchViews) {
+      DynamicMap<RestView<BranchResource>> branchViews,
+      WebLinks webLinks) {
     this.repoManager = repoManager;
     this.branchViews = branchViews;
+    this.webLinks = webLinks;
   }
 
   @Override
   public List<BranchInfo> apply(ProjectResource rsrc)
-      throws ResourceNotFoundException, IOException {
+      throws ResourceNotFoundException, IOException, BadRequestException {
     List<BranchInfo> branches = Lists.newArrayList();
 
     BranchInfo headBranch = null;
@@ -148,6 +172,73 @@
     if (headBranch != null) {
       branches.add(0, headBranch);
     }
+
+    List<BranchInfo> filteredBranches;
+    if ((matchSubstring != null && !matchSubstring.isEmpty())
+        || (matchRegex != null && !matchRegex.isEmpty())) {
+      filteredBranches = filterBranches(branches);
+    } else {
+      filteredBranches = branches;
+    }
+    if (!filteredBranches.isEmpty()) {
+      int end = filteredBranches.size();
+      if (limit > 0 && start + limit < end) {
+        end = start + limit;
+      }
+      if (start <= end) {
+        filteredBranches = filteredBranches.subList(start, end);
+      } else {
+        filteredBranches = Collections.emptyList();
+      }
+    }
+    return filteredBranches;
+  }
+
+  private List<BranchInfo> filterBranches(List<BranchInfo> branches)
+      throws BadRequestException {
+    if (matchSubstring != null) {
+      return Lists.newArrayList(Iterables.filter(branches,
+          new Predicate<BranchInfo>() {
+            @Override
+            public boolean apply(BranchInfo in) {
+              if (!in.ref.startsWith(Constants.R_HEADS)){
+                return in.ref.toLowerCase(Locale.US).contains(
+                    matchSubstring.toLowerCase(Locale.US));
+              } else {
+                return in.ref.substring(Constants.R_HEADS.length())
+                    .toLowerCase(Locale.US)
+                    .contains(matchSubstring.toLowerCase(Locale.US));
+              }
+            }
+          }));
+    } else if (matchRegex != null) {
+      if (matchRegex.startsWith("^")) {
+        matchRegex = matchRegex.substring(1);
+        if (matchRegex.endsWith("$") && !matchRegex.endsWith("\\$")) {
+          matchRegex = matchRegex.substring(0, matchRegex.length() - 1);
+        }
+      }
+      if (matchRegex.equals(".*")) {
+        return branches;
+      }
+      try {
+        final RunAutomaton a =
+            new RunAutomaton(new RegExp(matchRegex).toAutomaton());
+        return Lists.newArrayList(Iterables.filter(
+            branches, new Predicate<BranchInfo>() {
+              @Override
+              public boolean apply(BranchInfo in) {
+                if (!in.ref.startsWith(Constants.R_HEADS)){
+                  return a.run(in.ref);
+                } else {
+                  return a.run(in.ref.substring(Constants.R_HEADS.length()));
+                }
+              }
+            }));
+      } catch (IllegalArgumentException e) {
+        throw new BadRequestException(e.getMessage());
+      }
+    }
     return branches;
   }
 
@@ -165,6 +256,10 @@
       }
       info.actions.put(d.getId(), new ActionInfo(d));
     }
+    FluentIterable<WebLinkInfo> links =
+        webLinks.getBranchLinks(
+            refControl.getProjectControl().getProject().getName(), ref.getName());
+    info.webLinks = links.isEmpty() ? null : links.toList();
     return info;
   }
 
@@ -173,6 +268,7 @@
     public String revision;
     public Boolean canDelete;
     public Map<String, ActionInfo> actions;
+    public List<WebLinkInfo> webLinks;
 
     public BranchInfo(String ref, String revision, boolean canDelete) {
       this.ref = ref;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
index fbfcc8f..456abfc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
@@ -82,37 +82,27 @@
 
   private List<DashboardInfo> scan(ProjectControl ctl, String project,
       boolean setDefault) throws ResourceNotFoundException, IOException {
-    Repository git;
-    try {
-      git = gitManager.openRepository(ctl.getProject().getNameKey());
+    Project.NameKey projectName = ctl.getProject().getNameKey();
+    try (Repository git = gitManager.openRepository(projectName);
+        RevWalk rw = new RevWalk(git)) {
+      List<DashboardInfo> all = Lists.newArrayList();
+      for (Ref ref : git.getRefDatabase().getRefs(REFS_DASHBOARDS).values()) {
+        if (ctl.controlForRef(ref.getName()).canRead()) {
+          all.addAll(scanDashboards(ctl.getProject(), git, rw, ref,
+              project, setDefault));
+        }
+      }
+      return all;
     } catch (RepositoryNotFoundException e) {
       throw new ResourceNotFoundException();
     }
-    try {
-      RevWalk rw = new RevWalk(git);
-      try {
-        List<DashboardInfo> all = Lists.newArrayList();
-        for (Ref ref : git.getRefDatabase().getRefs(REFS_DASHBOARDS).values()) {
-          if (ctl.controlForRef(ref.getName()).canRead()) {
-            all.addAll(scanDashboards(ctl.getProject(), git, rw, ref,
-                project, setDefault));
-          }
-        }
-        return all;
-      } finally {
-        rw.release();
-      }
-    } finally {
-      git.close();
-    }
   }
 
   private List<DashboardInfo> scanDashboards(Project definingProject,
       Repository git, RevWalk rw, Ref ref, String project, boolean setDefault)
       throws IOException {
     List<DashboardInfo> list = Lists.newArrayList();
-    TreeWalk tw = new TreeWalk(rw.getObjectReader());
-    try {
+    try (TreeWalk tw = new TreeWalk(rw.getObjectReader())) {
       tw.addTree(rw.parseTree(ref.getObjectId()));
       tw.setRecursive(true);
       while (tw.next()) {
@@ -133,8 +123,6 @@
           }
         }
       }
-    } finally {
-      tw.release();
     }
     return list;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
index 3f829c9..a9851e4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
@@ -16,6 +16,8 @@
 
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -38,13 +40,10 @@
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.util.RegexListSearcher;
 import com.google.gerrit.server.util.TreeFormatter;
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import dk.brics.automaton.RegExp;
-import dk.brics.automaton.RunAutomaton;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
@@ -113,7 +112,7 @@
   private final GroupControl.Factory groupControlFactory;
   private final GitRepositoryManager repoManager;
   private final ProjectNode.Factory projectNodeFactory;
-  private final Provider<WebLinks> webLinks;
+  private final WebLinks webLinks;
 
   @Deprecated
   @Option(name = "--format", usage = "(deprecated) output format")
@@ -194,7 +193,7 @@
   protected ListProjects(CurrentUser currentUser, ProjectCache projectCache,
       GroupCache groupCache, GroupControl.Factory groupControlFactory,
       GitRepositoryManager repoManager, ProjectNode.Factory projectNodeFactory,
-      Provider<WebLinks> webLinks) {
+      WebLinks webLinks) {
     this.currentUser = currentUser;
     this.projectCache = projectCache;
     this.groupCache = groupCache;
@@ -384,13 +383,9 @@
             log.warn("Unexpected error reading " + projectName, err);
             continue;
           }
-
-          info.webLinks = Lists.newArrayList();
-          for (WebLinkInfo link : webLinks.get().getProjectLinks(projectName.get())) {
-            if (!Strings.isNullOrEmpty(link.name) && !Strings.isNullOrEmpty(link.url)) {
-              info.webLinks.add(link);
-            }
-          }
+          FluentIterable<WebLinkInfo> links =
+              webLinks.getProjectLinks(projectName.get());
+          info.webLinks = links.isEmpty() ? null : links.toList();
         }
 
         if (foundIndex++ < start) {
@@ -449,42 +444,44 @@
 
   private Iterable<Project.NameKey> scan() throws BadRequestException {
     if (matchPrefix != null) {
+      checkMatchOptions(matchSubstring == null && matchRegex == null);
       return projectCache.byName(matchPrefix);
     } else if (matchSubstring != null) {
+      checkMatchOptions(matchPrefix == null && matchRegex == null);
       return Iterables.filter(projectCache.all(),
           new Predicate<Project.NameKey>() {
+            @Override
             public boolean apply(Project.NameKey in) {
               return in.get().toLowerCase(Locale.US)
                   .contains(matchSubstring.toLowerCase(Locale.US));
             }
           });
     } else if (matchRegex != null) {
-      if (matchRegex.startsWith("^")) {
-        matchRegex = matchRegex.substring(1);
-        if (matchRegex.endsWith("$") && !matchRegex.endsWith("\\$")) {
-          matchRegex = matchRegex.substring(0, matchRegex.length() - 1);
-        }
-      }
-      if (matchRegex.equals(".*")) {
-        return projectCache.all();
-      }
+      checkMatchOptions(matchPrefix == null && matchSubstring == null);
+      RegexListSearcher<Project.NameKey> searcher;
       try {
-        final RunAutomaton a =
-            new RunAutomaton(new RegExp(matchRegex).toAutomaton());
-        return Iterables.filter(projectCache.all(),
-            new Predicate<Project.NameKey>() {
-              public boolean apply(Project.NameKey in) {
-                return a.run(in.get());
-              }
-            });
+        searcher = new RegexListSearcher<Project.NameKey>(matchRegex) {
+          @Override
+          public String apply(Project.NameKey in) {
+            return in.get();
+          }
+        };
       } catch (IllegalArgumentException e) {
         throw new BadRequestException(e.getMessage());
       }
+      return searcher.search(ImmutableList.copyOf(projectCache.all()));
     } else {
       return projectCache.all();
     }
   }
 
+  private static void checkMatchOptions(boolean cond)
+      throws BadRequestException {
+    if (!cond) {
+      throw new BadRequestException("specify exactly one of p/m/r");
+    }
+  }
+
   private void printProjectTree(final PrintWriter stdout,
       final TreeMap<Project.NameKey, ProjectNode> treeMap) {
     final SortedSet<ProjectNode> sortedNodes = new TreeSet<>();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
new file mode 100644
index 0000000..b01e563
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
@@ -0,0 +1,150 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.common.TagInfo;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CommonConverters;
+import com.google.gerrit.server.git.ChangeCache;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.TagCache;
+import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+@Singleton
+public class ListTags implements RestReadView<ProjectResource> {
+  private final GitRepositoryManager repoManager;
+  private final Provider<ReviewDb> dbProvider;
+  private final TagCache tagCache;
+  private final ChangeCache changeCache;
+
+  @Inject
+  public ListTags(GitRepositoryManager repoManager,
+      Provider<ReviewDb> dbProvider,
+      TagCache tagCache,
+      ChangeCache changeCache) {
+    this.repoManager = repoManager;
+    this.dbProvider = dbProvider;
+    this.tagCache = tagCache;
+    this.changeCache = changeCache;
+  }
+
+  @Override
+  public List<TagInfo> apply(ProjectResource resource) throws IOException,
+      ResourceNotFoundException {
+    List<TagInfo> tags = Lists.newArrayList();
+
+    Repository repo = getRepository(resource.getNameKey());
+
+    try {
+      RevWalk rw = new RevWalk(repo);
+      try {
+        Map<String, Ref> all = visibleTags(resource.getControl(), repo,
+            repo.getRefDatabase().getRefs(Constants.R_TAGS));
+        for (Ref ref : all.values()) {
+          tags.add(createTagInfo(ref, rw));
+        }
+      } finally {
+        rw.dispose();
+      }
+    } finally {
+      repo.close();
+    }
+
+    Collections.sort(tags, new Comparator<TagInfo>() {
+      @Override
+      public int compare(TagInfo a, TagInfo b) {
+        return a.ref.compareTo(b.ref);
+      }
+    });
+
+    return tags;
+  }
+
+  public TagInfo get(ProjectResource resource, IdString id)
+      throws ResourceNotFoundException, IOException {
+    try (Repository repo = getRepository(resource.getNameKey());
+        RevWalk rw = new RevWalk(repo)) {
+      String tagName = id.get();
+      if (!tagName.startsWith(Constants.R_TAGS)) {
+        tagName = Constants.R_TAGS + tagName;
+      }
+      Ref ref = repo.getRefDatabase().getRef(tagName);
+      if (ref != null && !visibleTags(resource.getControl(), repo,
+          ImmutableMap.of(ref.getName(), ref)).isEmpty()) {
+        return createTagInfo(ref, rw);
+      }
+    }
+    throw new ResourceNotFoundException(id);
+  }
+
+  private Repository getRepository(Project.NameKey project)
+      throws ResourceNotFoundException, IOException {
+    try {
+      return repoManager.openRepository(project);
+    } catch (RepositoryNotFoundException noGitRepository) {
+      throw new ResourceNotFoundException();
+    }
+  }
+
+  private Map<String, Ref> visibleTags(ProjectControl control, Repository repo,
+      Map<String, Ref> tags) {
+    return new VisibleRefFilter(tagCache, changeCache, repo,
+        control, dbProvider.get(), false).filter(tags, true);
+  }
+
+  private static TagInfo createTagInfo(Ref ref, RevWalk rw)
+      throws MissingObjectException, IOException {
+    RevObject object = rw.parseAny(ref.getObjectId());
+    if (object instanceof RevTag) {
+      RevTag tag = (RevTag)object;
+      // Annotated or signed tag
+      return new TagInfo(
+          Constants.R_TAGS + tag.getTagName(),
+          tag.getName(),
+          tag.getObject().getName(),
+          tag.getFullMessage().trim(),
+          CommonConverters.toGitPerson(tag.getTaggerIdent()));
+    } else {
+      // Lightweight tag
+      return new TagInfo(
+          ref.getName(),
+          ref.getObjectId().getName());
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
index 7b50b0f..430d8f5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
@@ -20,6 +20,7 @@
 import static com.google.gerrit.server.project.DashboardResource.DASHBOARD_KIND;
 import static com.google.gerrit.server.project.FileResource.FILE_KIND;
 import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
+import static com.google.gerrit.server.project.TagResource.TAG_KIND;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiModule;
@@ -37,6 +38,7 @@
     DynamicMap.mapOf(binder(), DASHBOARD_KIND);
     DynamicMap.mapOf(binder(), FILE_KIND);
     DynamicMap.mapOf(binder(), COMMIT_KIND);
+    DynamicMap.mapOf(binder(), TAG_KIND);
 
     put(PROJECT_KIND).to(PutProject.class);
     get(PROJECT_KIND).to(GetProject.class);
@@ -62,6 +64,7 @@
     put(BRANCH_KIND).to(PutBranch.class);
     get(BRANCH_KIND).to(GetBranch.class);
     delete(BRANCH_KIND).to(DeleteBranch.class);
+    post(PROJECT_KIND, "branches:delete").to(DeleteBranches.class);
     install(new FactoryModuleBuilder().build(CreateBranch.Factory.class));
     get(BRANCH_KIND, "reflog").to(GetReflog.class);
     child(BRANCH_KIND, "files").to(FilesCollection.class);
@@ -71,6 +74,9 @@
     get(COMMIT_KIND).to(GetCommit.class);
     child(COMMIT_KIND, "files").to(FilesInCommitCollection.class);
 
+    child(PROJECT_KIND, "tags").to(TagsCollection.class);
+    get(TAG_KIND).to(GetTag.class);
+
     child(PROJECT_KIND, "dashboards").to(DashboardsCollection.class);
     get(DASHBOARD_KIND).to(GetDashboard.class);
     put(DASHBOARD_KIND).to(SetDashboard.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java
index f8cd9c1..689920b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.gerrit.common.ProjectUtil;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupDescription;
@@ -22,7 +22,7 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.errors.ProjectCreationFailedException;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.events.NewProjectCreatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -187,12 +187,13 @@
 
       Project newProject = config.getProject();
       newProject.setDescription(createProjectArgs.projectDescription);
-      newProject.setSubmitType(Objects.firstNonNull(createProjectArgs.submitType,
+      newProject.setSubmitType(MoreObjects.firstNonNull(createProjectArgs.submitType,
           cfg.getEnum("repository", "*", "defaultSubmitType", SubmitType.MERGE_IF_NECESSARY)));
       newProject
           .setUseContributorAgreements(createProjectArgs.contributorAgreements);
       newProject.setUseSignedOffBy(createProjectArgs.signedOffBy);
       newProject.setUseContentMerge(createProjectArgs.contentMerge);
+      newProject.setCreateNewChangeForAllNotInTarget(createProjectArgs.newChangeForAllNotInTarget);
       newProject.setRequireChangeID(createProjectArgs.changeIdRequired);
       newProject.setMaxObjectSizeLimit(createProjectArgs.maxObjectSizeLimit);
       if (createProjectArgs.newParent != null) {
@@ -269,8 +270,7 @@
   private void createEmptyCommits(final Repository repo,
       final Project.NameKey project, final List<String> refs)
       throws IOException {
-    ObjectInserter oi = repo.newObjectInserter();
-    try {
+    try (ObjectInserter oi = repo.newObjectInserter()) {
       CommitBuilder cb = new CommitBuilder();
       cb.setTreeId(oi.insert(Constants.OBJ_TREE, new byte[] {}));
       cb.setAuthor(metaDataUpdateFactory.getUserPersonIdent());
@@ -299,8 +299,6 @@
           "Cannot create empty commit for "
               + createProjectArgs.getProjectName(), e);
       throw e;
-    } finally {
-      oi.release();
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
index 321c2ea..a5cf5d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
@@ -14,17 +14,20 @@
 
 package com.google.gerrit.server.project;
 
-import static com.google.common.base.Objects.firstNonNull;
+import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.gerrit.server.project.RefControl.isRE;
 
+import com.google.auto.value.AutoValue;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import java.util.ArrayList;
@@ -61,23 +64,22 @@
      *        priority order (project specific definitions must appear before
      *        inherited ones).
      * @param ref reference being accessed.
-     * @param usernames if the reference is a per-user reference, access sections
-     *        using the parameter variable "${username}" will first have each of
-     *        {@code usernames} inserted into them before seeing if they apply to
-     *        the reference named by {@code ref}. If null or empty, per-user
-     *        references are ignored.
+     * @param usernameProvider if the reference is a per-user reference, access
+     *        sections using the parameter variable "${username}" will first
+     *        have each of {@code usernames} inserted into them before seeing if
+     *        they apply to the reference named by {@code ref}.
      * @return map of permissions that apply to this reference, keyed by
      *         permission name.
      */
     PermissionCollection filter(Iterable<SectionMatcher> matcherList,
-        String ref, Collection<String> usernames) {
+        String ref, Provider<? extends Collection<String>> usernameProvider) {
       if (isRE(ref)) {
         ref = RefControl.shortestExample(ref);
       } else if (ref.endsWith("/*")) {
         ref = ref.substring(0, ref.length() - 1);
       }
 
-      boolean hasUsernames = usernames != null && !usernames.isEmpty();
+      Collection<String> usernames = null;
       boolean perUser = false;
       Map<AccessSection, Project.NameKey> sectionToProject = Maps.newLinkedHashMap();
       for (SectionMatcher sm : matcherList) {
@@ -93,9 +95,13 @@
         // that will never be shared with non-user references, and the per-user
         // references are usually less frequent than the non-user references.
         //
-        if (hasUsernames) {
-          if (!perUser && sm.matcher instanceof RefPatternMatcher.ExpandParameters) {
-            perUser = ((RefPatternMatcher.ExpandParameters) sm.matcher).matchPrefix(ref);
+        if (sm.matcher instanceof RefPatternMatcher.ExpandParameters) {
+          if (!((RefPatternMatcher.ExpandParameters) sm.matcher).matchPrefix(ref)) {
+            continue;
+          }
+          perUser = true;
+          if (usernames == null) {
+            usernames = usernameProvider.get();
           }
           for (String username : usernames) {
             if (sm.match(ref, username)) {
@@ -110,7 +116,7 @@
       List<AccessSection> sections = Lists.newArrayList(sectionToProject.keySet());
       sorter.sort(ref, sections);
 
-      Set<SeenRule> seen = new HashSet<SeenRule>();
+      Set<SeenRule> seen = new HashSet<>();
       Set<String> exclusiveGroupPermissions = new HashSet<>();
 
       HashMap<String, List<PermissionRule>> permissions = new HashMap<>();
@@ -123,7 +129,7 @@
               exclusiveGroupPermissions.contains(permission.getName());
 
           for (PermissionRule rule : permission.getRules()) {
-            SeenRule s = new SeenRule(section, permission, rule);
+            SeenRule s = SeenRule.create(section, permission, rule);
             boolean addRule;
             if (rule.isBlock()) {
               addRule = true;
@@ -145,7 +151,7 @@
                 p.put(permission.getName(), r);
               }
               r.add(rule);
-              ruleProps.put(rule, new ProjectRef(project, section.getName()));
+              ruleProps.put(rule, ProjectRef.create(project, section.getName()));
             }
           }
 
@@ -220,41 +226,19 @@
   }
 
   /** Tracks whether or not a permission has been overridden. */
-  private static class SeenRule {
-    final String refPattern;
-    final String permissionName;
-    final AccountGroup.UUID group;
+  @AutoValue
+  abstract static class SeenRule {
+    public abstract String refPattern();
+    public abstract String permissionName();
+    @Nullable public abstract AccountGroup.UUID group();
 
-    SeenRule(AccessSection section, Permission permission, PermissionRule rule) {
-      refPattern = section.getName();
-      permissionName = permission.getName();
-      group = rule.getGroup().getUUID();
-    }
-
-    @Override
-    public int hashCode() {
-      int hc = refPattern.hashCode();
-      hc = hc * 31 + permissionName.hashCode();
-      if (group != null) {
-        hc = hc * 31 + group.hashCode();
-      }
-      return hc;
-    }
-
-    @Override
-    public boolean equals(Object other) {
-      if (other instanceof SeenRule) {
-        SeenRule a = this;
-        SeenRule b = (SeenRule) other;
-        return a.refPattern.equals(b.refPattern) //
-            && a.permissionName.equals(b.permissionName) //
-            && eq(a.group, b.group);
-      }
-      return false;
-    }
-
-    private boolean eq(AccountGroup.UUID a, AccountGroup.UUID b) {
-      return a != null && b != null && a.equals(b);
+    static SeenRule create(AccessSection section, Permission permission,
+        @Nullable PermissionRule rule) {
+      AccountGroup.UUID group = rule != null && rule.getGroup() != null
+          ? rule.getGroup().getUUID()
+          : null;
+      return new AutoValue_PermissionCollection_SeenRule(
+          section.getName(), permission.getName(), group);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 1e7a221..3b2dfbc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -18,6 +18,7 @@
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.cache.CacheModule;
@@ -29,6 +30,7 @@
 import com.google.inject.Module;
 import com.google.inject.Singleton;
 import com.google.inject.TypeLiteral;
+import com.google.inject.internal.UniqueAnnotations;
 import com.google.inject.name.Named;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -70,6 +72,9 @@
 
         bind(ProjectCacheImpl.class);
         bind(ProjectCache.class).to(ProjectCacheImpl.class);
+        bind(LifecycleListener.class)
+          .annotatedWith(UniqueAnnotations.create())
+          .to(ProjectCacheWarmer.class);
       }
     };
   }
@@ -157,6 +162,7 @@
   }
 
   /** Invalidate the cached information about the given project. */
+  @Override
   public void evict(final Project.NameKey p) {
     if (p != null) {
       byName.invalidate(p.get());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheWarmer.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
new file mode 100644
index 0000000..2cdb172
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+@Singleton
+public class ProjectCacheWarmer implements LifecycleListener {
+  private static final Logger log =
+      LoggerFactory.getLogger(ProjectCacheWarmer.class);
+
+  private final Config config;
+  private final ProjectCache cache;
+
+  @Inject
+  ProjectCacheWarmer(@GerritServerConfig Config config, ProjectCache cache) {
+    this.config = config;
+    this.cache = cache;
+  }
+
+  @Override
+  public void start() {
+    int cpus = Runtime.getRuntime().availableProcessors();
+    if (config.getBoolean("cache", "projects", "loadOnStartup", false)) {
+      final ThreadPoolExecutor pool =
+          new ScheduledThreadPoolExecutor(config.getInt("cache", "projects",
+              "loadThreads", cpus), new ThreadFactoryBuilder().setNameFormat(
+              "ProjectCacheLoader-%d").build());
+
+      log.info("Loading project cache");
+      pool.execute(new Runnable() {
+        @Override
+        public void run() {
+          for (final Project.NameKey name : cache.all()) {
+            pool.execute(new Runnable() {
+              @Override
+              public void run() {
+                cache.get(name);
+              }
+            });
+          }
+          pool.shutdown();
+        }
+      });
+    }
+  }
+
+  @Override
+  public void stop() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index 7eda31f..3bbcf71 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AccessSection;
@@ -30,22 +30,25 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.InternalUser;
+import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.change.IncludedInResolver;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GitReceivePackGroups;
 import com.google.gerrit.server.config.GitUploadPackGroups;
+import com.google.gerrit.server.git.ChangeCache;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.TagCache;
+import com.google.gerrit.server.git.VisibleRefFilter;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -53,6 +56,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -148,6 +152,8 @@
   private final ChangeControl.AssistedFactory changeControlFactory;
   private final PermissionCollection.Factory permissionFilter;
   private final Collection<ContributorAgreement> contributorAgreements;
+  private final TagCache tagCache;
+  private final ChangeCache changeCache;
 
   private List<SectionMatcher> allSections;
   private List<SectionMatcher> localSections;
@@ -162,11 +168,15 @@
       PermissionCollection.Factory permissionFilter,
       GitRepositoryManager repoManager,
       ChangeControl.AssistedFactory changeControlFactory,
+      TagCache tagCache,
+      ChangeCache changeCache,
       @CanonicalWebUrl @Nullable String canonicalWebUrl,
       @Assisted CurrentUser who,
       @Assisted ProjectState ps) {
     this.repoManager = repoManager;
     this.changeControlFactory = changeControlFactory;
+    this.tagCache = tagCache;
+    this.changeCache = changeCache;
     this.uploadGroups = uploadGroups;
     this.receiveGroups = receiveGroups;
     this.permissionFilter = permissionFilter;
@@ -197,15 +207,25 @@
     }
     RefControl ctl = refControls.get(refName);
     if (ctl == null) {
-      ImmutableList.Builder<String> usernames = ImmutableList.<String> builder();
-      if (user.getUserName() != null) {
-        usernames.add(user.getUserName());
-      }
-      if (user instanceof IdentifiedUser) {
-        usernames.addAll(((IdentifiedUser) user).getEmailAddresses());
-      }
+      Provider<List<String>> usernames = new Provider<List<String>>() {
+        @Override
+        public List<String> get() {
+          List<String> r;
+          if (user.isIdentifiedUser()) {
+            Set<String> emails = ((IdentifiedUser) user).getEmailAddresses();
+            r = new ArrayList<>(emails.size() + 1);
+            r.addAll(emails);
+          } else {
+            r = new ArrayList<>(1);
+          }
+          if (user.getUserName() != null) {
+            r.add(user.getUserName());
+          }
+          return r;
+        }
+      };
       PermissionCollection relevant =
-          permissionFilter.filter(access(), refName, usernames.build());
+          permissionFilter.filter(access(), refName, usernames);
       ctl = new RefControl(this, refName, relevant);
       refControls.put(refName, ctl);
     }
@@ -233,7 +253,7 @@
 
   private boolean isHidden() {
     return getProject().getState().equals(
-        com.google.gerrit.extensions.api.projects.ProjectState.HIDDEN);
+        com.google.gerrit.extensions.client.ProjectState.HIDDEN);
   }
 
   /** Can this user see this project exists? */
@@ -263,12 +283,12 @@
 
   /** Can this user see all the refs in this projects? */
   public boolean allRefsAreVisible() {
-    return allRefsAreVisibleExcept(Collections.<String> emptySet());
+    return allRefsAreVisible(Collections.<String> emptySet());
   }
 
-  public boolean allRefsAreVisibleExcept(Set<String> except) {
+  public boolean allRefsAreVisible(Set<String> ignore) {
     return user instanceof InternalUser
-        || canPerformOnAllRefs(Permission.READ, except);
+        || canPerformOnAllRefs(Permission.READ, ignore);
   }
 
   /** Is this user a project owner? Ownership does not imply {@link #isVisible()} */
@@ -279,7 +299,8 @@
 
   private boolean isDeclaredOwner() {
     if (declaredOwner == null) {
-      declaredOwner = state.isOwner(user.getEffectiveGroups());
+      GroupMembership effectiveGroups = user.getEffectiveGroups();
+      declaredOwner = effectiveGroups.containsAnyOf(state.getAllOwners());
     }
     return declaredOwner;
   }
@@ -426,7 +447,7 @@
     return false;
   }
 
-  private boolean canPerformOnAllRefs(String permission, Set<String> except) {
+  private boolean canPerformOnAllRefs(String permission, Set<String> ignore) {
     boolean canPerform = false;
     Set<String> patterns = allRefPatterns(permission);
     if (patterns.contains(AccessSection.ALL)) {
@@ -437,7 +458,7 @@
       for (final String pattern : patterns) {
         if (controlForRef(pattern).canPerform(permission)) {
           canPerform = true;
-        } else if (except.contains(pattern)) {
+        } else if (ignore.contains(pattern)) {
           continue;
         } else {
           return false;
@@ -513,40 +534,38 @@
     return false;
   }
 
-  public boolean canReadCommit(RevWalk rw, RevCommit commit) {
-    if (controlForRef("refs/*").canPerform(Permission.READ)) {
-      return true;
-    }
-
-    Project.NameKey projName = state.getProject().getNameKey();
+  public boolean canReadCommit(ReviewDb db, RevWalk rw, RevCommit commit) {
     try {
-      Repository repo = repoManager.openRepository(projName);
+      Repository repo = openRepository();
       try {
-        RefDatabase refDb = repo.getRefDatabase();
-        List<Ref> allRefs = Lists.newLinkedList();
-        allRefs.addAll(refDb.getRefs(Constants.R_HEADS).values());
-        allRefs.addAll(refDb.getRefs(Constants.R_TAGS).values());
-        List<Ref> canReadRefs = Lists.newLinkedList();
-        for (Ref r : allRefs) {
-          if (controlForRef(r.getName()).canPerform(Permission.READ)) {
-            canReadRefs.add(r);
-          }
-        }
-
-        if (!canReadRefs.isEmpty() && IncludedInResolver.includedInOne(
-            repo, rw, commit, canReadRefs)) {
-          return true;
-        }
+        return isMergedIntoVisibleRef(repo, db, rw, commit,
+            repo.getAllRefs().values());
       } finally {
         repo.close();
       }
     } catch (IOException e) {
-      String msg =
-          String.format(
-              "Cannot verify permissions to commit object %s in repository %s",
-              commit.name(), projName.get());
+      String msg = String.format(
+          "Cannot verify permissions to commit object %s in repository %s",
+          commit.name(), getProject().getNameKey());
       log.error(msg, e);
+      return false;
     }
-    return false;
+  }
+
+  boolean isMergedIntoVisibleRef(Repository repo, ReviewDb db, RevWalk rw,
+      RevCommit commit, Collection<Ref> unfilteredRefs) throws IOException {
+    VisibleRefFilter filter =
+        new VisibleRefFilter(tagCache, changeCache, repo, this, db, true);
+    Map<String, Ref> m = Maps.newHashMapWithExpectedSize(unfilteredRefs.size());
+    for (Ref r : unfilteredRefs) {
+      m.put(r.getName(), r);
+    }
+    Map<String, Ref> refs = filter.filter(m, true);
+    return !refs.isEmpty()
+        && IncludedInResolver.includedInOne(repo, rw, commit, refs.values());
+  }
+
+  Repository openRepository() throws IOException {
+    return repoManager.openRepository(getProject().getNameKey());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectJson.java
index 4cafc0a..6415664 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectJson.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.project;
 
 import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
+import com.google.common.collect.FluentIterable;
 import com.google.gerrit.extensions.common.ProjectInfo;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import com.google.gerrit.extensions.restapi.Url;
@@ -24,18 +24,17 @@
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
 public class ProjectJson {
 
   private final AllProjectsName allProjects;
-  private final Provider<WebLinks> webLinks;
+  private final WebLinks webLinks;
 
   @Inject
   ProjectJson(AllProjectsNameProvider allProjectsNameProvider,
-      Provider<WebLinks> webLinks) {
+      WebLinks webLinks) {
     this.allProjects = allProjectsNameProvider.get();
     this.webLinks = webLinks;
   }
@@ -52,14 +51,9 @@
     info.description = Strings.emptyToNull(p.getDescription());
     info.state = p.getState();
     info.id = Url.encode(info.name);
-
-    info.webLinks = Lists.newArrayList();
-    for (WebLinkInfo link : webLinks.get().getProjectLinks(p.getName())) {
-      if (!Strings.isNullOrEmpty(link.name) && !Strings.isNullOrEmpty(link.url)) {
-        info.webLinks.add(link);
-      }
-    }
-
+    FluentIterable<WebLinkInfo> links =
+        webLinks.getProjectLinks(p.getName());
+    info.webLinks = links.isEmpty() ? null : links.toList();
     return info;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectRef.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectRef.java
index 0315fad..8d3185d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectRef.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectRef.java
@@ -14,32 +14,15 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.auto.value.AutoValue;
 import com.google.gerrit.reviewdb.client.Project;
 
-class ProjectRef {
+@AutoValue
+abstract class ProjectRef {
+  public abstract Project.NameKey project();
+  public abstract String ref();
 
-  final Project.NameKey project;
-  final String ref;
-
-  ProjectRef(Project.NameKey project, String ref) {
-    this.project = project;
-    this.ref = ref;
-  }
-
-  @Override
-  public boolean equals(Object other) {
-    return other instanceof ProjectRef
-        && project.equals(((ProjectRef) other).project)
-        && ref.equals(((ProjectRef) other).ref);
-  }
-
-  @Override
-  public int hashCode() {
-    return project.hashCode() * 31 + ref.hashCode();
-  }
-
-  @Override
-  public String toString() {
-    return project + ", " + ref;
+  static ProjectRef create(Project.NameKey project, String ref) {
+    return new AutoValue_ProjectRef(project, ref);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java
index d6a2e09..b8830a0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.extensions.api.projects.ProjectState;
+import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.Project;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index b9cead3..558b572 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -18,7 +18,6 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Function;
-import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
@@ -30,7 +29,7 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.extensions.common.InheritableBoolean;
+import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -38,7 +37,6 @@
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.CapabilityCollection;
-import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.BranchOrderSection;
@@ -314,16 +312,20 @@
   }
 
   /**
-   * @return true if any of the groups listed in {@code groups} was declared to
-   *         be an owner of this project, or one of its parent projects..
+   * @return all {@link AccountGroup}'s that are allowed to administrate the
+   *         complete project. This includes all groups to which the owner
+   *         privilege for 'refs/*' is assigned for this project (the local
+   *         owners) and all groups to which the owner privilege for 'refs/*' is
+   *         assigned for one of the parent projects (the inherited owners).
    */
-  boolean isOwner(final GroupMembership groups) {
-    return Iterables.any(tree(), new Predicate<ProjectState>() {
-      @Override
-      public boolean apply(ProjectState in) {
-        return groups.containsAnyOf(in.localOwners);
-      }
-    });
+  public Set<AccountGroup.UUID> getAllOwners() {
+    Set<AccountGroup.UUID> result = new HashSet<>();
+
+    for (ProjectState p : tree()) {
+      result.addAll(p.localOwners);
+    }
+
+    return result;
   }
 
   public ProjectControl controlFor(final CurrentUser user) {
@@ -405,6 +407,15 @@
     });
   }
 
+  public boolean isCreateNewChangeForAllNotInTarget() {
+    return getInheritableBoolean(new Function<Project, InheritableBoolean>() {
+      @Override
+      public InheritableBoolean apply(Project input) {
+        return input.getCreateNewChangeForAllNotInTarget();
+      }
+    });
+  }
+
   public LabelTypes getLabelTypes() {
     Map<String, LabelType> types = Maps.newLinkedHashMap();
     for (ProjectState s : treeInOrder()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
index 519f4f2..0ffbe3e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
@@ -29,6 +29,8 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.lib.Constants;
+
 import java.io.IOException;
 
 @Singleton
@@ -88,6 +90,9 @@
   }
 
   private ProjectResource _parse(String id) throws IOException {
+    if (id.endsWith(Constants.DOT_GIT_EXT)) {
+      id = id.substring(0, id.length() - Constants.DOT_GIT_EXT.length());
+    }
     ProjectControl ctl;
     try {
       ctl = controlFactory.controlFor(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
index 9fa9b52..ca15287 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -16,12 +16,11 @@
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Joiner;
-import com.google.common.base.Objects;
 import com.google.common.base.Strings;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.extensions.api.projects.ProjectInput.ConfigValue;
-import com.google.gerrit.extensions.common.InheritableBoolean;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -32,11 +31,11 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.ProjectConfigEntry;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
@@ -57,6 +56,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Objects;
 
 @Singleton
 public class PutConfig implements RestModifyView<ProjectResource, Input> {
@@ -66,10 +66,11 @@
     public InheritableBoolean useContributorAgreements;
     public InheritableBoolean useContentMerge;
     public InheritableBoolean useSignedOffBy;
+    public InheritableBoolean createNewChangeForAllNotInTarget;
     public InheritableBoolean requireChangeId;
     public String maxObjectSizeLimit;
     public SubmitType submitType;
-    public com.google.gerrit.extensions.api.projects.ProjectState state;
+    public com.google.gerrit.extensions.client.ProjectState state;
     public Map<String, Map<String, ConfigValue>> pluginConfigValues;
   }
 
@@ -146,6 +147,11 @@
       if (input.useSignedOffBy != null) {
         p.setUseSignedOffBy(input.useSignedOffBy);
       }
+
+      if (input.createNewChangeForAllNotInTarget != null) {
+        p.setCreateNewChangeForAllNotInTarget(input.createNewChangeForAllNotInTarget);
+      }
+
       if (input.requireChangeId != null) {
         p.setRequireChangeID(input.requireChangeId);
       }
@@ -172,12 +178,12 @@
         ObjectId baseRev = projectConfig.getRevision();
         ObjectId commitRev = projectConfig.commit(md);
         // Only fire hook if project was actually changed.
-        if (!Objects.equal(baseRev, commitRev)) {
+        if (!Objects.equals(baseRev, commitRev)) {
           IdentifiedUser user = (IdentifiedUser) currentUser.get();
           hooks.doRefUpdatedHook(
             new Branch.NameKey(projectName, RefNames.REFS_CONFIG),
             baseRev, commitRev, user.getAccount());
-        };
+        }
         projectCache.evict(projectConfig.getProject());
         gitMgr.setProjectDescription(projectName, p.getDescription());
       } catch (IOException e) {
@@ -247,6 +253,7 @@
                           "The value '%s' is not permitted for parameter '%s' of plugin '"
                               + pluginName + "'", value, v.getKey()));
                     }
+                    //$FALL-THROUGH$
                   case STRING:
                     cfg.setString(v.getKey(), value);
                     break;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
index b8a4c7c..536bfa7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -39,6 +39,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 
 import java.io.IOException;
+import java.util.Objects;
 
 @Singleton
 class PutDescription implements RestModifyView<ProjectResource, Input> {
@@ -85,7 +86,7 @@
         Project project = config.getProject();
         project.setDescription(Strings.emptyToNull(input.description));
 
-        String msg = Objects.firstNonNull(
+        String msg = MoreObjects.firstNonNull(
           Strings.emptyToNull(input.commitMessage),
           "Updated description.\n");
         if (!msg.endsWith("\n")) {
@@ -96,7 +97,7 @@
         ObjectId baseRev = config.getRevision();
         ObjectId commitRev = config.commit(md);
         // Only fire hook if project was actually changed.
-        if (!Objects.equal(baseRev, commitRev)) {
+        if (!Objects.equals(baseRev, commitRev)) {
           hooks.doRefUpdatedHook(
             new Branch.NameKey(resource.getNameKey(), RefNames.REFS_CONFIG),
             baseRev, commitRev, user.getAccount());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index 7dc7a2a..71c3104 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -22,8 +22,9 @@
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.RefConfigSection;
 import com.google.gerrit.common.errors.InvalidNameException;
-import com.google.gerrit.extensions.api.projects.ProjectState;
+import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.InternalUser;
@@ -31,12 +32,16 @@
 
 import dk.brics.automaton.RegExp;
 
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTag;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -49,6 +54,8 @@
 
 /** Manages access control for Git references (aka branches, tags). */
 public class RefControl {
+  private static final Logger log = LoggerFactory.getLogger(RefControl.class);
+
   private final ProjectControl projectControl;
   private final String refName;
 
@@ -237,12 +244,13 @@
   /**
    * Determines whether the user can create a new Git ref.
    *
-   * @param rw revision pool {@code object} was parsed in.
+   * @param db db for checking change visibility.
+   * @param rw revision pool {@code object} was parsed in; must be reset before
+   *     calling this method.
    * @param object the object the user will start the reference with.
-   * @param existsOnServer the object exists on server or not.
    * @return {@code true} if the user specified can create a new Git ref
    */
-  public boolean canCreate(RevWalk rw, RevObject object, boolean existsOnServer) {
+  public boolean canCreate(ReviewDb db, RevWalk rw, RevObject object) {
     if (!canWrite()) {
       return false;
     }
@@ -261,10 +269,24 @@
     }
 
     if (object instanceof RevCommit) {
-      return admin
-          || (owner && !isBlocked(Permission.CREATE))
-          || (canPerform(Permission.CREATE) && (!existsOnServer && canUpdate() || projectControl
-              .canReadCommit(rw, (RevCommit) object)));
+      if (admin || (owner && !isBlocked(Permission.CREATE))) {
+        // Admin or project owner; bypass visibility check.
+        return true;
+      } else if (!canPerform(Permission.CREATE)) {
+        // No create permissions.
+        return false;
+      } else if (canUpdate()) {
+        // If the user has push permissions, they can create the ref regardless
+        // of whether they are pushing any new objects along with the create.
+        return true;
+      } else if (isMergedIntoBranchOrTag(db, rw, (RevCommit) object)) {
+        // If the user has no push permissions, check whether the object is
+        // merged into a branch or tag readable by this user. If so, they are
+        // not effectively "pushing" more objects, so they can create the ref
+        // even if they don't have push permission.
+        return true;
+      }
+      return false;
     } else if (object instanceof RevTag) {
       final RevTag tag = (RevTag) object;
       try {
@@ -281,7 +303,7 @@
         if (getCurrentUser().isIdentifiedUser()) {
           final IdentifiedUser user = (IdentifiedUser) getCurrentUser();
           final String addr = tagger.getEmailAddress();
-          valid = user.getEmailAddresses().contains(addr);
+          valid = user.hasEmailAddress(addr);
         } else {
           valid = false;
         }
@@ -303,6 +325,28 @@
     }
   }
 
+  private boolean isMergedIntoBranchOrTag(ReviewDb db, RevWalk rw,
+      RevCommit commit) {
+    try {
+      Repository repo = projectControl.openRepository();
+      try {
+        List<Ref> refs = new ArrayList<>(
+            repo.getRefDatabase().getRefs(Constants.R_HEADS).values());
+        refs.addAll(repo.getRefDatabase().getRefs(Constants.R_TAGS).values());
+        return projectControl.isMergedIntoVisibleRef(
+            repo, db, rw, commit, refs);
+      } finally {
+        repo.close();
+      }
+    } catch (IOException e) {
+      String msg = String.format(
+          "Cannot verify permissions to commit object %s in repository %s",
+          commit.name(), projectControl.getProject().getNameKey());
+      log.error(msg, e);
+    }
+    return false;
+  }
+
   /**
    * Determines whether the user can delete the Git ref controlled by this
    * object.
@@ -386,6 +430,11 @@
     return canPerform(Permission.EDIT_TOPIC_NAME);
   }
 
+  /** @return true if this user can edit hashtag names. */
+  public boolean canEditHashtags() {
+    return canPerform(Permission.EDIT_HASHTAGS);
+  }
+
   /** @return true if this user can force edit topic names. */
   public boolean canForceEditTopicName() {
     return canForcePerform(Permission.EDIT_TOPIC_NAME);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPatternMatcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPatternMatcher.java
index d882e0d..d80323f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPatternMatcher.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPatternMatcher.java
@@ -15,8 +15,11 @@
 package com.google.gerrit.server.project;
 
 import static com.google.gerrit.server.project.RefControl.isRE;
+
 import com.google.gerrit.common.data.ParameterizedString;
+
 import dk.brics.automaton.Automaton;
+
 import java.util.Collections;
 import java.util.regex.Pattern;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
index c012bd5..9009aad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.auto.value.AutoValue;
 import com.google.common.cache.Cache;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.util.MostSpecificComparator;
@@ -26,7 +28,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.Arrays;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.List;
@@ -62,7 +64,7 @@
       return;
     }
 
-    EntryKey key = new EntryKey(ref, sections);
+    EntryKey key = EntryKey.create(ref, sections);
     EntryVal val = cache.getIfPresent(key);
     if (val != null) {
       int[] srcIdx = val.order;
@@ -116,35 +118,27 @@
     return true;
   }
 
-  static final class EntryKey {
-    private final String ref;
-    private final String[] patterns;
-    private final int hashCode;
+  @AutoValue
+  abstract static class EntryKey {
+    public abstract String ref();
+    public abstract List<String> patterns();
+    public abstract int cachedHashCode();
 
-    EntryKey(String refName, List<AccessSection> sections) {
+    static EntryKey create(String refName, List<AccessSection> sections) {
       int hc = refName.hashCode();
-      ref = refName;
-      patterns = new String[sections.size()];
-      for (int i = 0; i < patterns.length; i++) {
-        String n = sections.get(i).getName();
-        patterns[i] = n;
+      List<String> patterns = new ArrayList<>(sections.size());
+      for (AccessSection s : sections) {
+        String n = s.getName();
+        patterns.add(n);
         hc = hc * 31 + n.hashCode();
       }
-      hashCode = hc;
+      return new AutoValue_SectionSortCache_EntryKey(
+          refName, ImmutableList.copyOf(patterns), hc);
     }
 
     @Override
     public int hashCode() {
-      return hashCode;
-    }
-
-    @Override
-    public boolean equals(Object other) {
-      if (other instanceof EntryKey) {
-        EntryKey b = (EntryKey) other;
-        return ref.equals(b.ref) && Arrays.equals(patterns, b.patterns);
-      }
-      return false;
+      return cachedHashCode();
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
index a323ec8..b18b8ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -97,7 +97,7 @@
           project.setLocalDefaultDashboard(input.id);
         }
 
-        String msg = Objects.firstNonNull(
+        String msg = MoreObjects.firstNonNull(
           Strings.emptyToNull(input.commitMessage),
           input.id == null
             ? "Removed default dashboard.\n"
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
index 7efc1b7..77c221c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
@@ -17,13 +17,13 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.events.HeadUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.auth.AuthException;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.SetHead.Input;
 import com.google.inject.Inject;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
index 81ce582..4ff2b0f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
@@ -77,7 +77,8 @@
         if (msg == null) {
           msg = String.format(
               "Changed parent to %s.\n",
-              Objects.firstNonNull(project.getParentName(), allProjects.get()));
+              MoreObjects.firstNonNull(project.getParentName(),
+                  allProjects.get()));
         } else if (!msg.endsWith("\n")) {
           msg += "\n";
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index d79716c..ba9e6b1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -14,24 +14,38 @@
 
 package com.google.gerrit.server.project;
 
-import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.rules.PrologEnvironment;
+import com.google.gerrit.rules.ReductionLimitException;
 import com.google.gerrit.rules.StoredValues;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
 
 import com.googlecode.prolog_cafe.compiler.CompileException;
+import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.ListTerm;
 import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.PrologException;
+import com.googlecode.prolog_cafe.lang.StructureTerm;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
 import com.googlecode.prolog_cafe.lang.VariableTerm;
 
-import java.io.InputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -42,155 +56,479 @@
  * all the way up to All-Projects.
  */
 public class SubmitRuleEvaluator {
-  private final ReviewDb db;
-  private final PatchSet patchSet;
-  private final ProjectControl projectControl;
-  private final ChangeControl changeControl;
-  private final Change change;
+  private static final Logger log = LoggerFactory
+      .getLogger(SubmitRuleEvaluator.class);
+
+  private static final String DEFAULT_MSG =
+      "Error evaluating project rules, check server log";
+
+  public static List<SubmitRecord> defaultRuleError() {
+    return createRuleError(DEFAULT_MSG);
+  }
+
+  public static List<SubmitRecord> createRuleError(String err) {
+    SubmitRecord rec = new SubmitRecord();
+    rec.status = SubmitRecord.Status.RULE_ERROR;
+    rec.errorMessage = err;
+    return Collections.singletonList(rec);
+  }
+
+  public static SubmitTypeRecord defaultTypeError() {
+    return createTypeError(DEFAULT_MSG);
+  }
+
+  public static SubmitTypeRecord createTypeError(String err) {
+    SubmitTypeRecord rec = new SubmitTypeRecord();
+    rec.status = SubmitTypeRecord.Status.RULE_ERROR;
+    rec.errorMessage = err;
+    return rec;
+  }
+
+  /**
+   * Exception thrown when the label term of a submit record
+   * unexpectedly didn't contain a user term.
+   */
+  private static class UserTermExpected extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public UserTermExpected(SubmitRecord.Label label) {
+      super(String.format("A label with the status %s must contain a user.",
+          label.toString()));
+    }
+  }
+
   private final ChangeData cd;
-  private final boolean fastEvalLabels;
-  private final String userRuleLocatorName;
-  private final String userRuleWrapperName;
-  private final String filterRuleLocatorName;
-  private final String filterRuleWrapperName;
-  private final boolean skipFilters;
-  private final InputStream rulesInputStream;
+  private final ChangeControl control;
+
+  private PatchSet patchSet;
+  private boolean fastEvalLabels;
+  private boolean allowDraft;
+  private boolean allowClosed;
+  private boolean skipFilters;
+  private String rule;
+  private boolean logErrors = true;
+  private int reductionsConsumed;
 
   private Term submitRule;
-  private String projectName;
 
-  /**
-   * @param userRuleLocatorName The name of the rule used to locate the
-   *        user-supplied rule.
-   * @param userRuleWrapperName The name of the wrapper rule used to evaluate
-   *        the user-supplied rule.
-   * @param filterRuleLocatorName The name of the rule used to locate the filter
-   *        rule.
-   * @param filterRuleWrapperName The name of the rule used to evaluate the
-   *        filter rule.
-   */
-  public SubmitRuleEvaluator(ReviewDb db, PatchSet patchSet,
-      ProjectControl projectControl,
-      ChangeControl changeControl, Change change, ChangeData cd,
-      boolean fastEvalLabels,
-      String userRuleLocatorName, String userRuleWrapperName,
-      String filterRuleLocatorName, String filterRuleWrapperName) {
-    this(db, patchSet, projectControl, changeControl, change, cd,
-        fastEvalLabels, userRuleLocatorName, userRuleWrapperName,
-        filterRuleLocatorName, filterRuleWrapperName, false, null);
+  public SubmitRuleEvaluator(ChangeData cd) throws OrmException {
+    this.cd = cd;
+    this.control = cd.changeControl();
   }
 
   /**
-   * @param userRuleLocatorName The name of the rule used to locate the
-   *        user-supplied rule.
-   * @param userRuleWrapperName The name of the wrapper rule used to evaluate
-   *        the user-supplied rule.
-   * @param filterRuleLocatorName The name of the rule used to locate the filter
-   *        rule.
-   * @param filterRuleWrapperName The name of the rule used to evaluate the
-   *        filter rule.
-   * @param skipSubmitFilters if {@code true} submit filter will not be
-   *        applied
-   * @param rules when non-null the rules will be read from this input stream
-   *        instead of refs/meta/config:rules.pl file
+   * @param ps patch set of the change to evaluate. If not set, the current
+   * patch set will be loaded from {@link #canSubmit()} or {@link
+   * #getSubmitType}.
+   * @return this
    */
-  public SubmitRuleEvaluator(ReviewDb db, PatchSet patchSet,
-      ProjectControl projectControl,
-      ChangeControl changeControl, Change change, ChangeData cd,
-      boolean fastEvalLabels,
-      String userRuleLocatorName, String userRuleWrapperName,
-      String filterRuleLocatorName, String filterRuleWrapperName,
-      boolean skipSubmitFilters, InputStream rules) {
-    this.db = db;
-    this.patchSet = patchSet;
-    this.projectControl = projectControl;
-    this.changeControl = changeControl;
-    this.change = change;
-    this.cd = checkNotNull(cd, "ChangeData");
-    this.fastEvalLabels = fastEvalLabels;
-    this.userRuleLocatorName = userRuleLocatorName;
-    this.userRuleWrapperName = userRuleWrapperName;
-    this.filterRuleLocatorName = filterRuleLocatorName;
-    this.filterRuleWrapperName = filterRuleWrapperName;
-    this.skipFilters = skipSubmitFilters;
-    this.rulesInputStream = rules;
+  public SubmitRuleEvaluator setPatchSet(PatchSet ps) {
+    checkArgument(ps.getId().getParentKey().equals(cd.getId()),
+        "Patch set %s does not match change %s", ps.getId(), cd.getId());
+    patchSet = ps;
+    return this;
   }
 
   /**
-   * Evaluates the given rule and filters.
-   *
-   * Sets the {@link #submitRule} to the Term found by the
-   * {@link #userRuleLocatorName}. This can be used when reporting error(s) on
-   * unexpected return value of this method.
-   *
-   * @return List of {@link Term} objects returned from the evaluated rules.
-   * @throws RuleEvalException
+   * @param fast if true, infer label information from rules rather than reading
+   *     from project config.
+   * @return this
    */
-  public List<Term> evaluate() throws RuleEvalException {
-    PrologEnvironment env = getPrologEnvironment();
+  public SubmitRuleEvaluator setFastEvalLabels(boolean fast) {
+    fastEvalLabels = fast;
+    return this;
+  }
+
+  /**
+   * @param allow whether to allow {@link #canSubmit()} on closed changes.
+   * @return this
+   */
+  public SubmitRuleEvaluator setAllowClosed(boolean allow) {
+    allowClosed = allow;
+    return this;
+  }
+
+  /**
+   * @param allow whether to allow {@link #canSubmit()} on closed changes.
+   * @return this
+   */
+  public SubmitRuleEvaluator setAllowDraft(boolean allow) {
+    allowDraft = allow;
+    return this;
+  }
+
+  /**
+   * @param skip if true, submit filter will not be applied.
+   * @return this
+   */
+  public SubmitRuleEvaluator setSkipSubmitFilters(boolean skip) {
+    skipFilters = skip;
+    return this;
+  }
+
+  /**
+   * @param rule custom rule to use, or null to use refs/meta/config:rules.pl.
+   * @return this
+   */
+  public SubmitRuleEvaluator setRule(@Nullable String rule) {
+    this.rule = rule;
+    return this;
+  }
+
+  /**
+   * @param log whether to log error messages in addition to returning error
+   *     records. If true, error record messages will be less descriptive.
+   */
+  public SubmitRuleEvaluator setLogErrors(boolean log) {
+    logErrors = log;
+    return this;
+  }
+
+  /** @return Prolog reductions consumed during evaluation. */
+  public int getReductionsConsumed() {
+    return reductionsConsumed;
+  }
+
+  /**
+   * Evaluate the submit rules.
+   *
+   * @return List of {@link SubmitRecord} objects returned from the evaluated
+   *     rules, including any errors.
+   */
+  public List<SubmitRecord> canSubmit() {
     try {
-      submitRule = env.once("gerrit", userRuleLocatorName, new VariableTerm());
+      initPatchSet();
+    } catch (OrmException e) {
+      return ruleError("Error looking up patch set "
+          + control.getChange().currentPatchSetId());
+    }
+    Change c = control.getChange();
+    if (!allowClosed && c.getStatus().isClosed()) {
+      SubmitRecord rec = new SubmitRecord();
+      rec.status = SubmitRecord.Status.CLOSED;
+      return Collections.singletonList(rec);
+    }
+    if ((c.getStatus() == Change.Status.DRAFT || patchSet.isDraft())
+        && !allowDraft) {
+      return cannotSubmitDraft();
+    }
+
+    List<Term> results;
+    try {
+      results = evaluateImpl("locate_submit_rule", "can_submit",
+          "locate_submit_filter", "filter_submit_results",
+          control.getCurrentUser());
+    } catch (RuleEvalException e) {
+      return ruleError(e.getMessage(), e);
+    }
+
+    if (results.isEmpty()) {
+      // This should never occur. A well written submit rule will always produce
+      // at least one result informing the caller of the labels that are
+      // required for this change to be submittable. Each label will indicate
+      // whether or not that is actually possible given the permissions.
+      return ruleError(String.format("Submit rule '%s' for change %s of %s has "
+            + "no solution.", getSubmitRule(), cd.getId(), getProjectName()));
+    }
+
+    return resultsToSubmitRecord(getSubmitRule(), results);
+  }
+
+  private List<SubmitRecord> cannotSubmitDraft() {
+    try {
+      if (!control.isDraftVisible(cd.db(), cd)) {
+        return createRuleError("Patch set " + patchSet.getId() + " not found");
+      } else if (patchSet.isDraft()) {
+        return createRuleError("Cannot submit draft patch sets");
+      } else {
+        return createRuleError("Cannot submit draft changes");
+      }
+    } catch (OrmException err) {
+      String msg = "Cannot check visibility of patch set " + patchSet.getId();
+      log.error(msg, err);
+      return createRuleError(msg);
+    }
+  }
+
+  /**
+   * Convert the results from Prolog Cafe's format to Gerrit's common format.
+   *
+   * can_submit/1 terminates when an ok(P) record is found. Therefore walk
+   * the results backwards, using only that ok(P) record if it exists. This
+   * skips partial results that occur early in the output. Later after the loop
+   * the out collection is reversed to restore it to the original ordering.
+   */
+  private List<SubmitRecord> resultsToSubmitRecord(
+      Term submitRule, List<Term> results) {
+    List<SubmitRecord> out = new ArrayList<>(results.size());
+    for (int resultIdx = results.size() - 1; 0 <= resultIdx; resultIdx--) {
+      Term submitRecord = results.get(resultIdx);
+      SubmitRecord rec = new SubmitRecord();
+      out.add(rec);
+
+      if (!submitRecord.isStructure() || 1 != submitRecord.arity()) {
+        return invalidResult(submitRule, submitRecord);
+      }
+
+      if ("ok".equals(submitRecord.name())) {
+        rec.status = SubmitRecord.Status.OK;
+
+      } else if ("not_ready".equals(submitRecord.name())) {
+        rec.status = SubmitRecord.Status.NOT_READY;
+
+      } else {
+        return invalidResult(submitRule, submitRecord);
+      }
+
+      // Unpack the one argument. This should also be a structure with one
+      // argument per label that needs to be reported on to the caller.
+      //
+      submitRecord = submitRecord.arg(0);
+
+      if (!submitRecord.isStructure()) {
+        return invalidResult(submitRule, submitRecord);
+      }
+
+      rec.labels = new ArrayList<>(submitRecord.arity());
+
+      for (Term state : ((StructureTerm) submitRecord).args()) {
+        if (!state.isStructure() || 2 != state.arity() || !"label".equals(state.name())) {
+          return invalidResult(submitRule, submitRecord);
+        }
+
+        SubmitRecord.Label lbl = new SubmitRecord.Label();
+        rec.labels.add(lbl);
+
+        lbl.label = state.arg(0).name();
+        Term status = state.arg(1);
+
+        try {
+          if ("ok".equals(status.name())) {
+            lbl.status = SubmitRecord.Label.Status.OK;
+            appliedBy(lbl, status);
+
+          } else if ("reject".equals(status.name())) {
+            lbl.status = SubmitRecord.Label.Status.REJECT;
+            appliedBy(lbl, status);
+
+          } else if ("need".equals(status.name())) {
+            lbl.status = SubmitRecord.Label.Status.NEED;
+
+          } else if ("may".equals(status.name())) {
+            lbl.status = SubmitRecord.Label.Status.MAY;
+
+          } else if ("impossible".equals(status.name())) {
+            lbl.status = SubmitRecord.Label.Status.IMPOSSIBLE;
+
+          } else {
+            return invalidResult(submitRule, submitRecord);
+          }
+        } catch (UserTermExpected e) {
+          return invalidResult(submitRule, submitRecord, e.getMessage());
+        }
+      }
+
+      if (rec.status == SubmitRecord.Status.OK) {
+        break;
+      }
+    }
+    Collections.reverse(out);
+
+    return out;
+  }
+
+  private List<SubmitRecord> invalidResult(Term rule, Term record, String reason) {
+    return ruleError(String.format("Submit rule %s for change %s of %s output "
+        + "invalid result: %s%s", rule, cd.getId(), getProjectName(), record,
+        (reason == null ? "" : ". Reason: " + reason)));
+  }
+
+  private List<SubmitRecord> invalidResult(Term rule, Term record) {
+    return invalidResult(rule, record, null);
+  }
+
+  private List<SubmitRecord> ruleError(String err) {
+    return ruleError(err, null);
+  }
+
+  private List<SubmitRecord> ruleError(String err, Exception e) {
+    if (logErrors) {
+      if (e == null) {
+        log.error(err);
+      } else {
+        log.error(err, e);
+      }
+      return defaultRuleError();
+    } else {
+      return createRuleError(err);
+    }
+  }
+
+  /**
+   * Evaluate the submit type rules to get the submit type.
+   *
+   * @return record from the evaluated rules.
+   */
+  public SubmitTypeRecord getSubmitType() {
+    try {
+      initPatchSet();
+    } catch (OrmException e) {
+      return typeError("Error looking up patch set "
+          + control.getChange().currentPatchSetId());
+    }
+
+    try {
+      if (control.getChange().getStatus() == Change.Status.DRAFT
+          && !control.isDraftVisible(cd.db(), cd)) {
+        return createTypeError("Patch set " + patchSet.getId() + " not found");
+      }
+      if (patchSet.isDraft() && !control.isDraftVisible(cd.db(), cd)) {
+        return createTypeError("Patch set " + patchSet.getId() + " not found");
+      }
+    } catch (OrmException err) {
+      String msg = "Cannot read patch set " + patchSet.getId();
+      log.error(msg, err);
+      return createTypeError(msg);
+    }
+
+    List<Term> results;
+    try {
+      results = evaluateImpl("locate_submit_type", "get_submit_type",
+          "locate_submit_type_filter", "filter_submit_type_results",
+          // Do not include current user in submit type evaluation. This is used
+          // for mergeability checks, which are stored persistently and so must
+          // have a consistent view of the submit type.
+          null);
+    } catch (RuleEvalException e) {
+      return typeError(e.getMessage(), e);
+    }
+
+    if (results.isEmpty()) {
+      // Should never occur for a well written rule
+      return typeError("Submit rule '" + getSubmitRule() + "' for change "
+          + cd.getId() + " of " + getProjectName() + " has no solution.");
+    }
+
+    Term typeTerm = results.get(0);
+    if (!typeTerm.isSymbol()) {
+      return typeError("Submit rule '" + getSubmitRule() + "' for change "
+          + cd.getId() + " of " + getProjectName()
+          + " did not return a symbol.");
+    }
+
+    String typeName = ((SymbolTerm) typeTerm).name();
+    try {
+      return SubmitTypeRecord.OK(
+          SubmitType.valueOf(typeName.toUpperCase()));
+    } catch (IllegalArgumentException e) {
+      return typeError("Submit type rule " + getSubmitRule() + " for change "
+          + cd.getId() + " of " + getProjectName() + " output invalid result: "
+          + typeName);
+    }
+  }
+
+  private SubmitTypeRecord typeError(String err) {
+    return typeError(err, null);
+  }
+
+  private SubmitTypeRecord typeError(String err, Exception e) {
+    if (logErrors) {
+      if (e == null) {
+        log.error(err);
+      } else {
+        log.error(err, e);
+      }
+      return defaultTypeError();
+    } else {
+      return createTypeError(err);
+    }
+  }
+
+  private List<Term> evaluateImpl(
+      String userRuleLocatorName,
+      String userRuleWrapperName,
+      String filterRuleLocatorName,
+      String filterRuleWrapperName,
+      CurrentUser user) throws RuleEvalException {
+    PrologEnvironment env = getPrologEnvironment(user);
+    try {
+      Term sr = env.once("gerrit", userRuleLocatorName, new VariableTerm());
       if (fastEvalLabels) {
         env.once("gerrit", "assume_range_from_label");
       }
 
       List<Term> results = new ArrayList<>();
       try {
-        for (Term[] template : env.all("gerrit", userRuleWrapperName,
-            submitRule, new VariableTerm())) {
+        for (Term[] template : env.all("gerrit", userRuleWrapperName, sr,
+              new VariableTerm())) {
           results.add(template[1]);
         }
-      } catch (PrologException err) {
-        throw new RuleEvalException("Exception calling " + submitRule
-            + " on change " + change.getId() + " of " + getProjectName(),
-            err);
+      } catch (ReductionLimitException err) {
+        throw new RuleEvalException(String.format(
+            "%s on change %d of %s",
+            err.getMessage(), cd.getId().get(), getProjectName()));
       } catch (RuntimeException err) {
-        throw new RuleEvalException("Exception calling " + submitRule
-            + " on change " + change.getId() + " of " + getProjectName(),
-            err);
+        throw new RuleEvalException(String.format(
+            "Exception calling %s on change %d of %s",
+            sr, cd.getId().get(), getProjectName()), err);
+      } finally {
+        reductionsConsumed = env.getReductions();
       }
 
       Term resultsTerm = toListTerm(results);
       if (!skipFilters) {
-        resultsTerm = runSubmitFilters(resultsTerm, env);
+        resultsTerm = runSubmitFilters(
+            resultsTerm, env, filterRuleLocatorName, filterRuleWrapperName);
       }
+      List<Term> r;
       if (resultsTerm.isList()) {
-        List<Term> r = Lists.newArrayList();
+        r = Lists.newArrayList();
         for (Term t = resultsTerm; t.isList();) {
           ListTerm l = (ListTerm) t;
           r.add(l.car().dereference());
           t = l.cdr().dereference();
         }
-        return r;
+      } else {
+        r = Collections.emptyList();
       }
-      return Collections.emptyList();
+      submitRule = sr;
+      return r;
     } finally {
       env.close();
     }
   }
 
-  private PrologEnvironment getPrologEnvironment() throws RuleEvalException {
-    ProjectState projectState = projectControl.getProjectState();
+  private PrologEnvironment getPrologEnvironment(CurrentUser user)
+      throws RuleEvalException {
+    checkState(patchSet != null,
+        "getPrologEnvironment() called before initPatchSet()");
+    ProjectState projectState = control.getProjectControl().getProjectState();
     PrologEnvironment env;
     try {
-      if (rulesInputStream == null) {
+      if (rule == null) {
         env = projectState.newPrologEnvironment();
       } else {
-        env = projectState.newPrologEnvironment("stdin", rulesInputStream);
+        env = projectState.newPrologEnvironment(
+            "stdin", new ByteArrayInputStream(rule.getBytes(UTF_8)));
       }
     } catch (CompileException err) {
       throw new RuleEvalException("Cannot consult rules.pl for "
           + getProjectName(), err);
     }
-    env.set(StoredValues.REVIEW_DB, db);
+    env.set(StoredValues.REVIEW_DB, cd.db());
     env.set(StoredValues.CHANGE_DATA, cd);
     env.set(StoredValues.PATCH_SET, patchSet);
-    env.set(StoredValues.CHANGE_CONTROL, changeControl);
+    env.set(StoredValues.CHANGE_CONTROL, control);
+    if (user != null) {
+      env.set(StoredValues.CURRENT_USER, user);
+    }
     return env;
   }
 
-  private Term runSubmitFilters(Term results, PrologEnvironment env) throws RuleEvalException {
-    ProjectState projectState = projectControl.getProjectState();
+  private Term runSubmitFilters(Term results, PrologEnvironment env,
+      String filterRuleLocatorName, String filterRuleWrapperName)
+      throws RuleEvalException {
+    ProjectState projectState = control.getProjectControl().getProjectState();
     PrologEnvironment childEnv = env;
     for (ProjectState parentState : projectState.parents()) {
       PrologEnvironment parentEnv;
@@ -213,14 +551,16 @@
             parentEnv.once("gerrit", filterRuleWrapperName, filterRule,
                 results, new VariableTerm());
         results = template[2];
-      } catch (PrologException err) {
-        throw new RuleEvalException("Exception calling " + filterRule
-            + " on change " + change.getId() + " of "
-            + parentState.getProject().getName(), err);
+      } catch (ReductionLimitException err) {
+        throw new RuleEvalException(String.format(
+            "%s on change %d of %s",
+            err.getMessage(), cd.getId().get(), parentState.getProject().getName()));
       } catch (RuntimeException err) {
-        throw new RuleEvalException("Exception calling " + filterRule
-            + " on change " + change.getId() + " of "
-            + parentState.getProject().getName(), err);
+        throw new RuleEvalException(String.format(
+            "Exception calling %s on change %d of %s",
+            filterRule, cd.getId().get(), parentState.getProject().getName()), err);
+      } finally {
+        reductionsConsumed += env.getReductions();
       }
       childEnv = parentEnv;
     }
@@ -235,14 +575,37 @@
     return list;
   }
 
+  private void appliedBy(SubmitRecord.Label label, Term status)
+      throws UserTermExpected {
+    if (status.isStructure() && status.arity() == 1) {
+      Term who = status.arg(0);
+      if (isUser(who)) {
+        label.appliedBy = new Account.Id(((IntegerTerm) who.arg(0)).intValue());
+      } else {
+        throw new UserTermExpected(label);
+      }
+    }
+  }
+
+  private static boolean isUser(Term who) {
+    return who.isStructure()
+        && who.arity() == 1
+        && who.name().equals("user")
+        && who.arg(0).isInteger();
+  }
+
   public Term getSubmitRule() {
+    checkState(submitRule != null, "getSubmitRule() invalid before evaluation");
     return submitRule;
   }
 
-  private String getProjectName() {
-    if (projectName == null) {
-      projectName = projectControl.getProjectState().getProject().getName();
+  private void initPatchSet() throws OrmException {
+    if (patchSet == null) {
+      patchSet = cd.currentPatchSet();
     }
-    return projectName;
+  }
+
+  private String getProjectName() {
+    return control.getProjectControl().getProjectState().getProject().getName();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SuggestParentCandidates.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SuggestParentCandidates.java
index 4b8d2a4..a6717d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SuggestParentCandidates.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SuggestParentCandidates.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -40,8 +39,7 @@
     this.allProject = allProject;
   }
 
-  public List<Project.NameKey> getNameKeys() throws OrmException,
-      NoSuchProjectException {
+  public List<Project.NameKey> getNameKeys() throws NoSuchProjectException {
     List<Project> pList = getProjects();
     final List<Project.NameKey> nameKeys = new ArrayList<>(pList.size());
     for (Project p : pList) {
@@ -50,8 +48,7 @@
     return nameKeys;
   }
 
-  public List<Project> getProjects() throws OrmException,
-      NoSuchProjectException {
+  public List<Project> getProjects() throws NoSuchProjectException {
     Set<Project> projects = new TreeSet<>(new Comparator<Project>() {
       @Override
       public int compare(Project o1, Project o2) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java
new file mode 100644
index 0000000..12be5d3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/TagResource.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.common.TagInfo;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+public class TagResource extends ProjectResource {
+  public static final TypeLiteral<RestView<TagResource>> TAG_KIND =
+      new TypeLiteral<RestView<TagResource>>() {};
+
+  private final TagInfo tag;
+
+  public TagResource(ProjectControl control, TagInfo tag) {
+    super(control);
+    this.tag = tag;
+  }
+
+  public TagInfo getTagInfo() {
+    return tag;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/TagsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/TagsCollection.java
new file mode 100644
index 0000000..0c70285
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/TagsCollection.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+
+@Singleton
+public class TagsCollection implements
+    ChildCollection<ProjectResource, TagResource> {
+  private final DynamicMap<RestView<TagResource>> views;
+  private final ListTags list;
+
+  @Inject
+  public TagsCollection(DynamicMap<RestView<TagResource>> views,
+     ListTags list) {
+    this.views = views;
+    this.list = list;
+  }
+
+  @Override
+  public RestView<ProjectResource> list() throws ResourceNotFoundException {
+    return list;
+  }
+
+  @Override
+  public TagResource parse(ProjectResource resource, IdString id)
+      throws ResourceNotFoundException, IOException {
+    return new TagResource(resource.getControl(), list.get(resource, id));
+  }
+
+  @Override
+  public DynamicMap<RestView<TagResource>> views() {
+    return views;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
index 953dabf..39b0fa3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
@@ -92,8 +92,9 @@
 
   @Override
   public boolean equals(final Object other) {
-    if (other == null)
+    if (other == null) {
       return false;
+    }
     return getClass() == other.getClass()
         && getChildren().equals(((Predicate<?>) other).getChildren());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
index 7b43572..f4be013 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
@@ -92,7 +92,7 @@
       //
       return that.getChild(0);
     }
-    return new NotPredicate<T>(that);
+    return new NotPredicate<>(that);
   }
 
   /** Get the children of this predicate, if any. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
index 5be42be..bd1fa0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
@@ -138,7 +138,8 @@
    * @param p the predicate to find.
    * @param clazz type of the predicate instance.
    * @param name name of the operator.
-   * @return the predicate, null if not found.
+   * @return the first instance of a predicate having the given type, as found
+   *     by a depth-first search.
    */
   @SuppressWarnings("unchecked")
   public static <T, P extends OperatorPredicate<T>> P find(Predicate<T> p,
@@ -159,16 +160,19 @@
     return null;
   }
 
+  protected final Definition<T, ? extends QueryBuilder<T>> builderDef;
+
   @SuppressWarnings("rawtypes")
   private final Map<String, OperatorFactory> opFactories;
 
   @SuppressWarnings({"unchecked", "rawtypes"})
   protected QueryBuilder(Definition<T, ? extends QueryBuilder<T>> def) {
+    builderDef = def;
     opFactories = (Map) def.opFactories;
   }
 
   /**
-   * Parse a user supplied query string into a predicate.
+   * Parse a user-supplied query string into a predicate.
    *
    * @param query the query string.
    * @return predicate representing the user query.
@@ -181,6 +185,27 @@
     return toPredicate(QueryParser.parse(query));
   }
 
+  /**
+   * Parse multiple user-supplied query strings into a list of predicates.
+   *
+   * @param queries the query strings.
+   * @return predicates representing the user query, in the same order as the
+   *         input.
+   * @throws QueryParseException one of the query strings is invalid and cannot
+   *         be parsed by this parser. This may be due to a syntax error, may be
+   *         due to an operator not being supported, or due to an invalid value
+   *         being passed to a recognized operator.
+   *
+   */
+  public List<Predicate<T>> parse(final List<String> queries)
+      throws QueryParseException {
+    List<Predicate<T>> predicates = new ArrayList<>(queries.size());
+    for (String query : queries) {
+      predicates.add(parse(query));
+    }
+    return predicates;
+  }
+
   private Predicate<T> toPredicate(final Tree r) throws QueryParseException,
       IllegalArgumentException {
     switch (r.getType()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AfterPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AfterPredicate.java
index cb0038c..aeb9619 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AfterPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AfterPredicate.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.TimestampRangePredicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gwtorm.server.OrmException;
@@ -24,9 +24,8 @@
 public class AfterPredicate extends TimestampRangePredicate<ChangeData> {
   private final Date cut;
 
-  AfterPredicate(Schema<ChangeData> schema, String value)
-      throws QueryParseException {
-    super(updatedField(schema), ChangeQueryBuilder.FIELD_BEFORE, value);
+  AfterPredicate(String value) throws QueryParseException {
+    super(ChangeField.UPDATED, ChangeQueryBuilder.FIELD_BEFORE, value);
     cut = parse(value);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
index 1894596..9a4ef19 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
@@ -17,11 +17,11 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.TimestampRangePredicate;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 
 import java.sql.Timestamp;
@@ -29,8 +29,8 @@
 public class AgePredicate extends TimestampRangePredicate<ChangeData> {
   private final long cut;
 
-  AgePredicate(Schema<ChangeData> schema, String value) {
-    super(updatedField(schema), ChangeQueryBuilder.FIELD_AGE, value);
+  AgePredicate(String value) {
+    super(ChangeField.UPDATED, ChangeQueryBuilder.FIELD_AGE, value);
 
     long s = ConfigUtil.getTimeUnit(getValue(), 0, SECONDS);
     long ms = MILLISECONDS.convert(s, SECONDS);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
index 55fd281..f48cfd8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
@@ -108,10 +108,6 @@
     if (source == null) {
       throw new OrmException("No ChangeDataSource: " + this);
     }
-    @SuppressWarnings("unchecked")
-    Predicate<ChangeData> pred = (Predicate<ChangeData>) source;
-    boolean useSortKey = ChangeQueryBuilder.hasSortKey(pred);
-
     List<ChangeData> r = Lists.newArrayList();
     ChangeData last = null;
     int nextStart = 0;
@@ -133,12 +129,8 @@
       //
       Paginated p = (Paginated) source;
       while (skipped && r.size() < p.limit() + start) {
-        ChangeData lastBeforeRestart = last;
         skipped = false;
-        last = null;
-        ResultSet<ChangeData> next = useSortKey
-            ? p.restart(lastBeforeRestart)
-            : p.restart(nextStart);
+        ResultSet<ChangeData> next = p.restart(nextStart);
 
         for (ChangeData data : buffer(source, next)) {
           if (match(data)) {
@@ -146,7 +138,6 @@
           } else {
             skipped = true;
           }
-          last = data;
           nextStart++;
         }
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java
index 8ce6fa3..1053d92 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java
@@ -16,51 +16,46 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.IntPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryRewriter;
 import com.google.inject.Inject;
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
-import com.google.inject.name.Named;
 
 public class BasicChangeRewrites extends QueryRewriter<ChangeData> {
   private static final ChangeQueryBuilder BUILDER = new ChangeQueryBuilder(
-      new ChangeQueryBuilder.Arguments( //
-          new InvalidProvider<ReviewDb>(), //
-          new InvalidProvider<ChangeQueryRewriter>(), //
-          null, null, null, null, null, null, null, //
-          null, null, null, null, null, null, null, null, null, null), null);
+      new ChangeQueryBuilder.Arguments(
+          new InvalidProvider<ReviewDb>(),
+          new InvalidProvider<InternalChangeQuery>(),
+          new InvalidProvider<ChangeQueryRewriter>(),
+          null, null, null, null, null, null, null, null, null, null, null,
+          null, null, null, null, null, null, null, null));
 
   private static final QueryRewriter.Definition<ChangeData, BasicChangeRewrites> mydef =
-      new QueryRewriter.Definition<ChangeData, BasicChangeRewrites>(
-          BasicChangeRewrites.class, BUILDER);
-
-  protected final Provider<ReviewDb> dbProvider;
+      new QueryRewriter.Definition<>(BasicChangeRewrites.class, BUILDER);
 
   @Inject
-  public BasicChangeRewrites(Provider<ReviewDb> dbProvider) {
+  public BasicChangeRewrites() {
     super(mydef);
-    this.dbProvider = dbProvider;
   }
 
   @Rewrite("-status:open")
   @NoCostComputation
   public Predicate<ChangeData> r00_notOpen() {
-    return ChangeStatusPredicate.closed(dbProvider);
+    return ChangeStatusPredicate.closed();
   }
 
   @Rewrite("-status:closed")
   @NoCostComputation
   public Predicate<ChangeData> r00_notClosed() {
-    return ChangeStatusPredicate.open(dbProvider);
+    return ChangeStatusPredicate.open();
   }
 
   @SuppressWarnings("unchecked")
   @NoCostComputation
   @Rewrite("-status:merged")
   public Predicate<ChangeData> r00_notMerged() {
-    return or(ChangeStatusPredicate.open(dbProvider),
+    return or(ChangeStatusPredicate.open(),
         new ChangeStatusPredicate(Change.Status.ABANDONED));
   }
 
@@ -68,18 +63,10 @@
   @NoCostComputation
   @Rewrite("-status:abandoned")
   public Predicate<ChangeData> r00_notAbandoned() {
-    return or(ChangeStatusPredicate.open(dbProvider),
+    return or(ChangeStatusPredicate.open(),
         new ChangeStatusPredicate(Change.Status.MERGED));
   }
 
-  @NoCostComputation
-  @Rewrite("A=(limit:*) B=(limit:*)")
-  public Predicate<ChangeData> r00_smallestLimit(
-      @Named("A") IntPredicate<ChangeData> a,
-      @Named("B") IntPredicate<ChangeData> b) {
-    return a.intValue() <= b.intValue() ? a : b;
-  }
-
   private static final class InvalidProvider<T> implements Provider<T> {
     @Override
     public T get() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BeforePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BeforePredicate.java
index f724676..8f51476 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BeforePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BeforePredicate.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.TimestampRangePredicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gwtorm.server.OrmException;
@@ -24,9 +24,8 @@
 public class BeforePredicate extends TimestampRangePredicate<ChangeData> {
   private final Date cut;
 
-  BeforePredicate(Schema<ChangeData> schema, String value)
-      throws QueryParseException {
-    super(updatedField(schema), ChangeQueryBuilder.FIELD_BEFORE, value);
+  BeforePredicate(String value) throws QueryParseException {
+    super(ChangeField.UPDATED, ChangeQueryBuilder.FIELD_BEFORE, value);
     cut = parse(value);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeCosts.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeCosts.java
deleted file mode 100644
index ba42803..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeCosts.java
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-public class ChangeCosts {
-  public static final int IDS_MEMORY = 1;
-  public static final int CHANGES_SCAN = 2;
-  public static final int TR_SCAN = 20;
-  public static final int APPROVALS_SCAN = 30;
-  public static final int PATCH_SETS_SCAN = 30;
-
-  /** Estimated matches for a Change-Id string. */
-  public static final int CARD_KEY = 5;
-
-  /** Estimated matches for a commit SHA-1 string. */
-  public static final int CARD_COMMIT = 5;
-
-  /** Estimated matches for a tracking/bug id string. */
-  public static final int CARD_TRACKING_IDS = 5;
-
-  public static int cost(int cost, int cardinality) {
-    return Math.max(1, cost) * Math.max(0, cardinality);
-  }
-
-  private ChangeCosts() {
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index e9266cd..e4e94a1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -16,13 +16,14 @@
 
 import static com.google.gerrit.server.ApprovalsUtil.sortApprovals;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.SetMultimap;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -35,7 +36,10 @@
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.change.MergeabilityCache;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.notedb.ReviewerState;
@@ -45,6 +49,8 @@
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.assistedinject.Assisted;
@@ -54,6 +60,7 @@
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.FooterLine;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -68,6 +75,24 @@
 import java.util.Map;
 
 public class ChangeData {
+  public static List<Change> asChanges(List<ChangeData> changeDatas)
+      throws OrmException {
+    List<Change> result = new ArrayList<>(changeDatas.size());
+    for (ChangeData cd : changeDatas) {
+      result.add(cd.change());
+    }
+    return result;
+  }
+
+  public static Map<Change.Id, ChangeData> asMap(List<ChangeData> changes) {
+    Map<Change.Id, ChangeData> result =
+        Maps.newHashMapWithExpectedSize(changes.size());
+    for (ChangeData cd : changes) {
+      result.put(cd.getId(), cd);
+    }
+    return result;
+  }
+
   public static void ensureChangeLoaded(Iterable<ChangeData> changes)
       throws OrmException {
     Map<Change.Id, ChangeData> missing = Maps.newHashMap();
@@ -78,7 +103,7 @@
     }
     if (!missing.isEmpty()) {
       ChangeData first = missing.values().iterator().next();
-      if (!first.notesMigration.readPatchSetApprovals()) {
+      if (!first.notesMigration.readChanges()) {
         ReviewDb db = missing.values().iterator().next().db;
         for (Change change : db.changes().get(missing.keySet())) {
           missing.get(change.getId()).change = change;
@@ -119,7 +144,7 @@
       throws OrmException {
     List<ResultSet<PatchSetApproval>> pending = Lists.newArrayList();
     for (ChangeData cd : changes) {
-      if (!cd.notesMigration.readPatchSetApprovals()) {
+      if (!cd.notesMigration.readChanges()) {
         if (cd.currentApprovals == null) {
           pending.add(cd.db.patchSetApprovals()
               .byPatchSet(cd.change().currentPatchSetId()));
@@ -153,8 +178,8 @@
    * @return instance for testing.
    */
   static ChangeData createForTest(Change.Id id, int currentPatchSetId) {
-    ChangeData cd = new ChangeData(null, null, null, null, null,
-        null, null, null, null, id);
+    ChangeData cd = new ChangeData(null, null, null, null, null, null, null,
+        null, null, null, null, null, null, id);
     cd.currentPatchSet = new PatchSet(new PatchSet.Id(id, currentPatchSetId));
     return cd;
   }
@@ -163,11 +188,15 @@
   private final GitRepositoryManager repoManager;
   private final ChangeControl.GenericFactory changeControlFactory;
   private final IdentifiedUser.GenericFactory userFactory;
+  private final ProjectCache projectCache;
+  private final MergeUtil.Factory mergeUtilFactory;
   private final ChangeNotes.Factory notesFactory;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeMessagesUtil cmUtil;
+  private final PatchLineCommentsUtil plcUtil;
   private final PatchListCache patchListCache;
   private final NotesMigration notesMigration;
+  private final MergeabilityCache mergeabilityCache;
   private final Change.Id legacyId;
   private ChangeDataSource returnedBySource;
   private Change change;
@@ -179,34 +208,43 @@
   private ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals;
   private List<PatchSetApproval> currentApprovals;
   private Map<Integer, List<String>> files = new HashMap<>();
-  private Collection<PatchLineComment> comments;
+  private Collection<PatchLineComment> publishedComments;
   private CurrentUser visibleTo;
   private ChangeControl changeControl;
   private List<ChangeMessage> messages;
   private List<SubmitRecord> submitRecords;
   private ChangedLines changedLines;
+  private Boolean mergeable;
 
   @AssistedInject
   private ChangeData(
       GitRepositoryManager repoManager,
       ChangeControl.GenericFactory changeControlFactory,
       IdentifiedUser.GenericFactory userFactory,
+      ProjectCache projectCache,
+      MergeUtil.Factory mergeUtilFactory,
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
+      PatchLineCommentsUtil plcUtil,
       PatchListCache patchListCache,
       NotesMigration notesMigration,
+      MergeabilityCache mergeabilityCache,
       @Assisted ReviewDb db,
       @Assisted Change.Id id) {
     this.db = db;
     this.repoManager = repoManager;
     this.changeControlFactory = changeControlFactory;
     this.userFactory = userFactory;
+    this.projectCache = projectCache;
+    this.mergeUtilFactory = mergeUtilFactory;
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
+    this.plcUtil = plcUtil;
     this.patchListCache = patchListCache;
     this.notesMigration = notesMigration;
+    this.mergeabilityCache = mergeabilityCache;
     legacyId = id;
   }
 
@@ -215,22 +253,30 @@
       GitRepositoryManager repoManager,
       ChangeControl.GenericFactory changeControlFactory,
       IdentifiedUser.GenericFactory userFactory,
+      ProjectCache projectCache,
+      MergeUtil.Factory mergeUtilFactory,
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
+      PatchLineCommentsUtil plcUtil,
       PatchListCache patchListCache,
       NotesMigration notesMigration,
+      MergeabilityCache mergeabilityCache,
       @Assisted ReviewDb db,
       @Assisted Change c) {
     this.db = db;
     this.repoManager = repoManager;
     this.changeControlFactory = changeControlFactory;
     this.userFactory = userFactory;
+    this.projectCache = projectCache;
+    this.mergeUtilFactory = mergeUtilFactory;
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
+    this.plcUtil = plcUtil;
     this.patchListCache = patchListCache;
     this.notesMigration = notesMigration;
+    this.mergeabilityCache = mergeabilityCache;
     legacyId = c.getId();
     change = c;
   }
@@ -240,28 +286,40 @@
       GitRepositoryManager repoManager,
       ChangeControl.GenericFactory changeControlFactory,
       IdentifiedUser.GenericFactory userFactory,
+      ProjectCache projectCache,
+      MergeUtil.Factory mergeUtilFactory,
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
+      PatchLineCommentsUtil plcUtil,
       PatchListCache patchListCache,
       NotesMigration notesMigration,
+      MergeabilityCache mergeabilityCache,
       @Assisted ReviewDb db,
       @Assisted ChangeControl c) {
     this.db = db;
     this.repoManager = repoManager;
     this.changeControlFactory = changeControlFactory;
     this.userFactory = userFactory;
+    this.projectCache = projectCache;
+    this.mergeUtilFactory = mergeUtilFactory;
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
+    this.plcUtil = plcUtil;
     this.patchListCache = patchListCache;
     this.notesMigration = notesMigration;
+    this.mergeabilityCache = mergeabilityCache;
     legacyId = c.getChange().getId();
     change = c.getChange();
     changeControl = c;
     notes = c.getNotes();
   }
 
+  public ReviewDb db() {
+    return db;
+  }
+
   public boolean isFromSource(ChangeDataSource s) {
     return s == returnedBySource;
   }
@@ -387,11 +445,16 @@
 
   public Change change() throws OrmException {
     if (change == null) {
-      change = db.changes().get(legacyId);
+      reloadChange();
     }
     return change;
   }
 
+  public Change reloadChange() throws OrmException {
+    change = db.changes().get(legacyId);
+    return change;
+  }
+
   public ChangeNotes notes() throws OrmException {
     if (notes == null) {
       notes = notesFactory.create(change());
@@ -433,44 +496,40 @@
     currentApprovals = approvals;
   }
 
-  public String commitMessage() throws NoSuchChangeException, IOException,
-      OrmException {
+  public String commitMessage() throws IOException, OrmException {
     if (commitMessage == null) {
-      loadCommitData();
+      if (!loadCommitData()) {
+        return null;
+      }
     }
     return commitMessage;
   }
 
-  public List<FooterLine> commitFooters() throws NoSuchChangeException,
-      IOException, OrmException {
+  public List<FooterLine> commitFooters() throws IOException, OrmException {
     if (commitFooters == null) {
-      loadCommitData();
+      if (!loadCommitData()) {
+        return null;
+      }
     }
     return commitFooters;
   }
 
-  private void loadCommitData() throws NoSuchChangeException, OrmException,
+  private boolean loadCommitData() throws OrmException,
       RepositoryNotFoundException, IOException, MissingObjectException,
       IncorrectObjectTypeException {
     PatchSet.Id psId = change().currentPatchSetId();
     PatchSet ps = db.patchSets().get(psId);
     if (ps == null) {
-      throw new NoSuchChangeException(legacyId);
+      return false;
     }
     String sha1 = ps.getRevision().get();
-    Repository repo = repoManager.openRepository(change().getProject());
-    try {
-      RevWalk walk = new RevWalk(repo);
-      try {
-        RevCommit c = walk.parseCommit(ObjectId.fromString(sha1));
-        commitMessage = c.getFullMessage();
-        commitFooters = c.getFooterLines();
-      } finally {
-        walk.release();
-      }
-    } finally {
-      repo.close();
+    try (Repository repo = repoManager.openRepository(change().getProject());
+        RevWalk walk = new RevWalk(repo)) {
+      RevCommit c = walk.parseCommit(ObjectId.fromString(sha1));
+      commitMessage = c.getFullMessage();
+      commitFooters = c.getFooterLines();
     }
+    return true;
   }
 
   /**
@@ -519,12 +578,12 @@
     return approvalsUtil.getReviewers(notes(), approvals().values());
   }
 
-  public Collection<PatchLineComment> comments()
+  public Collection<PatchLineComment> publishedComments()
       throws OrmException {
-    if (comments == null) {
-      comments = db.patchComments().byChange(legacyId).toList();
+    if (publishedComments == null) {
+      publishedComments = plcUtil.publishedByChange(db, notes());
     }
-    return comments;
+    return publishedComments;
   }
 
   public List<ChangeMessage> messages()
@@ -543,9 +602,60 @@
     return submitRecords;
   }
 
+  public void setMergeable(boolean mergeable) {
+    this.mergeable = mergeable;
+  }
+
+  public Boolean isMergeable() throws OrmException {
+    if (mergeable == null) {
+      Change c = change();
+      if (c == null) {
+        return null;
+      }
+      if (c.getStatus() == Change.Status.MERGED) {
+        mergeable = true;
+      } else {
+        PatchSet ps = currentPatchSet();
+        if (ps == null || !changeControl().isPatchVisible(ps, db)) {
+          return null;
+        }
+        Repository repo = null;
+        try {
+          repo = repoManager.openRepository(c.getProject());
+          Ref ref = repo.getRef(c.getDest().get());
+          SubmitTypeRecord rec = new SubmitRuleEvaluator(this)
+              .getSubmitType();
+          if (rec.status != SubmitTypeRecord.Status.OK) {
+            throw new OrmException(
+                "Error in mergeability check: " + rec.errorMessage);
+          }
+          String mergeStrategy = mergeUtilFactory
+              .create(projectCache.get(c.getProject()))
+              .mergeStrategyName();
+          mergeable = mergeabilityCache.get(
+              ObjectId.fromString(ps.getRevision().get()),
+              ref, rec.type, mergeStrategy, c.getDest(), repo, db);
+        } catch (IOException e) {
+          throw new OrmException(e);
+        } finally {
+          if (repo != null) {
+            repo.close();
+          }
+        }
+      }
+    }
+    return mergeable;
+  }
+
   @Override
   public String toString() {
-    return Objects.toStringHelper(this).addValue(getId()).toString();
+    MoreObjects.ToStringHelper h = MoreObjects.toStringHelper(this);
+    if (change != null) {
+      h.addValue(change);
+    } else {
+      h.addValue(legacyId);
+    }
+    return h.toString();
   }
 
   public static class ChangedLines {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
index d1a6c6e..7f517d8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
@@ -17,21 +17,16 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.IndexPredicate;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 
-class ChangeIdPredicate extends IndexPredicate<ChangeData> implements
-    ChangeDataSource {
-  private final Arguments args;
-
-  ChangeIdPredicate(Arguments args, String id) {
+/** Predicate over Change-Id strings (aka Change.Key). */
+class ChangeIdPredicate extends IndexPredicate<ChangeData> {
+  ChangeIdPredicate(String id) {
     super(ChangeField.ID, ChangeQueryBuilder.FIELD_CHANGE, id);
-    this.args = args;
   }
 
   @Override
-  public boolean match(final ChangeData cd) throws OrmException {
+  public boolean match(ChangeData cd) throws OrmException {
     Change change = cd.change();
     if (change == null) {
       return false;
@@ -45,25 +40,7 @@
   }
 
   @Override
-  public ResultSet<ChangeData> read() throws OrmException {
-    Change.Key a = new Change.Key(getValue());
-    Change.Key b = a.max();
-    return ChangeDataResultSet.change(args.changeDataFactory, args.db,
-        args.db.get().changes().byKeyRange(a, b));
-  }
-
-  @Override
-  public boolean hasChange() {
-    return true;
-  }
-
-  @Override
   public int getCost() {
-    return ChangeCosts.cost(ChangeCosts.CHANGES_SCAN, getCardinality());
-  }
-
-  @Override
-  public int getCardinality() {
-    return ChangeCosts.CARD_KEY;
+    return 1;
   }
 }
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 ac7b9ae..c9d7e6c 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
@@ -14,10 +14,14 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.gerrit.server.query.change.ChangeData.asChanges;
+
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.errors.NotSignedInException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -25,31 +29,33 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
+import com.google.gerrit.server.change.ChangeTriplet;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
-import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.FieldDef;
 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.ListChildProjects;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.query.IntPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryBuilder;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.assistedinject.Assisted;
+import com.google.inject.ProvisionException;
+import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.Config;
@@ -69,8 +75,8 @@
   private static final Pattern PAT_LEGACY_ID = Pattern.compile("^[1-9][0-9]*$");
   private static final Pattern PAT_CHANGE_ID =
       Pattern.compile("^[iI][0-9a-f]{4,}.*$");
-  private static final Pattern DEF_CHANGE =
-      Pattern.compile("^([1-9][0-9]*|[iI][0-9a-f]{4,}.*)$");
+  private static final Pattern DEF_CHANGE = Pattern.compile(
+      "^(?:[1-9][0-9]*|(?:[^~]+~[^~]+~)?[iI][0-9a-f]{4,}.*)$");
 
   // NOTE: As new search operations are added, please keep the
   // SearchSuggestOracle up to date.
@@ -90,6 +96,7 @@
   public static final String FIELD_FILE = "file";
   public static final String FIELD_IS = "is";
   public static final String FIELD_HAS = "has";
+  public static final String FIELD_HASHTAG = "hashtag";
   public static final String FIELD_LABEL = "label";
   public static final String FIELD_LIMIT = "limit";
   public static final String FIELD_MERGEABLE = "mergeable";
@@ -115,37 +122,19 @@
 
 
   private static final QueryBuilder.Definition<ChangeData, ChangeQueryBuilder> mydef =
-      new QueryBuilder.Definition<ChangeData, ChangeQueryBuilder>(
-          ChangeQueryBuilder.class);
-
-  @SuppressWarnings("unchecked")
-  public static Integer getLimit(Predicate<ChangeData> p) {
-    IntPredicate<?> ip =
-        (IntPredicate<?>) find(p, IntPredicate.class, FIELD_LIMIT);
-    return ip != null ? ip.intValue() : null;
-  }
-
-  public static boolean hasNonTrivialSortKeyAfter(Schema<ChangeData> schema,
-      Predicate<ChangeData> p) {
-    SortKeyPredicate after =
-        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;
-  }
+      new QueryBuilder.Definition<>(ChangeQueryBuilder.class);
 
   @VisibleForTesting
   public static class Arguments {
     final Provider<ReviewDb> db;
+    final Provider<InternalChangeQuery> queryProvider;
     final Provider<ChangeQueryRewriter> rewriter;
     final IdentifiedUser.GenericFactory userFactory;
-    final Provider<CurrentUser> self;
     final CapabilityControl.Factory capabilityControlFactory;
     final ChangeControl.GenericFactory changeControlGenericFactory;
     final ChangeData.Factory changeDataFactory;
+    final FieldDef.FillArgs fillArgs;
+    final PatchLineCommentsUtil plcUtil;
     final AccountResolver accountResolver;
     final GroupBackend groupBackend;
     final AllProjectsName allProjectsName;
@@ -159,15 +148,20 @@
     final TrackingFooters trackingFooters;
     final boolean allowsDrafts;
 
+    private final Provider<CurrentUser> self;
+
     @Inject
     @VisibleForTesting
-    public Arguments(Provider<ReviewDb> dbProvider,
+    public Arguments(Provider<ReviewDb> db,
+        Provider<InternalChangeQuery> queryProvider,
         Provider<ChangeQueryRewriter> rewriter,
         IdentifiedUser.GenericFactory userFactory,
         Provider<CurrentUser> self,
         CapabilityControl.Factory capabilityControlFactory,
         ChangeControl.GenericFactory changeControlGenericFactory,
         ChangeData.Factory changeDataFactory,
+        FieldDef.FillArgs fillArgs,
+        PatchLineCommentsUtil plcUtil,
         AccountResolver accountResolver,
         GroupBackend groupBackend,
         AllProjectsName allProjectsName,
@@ -180,61 +174,134 @@
         ConflictsCache conflictsCache,
         TrackingFooters trackingFooters,
         @GerritServerConfig Config cfg) {
-      this.db = dbProvider;
-      this.rewriter = rewriter;
-      this.userFactory = userFactory;
-      this.self = self;
-      this.capabilityControlFactory = capabilityControlFactory;
-      this.changeControlGenericFactory = changeControlGenericFactory;
-      this.changeDataFactory = changeDataFactory;
-      this.accountResolver = accountResolver;
-      this.groupBackend = groupBackend;
-      this.allProjectsName = allProjectsName;
-      this.patchListCache = patchListCache;
-      this.repoManager = repoManager;
-      this.projectCache = projectCache;
-      this.listChildProjects = listChildProjects;
-      this.indexes = indexes;
-      this.submitStrategyFactory = submitStrategyFactory;
-      this.conflictsCache = conflictsCache;
-      this.trackingFooters = trackingFooters;
-      this.allowsDrafts = cfg == null
-          ? true
-          : cfg.getBoolean("change", "allowDrafts", true);
+      this(db, queryProvider, rewriter, userFactory, self,
+          capabilityControlFactory, changeControlGenericFactory,
+          changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend,
+          allProjectsName, patchListCache, repoManager, projectCache,
+          listChildProjects, indexes, submitStrategyFactory, conflictsCache,
+          trackingFooters,
+          cfg == null ? true : cfg.getBoolean("change", "allowDrafts", true));
+    }
+
+    private Arguments(
+        Provider<ReviewDb> db,
+        Provider<InternalChangeQuery> queryProvider,
+        Provider<ChangeQueryRewriter> rewriter,
+        IdentifiedUser.GenericFactory userFactory,
+        Provider<CurrentUser> self,
+        CapabilityControl.Factory capabilityControlFactory,
+        ChangeControl.GenericFactory changeControlGenericFactory,
+        ChangeData.Factory changeDataFactory,
+        FieldDef.FillArgs fillArgs,
+        PatchLineCommentsUtil plcUtil,
+        AccountResolver accountResolver,
+        GroupBackend groupBackend,
+        AllProjectsName allProjectsName,
+        PatchListCache patchListCache,
+        GitRepositoryManager repoManager,
+        ProjectCache projectCache,
+        Provider<ListChildProjects> listChildProjects,
+        IndexCollection indexes,
+        SubmitStrategyFactory submitStrategyFactory,
+        ConflictsCache conflictsCache,
+        TrackingFooters trackingFooters,
+        boolean allowsDrafts) {
+     this.db = db;
+     this.queryProvider = queryProvider;
+     this.rewriter = rewriter;
+     this.userFactory = userFactory;
+     this.self = self;
+     this.capabilityControlFactory = capabilityControlFactory;
+     this.changeControlGenericFactory = changeControlGenericFactory;
+     this.changeDataFactory = changeDataFactory;
+     this.fillArgs = fillArgs;
+     this.plcUtil = plcUtil;
+     this.accountResolver = accountResolver;
+     this.groupBackend = groupBackend;
+     this.allProjectsName = allProjectsName;
+     this.patchListCache = patchListCache;
+     this.repoManager = repoManager;
+     this.projectCache = projectCache;
+     this.listChildProjects = listChildProjects;
+     this.indexes = indexes;
+     this.submitStrategyFactory = submitStrategyFactory;
+     this.conflictsCache = conflictsCache;
+     this.trackingFooters = trackingFooters;
+     this.allowsDrafts = allowsDrafts;
+    }
+
+    Arguments asUser(CurrentUser otherUser) {
+      return new Arguments(db, queryProvider, rewriter, userFactory,
+          Providers.of(otherUser),
+          capabilityControlFactory, changeControlGenericFactory,
+          changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend,
+          allProjectsName, patchListCache, repoManager, projectCache,
+          listChildProjects, indexes, submitStrategyFactory, conflictsCache,
+          trackingFooters, allowsDrafts);
+    }
+
+    Arguments asUser(Account.Id otherId) {
+      try {
+        CurrentUser u = self.get();
+        if (u.isIdentifiedUser()
+            && otherId.equals(((IdentifiedUser) u).getAccountId())) {
+          return this;
+        }
+      } catch (ProvisionException e) {
+        // Doesn't match current user, continue.
+      }
+      return asUser(userFactory.create(db, otherId));
+    }
+
+    IdentifiedUser getIdentifiedUser() throws QueryParseException {
+      try {
+        CurrentUser u = getCurrentUser();
+        if (u.isIdentifiedUser()) {
+          return (IdentifiedUser) u;
+        }
+        throw new QueryParseException(NotSignedInException.MESSAGE);
+      } catch (ProvisionException e) {
+        throw new QueryParseException(NotSignedInException.MESSAGE, e);
+      }
+    }
+
+    CurrentUser getCurrentUser() throws QueryParseException {
+      try {
+        return self.get();
+      } catch (ProvisionException e) {
+        throw new QueryParseException(NotSignedInException.MESSAGE, e);
+      }
     }
   }
 
-  public interface Factory {
-    ChangeQueryBuilder create(CurrentUser user);
-  }
-
   private final Arguments args;
-  private final CurrentUser currentUser;
 
   @Inject
-  public ChangeQueryBuilder(Arguments args, @Assisted CurrentUser currentUser) {
+  ChangeQueryBuilder(Arguments args) {
     super(mydef);
     this.args = args;
-    this.currentUser = currentUser;
   }
 
   @VisibleForTesting
   protected ChangeQueryBuilder(
-      QueryBuilder.Definition<ChangeData, ? extends ChangeQueryBuilder> def,
-      Arguments args, CurrentUser currentUser) {
+      Definition<ChangeData, ? extends QueryBuilder<ChangeData>> def,
+      Arguments args) {
     super(def);
     this.args = args;
-    this.currentUser = currentUser;
+  }
+
+  public ChangeQueryBuilder asUser(CurrentUser user) {
+    return new ChangeQueryBuilder(builderDef, args.asUser(user));
   }
 
   @Operator
   public Predicate<ChangeData> age(String value) {
-    return new AgePredicate(schema(args.indexes), value);
+    return new AgePredicate(value);
   }
 
   @Operator
   public Predicate<ChangeData> before(String value) throws QueryParseException {
-    return new BeforePredicate(schema(args.indexes), value);
+    return new BeforePredicate(value);
   }
 
   @Operator
@@ -244,7 +311,7 @@
 
   @Operator
   public Predicate<ChangeData> after(String value) throws QueryParseException {
-    return new AfterPredicate(schema(args.indexes), value);
+    return new AfterPredicate(value);
   }
 
   @Operator
@@ -253,47 +320,46 @@
   }
 
   @Operator
-  public Predicate<ChangeData> change(String query) {
+  public Predicate<ChangeData> change(String query) throws QueryParseException {
     if (PAT_LEGACY_ID.matcher(query).matches()) {
-      return new LegacyChangeIdPredicate(args, Change.Id.parse(query));
-
+      return new LegacyChangeIdPredicate(Change.Id.parse(query));
     } else if (PAT_CHANGE_ID.matcher(query).matches()) {
-      return new ChangeIdPredicate(args, parseChangeId(query));
+      return new ChangeIdPredicate(parseChangeId(query));
+    }
+    Optional<ChangeTriplet> triplet = ChangeTriplet.parse(query);
+    if (triplet.isPresent()) {
+      return Predicate.and(
+          project(triplet.get().project().get()),
+          branch(triplet.get().branch().get()),
+          new ChangeIdPredicate(parseChangeId(triplet.get().id().get())));
     }
 
-    throw new IllegalArgumentException();
+    throw new QueryParseException("Invalid change format");
   }
 
   @Operator
-  public Predicate<ChangeData> comment(String value) throws QueryParseException {
+  public Predicate<ChangeData> comment(String value) {
     ChangeIndex index = args.indexes.getSearchIndex();
-    return new CommentPredicate(args, index, value);
+    return new CommentPredicate(index, value);
   }
 
   @Operator
   public Predicate<ChangeData> status(String statusName) {
-    if ("open".equals(statusName) || "pending".equals(statusName)) {
-      return status_open();
-
-    } else if ("closed".equals(statusName)) {
-      return ChangeStatusPredicate.closed(args.db);
-
-    } else if ("reviewed".equalsIgnoreCase(statusName)) {
+    if ("reviewed".equalsIgnoreCase(statusName)) {
       return new IsReviewedPredicate();
-
     } else {
-      return new ChangeStatusPredicate(statusName);
+      return ChangeStatusPredicate.parse(statusName);
     }
   }
 
   public Predicate<ChangeData> status_open() {
-    return ChangeStatusPredicate.open(args.db);
+    return ChangeStatusPredicate.open();
   }
 
   @Operator
-  public Predicate<ChangeData> has(String value) {
+  public Predicate<ChangeData> has(String value) throws QueryParseException {
     if ("star".equalsIgnoreCase(value)) {
-      return new IsStarredByPredicate(args, currentUser);
+      return new IsStarredByPredicate(args);
     }
 
     if ("draft".equalsIgnoreCase(value)) {
@@ -306,11 +372,11 @@
   @Operator
   public Predicate<ChangeData> is(String value) throws QueryParseException {
     if ("starred".equalsIgnoreCase(value)) {
-      return new IsStarredByPredicate(args, currentUser);
+      return new IsStarredByPredicate(args);
     }
 
     if ("watched".equalsIgnoreCase(value)) {
-      return new IsWatchedByPredicate(args, currentUser, false);
+      return new IsWatchedByPredicate(args, false);
     }
 
     if ("visible".equalsIgnoreCase(value)) {
@@ -330,7 +396,7 @@
     }
 
     if ("mergeable".equalsIgnoreCase(value)) {
-      return new IsMergeablePredicate();
+      return new IsMergeablePredicate(schema(args.indexes), args.fillArgs);
     }
 
     try {
@@ -344,7 +410,7 @@
 
   @Operator
   public Predicate<ChangeData> commit(String id) {
-    return new CommitPredicate(args, AbbreviatedObjectId.fromString(id));
+    return new CommitPredicate(AbbreviatedObjectId.fromString(id));
   }
 
   @Operator
@@ -366,17 +432,14 @@
   }
 
   @Operator
-  public Predicate<ChangeData> projects(String name) throws QueryParseException {
-    if (!schema(args.indexes).hasField(ChangeField.PROJECTS)) {
-      throw new QueryParseException("Unsupported operator: " + FIELD_PROJECTS);
-    }
+  public Predicate<ChangeData> projects(String name) {
     return new ProjectPrefixPredicate(name);
   }
 
   @Operator
   public Predicate<ChangeData> parentproject(String name) {
-    return new ParentProjectPredicate(args.db, args.projectCache,
-        args.listChildProjects, args.self, name);
+    return new ParentProjectPredicate(args.projectCache, args.listChildProjects,
+        args.self, name);
   }
 
   @Operator
@@ -393,10 +456,15 @@
   }
 
   @Operator
+  public Predicate<ChangeData> hashtag(String hashtag) {
+    return new HashtagPredicate(hashtag);
+  }
+
+  @Operator
   public Predicate<ChangeData> topic(String name) {
     if (name.startsWith("^"))
-      return new RegexTopicPredicate(schema(args.indexes), name);
-    return new TopicPredicate(schema(args.indexes), name);
+      return new RegexTopicPredicate(name);
+    return new TopicPredicate(name);
   }
 
   @Operator
@@ -407,23 +475,23 @@
   }
 
   @Operator
-  public Predicate<ChangeData> f(String file) throws QueryParseException {
+  public Predicate<ChangeData> f(String file) {
     return file(file);
   }
 
   @Operator
-  public Predicate<ChangeData> file(String file) throws QueryParseException {
+  public Predicate<ChangeData> file(String file) {
     if (file.startsWith("^")) {
-      return new RegexPathPredicate(FIELD_FILE, file);
+      return new RegexPathPredicate(file);
     } else {
       return EqualsFilePredicate.create(args, file);
     }
   }
 
   @Operator
-  public Predicate<ChangeData> path(String path) throws QueryParseException {
+  public Predicate<ChangeData> path(String path) {
     if (path.startsWith("^")) {
-      return new RegexPathPredicate(FIELD_PATH, path);
+      return new RegexPathPredicate(path);
     } else {
       return new EqualsPathPredicate(FIELD_PATH, path);
     }
@@ -484,22 +552,21 @@
   }
 
   @Operator
-  public Predicate<ChangeData> message(String text) throws QueryParseException {
+  public Predicate<ChangeData> message(String text) {
     ChangeIndex index = args.indexes.getSearchIndex();
-    return new MessagePredicate(args, index, text);
+    return new MessagePredicate(index, text);
   }
 
   @Operator
   public Predicate<ChangeData> starredby(String who)
       throws QueryParseException, OrmException {
     if ("self".equals(who)) {
-      return new IsStarredByPredicate(args, currentUser);
+      return new IsStarredByPredicate(args);
     }
     Set<Account.Id> m = parseAccount(who);
     List<IsStarredByPredicate> p = Lists.newArrayListWithCapacity(m.size());
     for (Account.Id id : m) {
-      p.add(new IsStarredByPredicate(args,
-          args.userFactory.create(args.db, id)));
+      p.add(new IsStarredByPredicate(args.asUser(id)));
     }
     return Predicate.or(p);
   }
@@ -509,14 +576,26 @@
       throws QueryParseException, OrmException {
     Set<Account.Id> m = parseAccount(who);
     List<IsWatchedByPredicate> p = Lists.newArrayListWithCapacity(m.size());
-    for (Account.Id id : m) {
-      if (currentUser.isIdentifiedUser()
-          && id.equals(((IdentifiedUser) currentUser).getAccountId())) {
-        p.add(new IsWatchedByPredicate(args, currentUser, false));
+
+    Account.Id callerId;
+    try {
+      CurrentUser caller = args.self.get();
+      if (caller.isIdentifiedUser()) {
+        callerId = ((IdentifiedUser) caller).getAccountId();
       } else {
-        p.add(new IsWatchedByPredicate(args,
-            args.userFactory.create(args.db, id), true));
+        callerId = null;
       }
+    } catch (ProvisionException e) {
+      callerId = null;
+    }
+
+    for (Account.Id id : m) {
+      // Each child IsWatchedByPredicate includes a visibility filter for the
+      // corresponding user, to ensure that predicate subtree only returns
+      // changes visible to that user. The exception is if one of the users is
+      // the caller of this method, in which case visibility is already being
+      // checked at the top level.
+      p.add(new IsWatchedByPredicate(args.asUser(id), !id.equals(callerId)));
     }
     return Predicate.or(p);
   }
@@ -567,8 +646,8 @@
         user);
   }
 
-  public Predicate<ChangeData> is_visible() {
-    return visibleto(currentUser);
+  public Predicate<ChangeData> is_visible() throws QueryParseException {
+    return visibleto(args.getCurrentUser());
   }
 
   @Operator
@@ -636,47 +715,8 @@
   }
 
   @Operator
-  public Predicate<ChangeData> limit(String limit) {
-    return limit(Integer.parseInt(limit));
-  }
-
-  static class LimitPredicate extends IntPredicate<ChangeData> {
-    LimitPredicate(int limit) {
-      super(FIELD_LIMIT, limit);
-    }
-
-    @Override
-    public boolean match(ChangeData object) {
-      return true;
-    }
-
-    @Override
-    public int getCost() {
-      return 0;
-    }
-  }
-
-  public Predicate<ChangeData> limit(int limit) {
-    return new LimitPredicate(limit);
-  }
-
-  boolean supportsSortKey() {
-    return SortKeyPredicate.hasSortKeyField(schema(args.indexes));
-  }
-
-  @Operator
-  public Predicate<ChangeData> sortkey_after(String sortKey) {
-    return new SortKeyPredicate.After(schema(args.indexes), args.db, sortKey);
-  }
-
-  @Operator
-  public Predicate<ChangeData> sortkey_before(String sortKey) {
-    return new SortKeyPredicate.Before(schema(args.indexes), args.db, sortKey);
-  }
-
-  @Operator
-  public Predicate<ChangeData> resume_sortkey(String sortKey) {
-    return sortkey_before(sortKey);
+  public Predicate<ChangeData> limit(String limit) throws QueryParseException {
+    return new LimitPredicate(Integer.parseInt(limit));
   }
 
   @Operator
@@ -704,11 +744,15 @@
   }
 
   @Override
-  protected Predicate<ChangeData> defaultField(String query) {
+  protected Predicate<ChangeData> defaultField(String query) throws QueryParseException {
     if (query.startsWith("refs/")) {
       return ref(query);
     } else if (DEF_CHANGE.matcher(query).matches()) {
-      return change(query);
+      try {
+        return change(query);
+      } catch (QueryParseException e) {
+        // Skip.
+      }
     }
 
     List<Predicate<ChangeData>> predicates = Lists.newArrayListWithCapacity(9);
@@ -727,31 +771,15 @@
     } catch (OrmException | QueryParseException e) {
       // Skip.
     }
-    try {
-      predicates.add(file(query));
-    } catch (QueryParseException e) {
-      // Skip.
-    }
+    predicates.add(file(query));
     try {
       predicates.add(label(query));
     } catch (OrmException | QueryParseException e) {
       // Skip.
     }
-    try {
-      predicates.add(message(query));
-    } catch (QueryParseException e) {
-      // Skip.
-    }
-    try {
-      predicates.add(comment(query));
-    } catch (QueryParseException e) {
-      // Skip.
-    }
-    try {
-      predicates.add(projects(query));
-    } catch (QueryParseException e) {
-      // Skip.
-    }
+    predicates.add(message(query));
+    predicates.add(comment(query));
+    predicates.add(projects(query));
     predicates.add(ref(query));
     predicates.add(branch(query));
     predicates.add(topic(query));
@@ -785,9 +813,8 @@
       return Collections.singletonList(args.db.get().changes()
           .get(Change.Id.parse(value)));
     } else if (PAT_CHANGE_ID.matcher(value).matches()) {
-      Change.Key a = new Change.Key(parseChangeId(value));
       List<Change> changes =
-          args.db.get().changes().byKeyRange(a, a.max()).toList();
+          asChanges(args.queryProvider.get().byKeyPrefix(parseChangeId(value)));
       if (changes.isEmpty()) {
         throw error("Change " + value + " not found");
       }
@@ -804,11 +831,8 @@
     return value;
   }
 
-  private Account.Id self() {
-    if (currentUser.isIdentifiedUser()) {
-      return ((IdentifiedUser) currentUser).getAccountId();
-    }
-    throw new IllegalArgumentException();
+  private Account.Id self() throws QueryParseException {
+    return args.getIdentifiedUser().getAccountId();
   }
 
   private static Schema<ChangeData> schema(@Nullable IndexCollection indexes) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
index bbf235b..83492d2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
@@ -18,6 +18,6 @@
 import com.google.gerrit.server.query.QueryParseException;
 
 public interface ChangeQueryRewriter {
-  Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int start)
+  Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int start, int limit)
       throws QueryParseException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index cea6af8..8cbe71f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -14,20 +14,18 @@
 
 package com.google.gerrit.server.query.change;
 
-import static com.google.common.base.Preconditions.checkArgument;
-
-import com.google.common.collect.ImmutableBiMap;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gwtorm.server.OrmException;
-import com.google.inject.Provider;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.TreeMap;
 
 /**
  * Predicate for a {@link Status}.
@@ -35,49 +33,64 @@
  * The actual name of this operator can differ, it usually comes as {@code
  * status:} but may also be {@code is:} to help do-what-i-meanery for end-users
  * searching for changes. Either operator name has the same meaning.
+ * <p>
+ * Status names are looked up by prefix case-insensitively.
  */
 public final class ChangeStatusPredicate extends IndexPredicate<ChangeData> {
-  public static final ImmutableBiMap<Change.Status, String> VALUES;
+  private static final TreeMap<String, Predicate<ChangeData>> PREDICATES;
+  private static final Predicate<ChangeData> CLOSED;
+  private static final Predicate<ChangeData> OPEN;
 
   static {
-    ImmutableBiMap.Builder<Change.Status, String> values =
-        ImmutableBiMap.builder();
+    PREDICATES = new TreeMap<>();
+    List<Predicate<ChangeData>> open = new ArrayList<>();
+    List<Predicate<ChangeData>> closed = new ArrayList<>();
+
     for (Change.Status s : Change.Status.values()) {
-      values.put(s, s.name().toLowerCase());
+      ChangeStatusPredicate p = new ChangeStatusPredicate(s);
+      PREDICATES.put(canonicalize(s), p);
+      (s.isOpen() ? open : closed).add(p);
     }
-    VALUES = values.build();
+
+    CLOSED = Predicate.or(closed);
+    OPEN = Predicate.or(open);
+
+    PREDICATES.put("closed", CLOSED);
+    PREDICATES.put("open", OPEN);
+    PREDICATES.put("pending", OPEN);
   }
 
-  public static Predicate<ChangeData> open(Provider<ReviewDb> dbProvider) {
-    List<Predicate<ChangeData>> r = new ArrayList<>(4);
-    for (final Change.Status e : Change.Status.values()) {
-      if (e.isOpen()) {
-        r.add(new ChangeStatusPredicate(e));
-      }
-    }
-    return r.size() == 1 ? r.get(0) : or(r);
+  public static String canonicalize(Change.Status status) {
+    return status.name().toLowerCase();
   }
 
-  public static Predicate<ChangeData> closed(Provider<ReviewDb> dbProvider) {
-    List<Predicate<ChangeData>> r = new ArrayList<>(4);
-    for (final Change.Status e : Change.Status.values()) {
-      if (e.isClosed()) {
-        r.add(new ChangeStatusPredicate(e));
+  public static Predicate<ChangeData> parse(String value) {
+    String lower = value.toLowerCase();
+    NavigableMap<String, Predicate<ChangeData>> head =
+        PREDICATES.tailMap(lower, true);
+    if (!head.isEmpty()) {
+      // Assume no statuses share a common prefix so we can only walk one entry.
+      Map.Entry<String, Predicate<ChangeData>> e =
+          head.entrySet().iterator().next();
+      if (e.getKey().startsWith(lower)) {
+        return e.getValue();
       }
     }
-    return r.size() == 1 ? r.get(0) : or(r);
+    throw new IllegalArgumentException("invalid change status: " + value);
+  }
+
+  public static Predicate<ChangeData> open() {
+    return OPEN;
+  }
+
+  public static Predicate<ChangeData> closed() {
+    return CLOSED;
   }
 
   private final Change.Status status;
 
-  ChangeStatusPredicate(String value) {
-    super(ChangeField.STATUS, value);
-    status = VALUES.inverse().get(value);
-    checkArgument(status != null, "invalid change status: %s", value);
-  }
-
   ChangeStatusPredicate(Change.Status status) {
-    super(ChangeField.STATUS, VALUES.get(status));
+    super(ChangeField.STATUS, canonicalize(status));
     this.status = status;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
index 5994e5c..3983f62 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
@@ -19,16 +19,13 @@
 import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
 import com.google.gwtorm.server.OrmException;
 
 class CommentPredicate extends IndexPredicate<ChangeData> {
-  private final Arguments args;
   private final ChangeIndex index;
 
-  CommentPredicate(Arguments args, ChangeIndex index, String value) {
+  CommentPredicate(ChangeIndex index, String value) {
     super(ChangeField.COMMENT, value);
-    this.args = args;
     this.index = index;
   }
 
@@ -36,7 +33,7 @@
   public boolean match(ChangeData object) throws OrmException {
     try {
       for (ChangeData cData : index.getSource(
-          Predicate.and(new LegacyChangeIdPredicate(args, object.getId()), this), 0, 1)
+          Predicate.and(new LegacyChangeIdPredicate(object.getId()), this), 0, 1)
           .read()) {
         if (cData.getId().equals(object.getId())) {
           return true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
index 109d67a..14daa4d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
@@ -15,24 +15,18 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.IndexPredicate;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 
-class CommitPredicate extends IndexPredicate<ChangeData> implements
-    ChangeDataSource {
-  private final Arguments args;
+class CommitPredicate extends IndexPredicate<ChangeData> {
   private final AbbreviatedObjectId abbrevId;
 
-  CommitPredicate(Arguments args, AbbreviatedObjectId id) {
+  CommitPredicate(AbbreviatedObjectId id) {
     super(ChangeField.COMMIT, id.name());
-    this.args = args;
     this.abbrevId = id;
   }
 
@@ -50,30 +44,7 @@
   }
 
   @Override
-  public ResultSet<ChangeData> read() throws OrmException {
-    final RevId id = new RevId(abbrevId.name());
-    if (id.isComplete()) {
-      return ChangeDataResultSet.patchSet(args.changeDataFactory, args.db,
-          args.db.get().patchSets().byRevision(id));
-
-    } else {
-      return ChangeDataResultSet.patchSet(args.changeDataFactory, args.db,
-          args.db.get().patchSets().byRevisionRange(id, id.max()));
-    }
-  }
-
-  @Override
-  public boolean hasChange() {
-    return false;
-  }
-
-  @Override
-  public int getCardinality() {
-    return ChangeCosts.CARD_COMMIT;
-  }
-
-  @Override
   public int getCost() {
-    return ChangeCosts.cost(ChangeCosts.PATCH_SETS_SCAN, getCardinality());
+    return 1;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictKey.java
index e64ff13..3b3d986 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictKey.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.common.base.Objects;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.SubmitType;
 
 import org.eclipse.jgit.lib.ObjectId;
 
 import java.io.Serializable;
+import java.util.Objects;
 
 public class ConflictKey implements Serializable {
   private static final long serialVersionUID = 2L;
@@ -73,6 +73,6 @@
 
   @Override
   public int hashCode() {
-    return Objects.hashCode(commit, otherCommit, submitType, contentMerge);
+    return Objects.hash(commit, otherCommit, submitType, contentMerge);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java
index 1b3473e..8cca00d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsCacheImpl.java
@@ -23,7 +23,7 @@
 
 @Singleton
 public class ConflictsCacheImpl implements ConflictsCache {
-  public final static String NAME = "conflicts";
+  public static final String NAME = "conflicts";
 
   public static Module module() {
     return new CacheModule() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index 61454a8..2070746 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -17,16 +17,16 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.SubmitTypeRecord;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.git.strategy.SubmitStrategy;
-import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.OperatorPredicate;
 import com.google.gerrit.server.query.OrPredicate;
 import com.google.gerrit.server.query.Predicate;
@@ -35,7 +35,6 @@
 import com.google.inject.Provider;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
@@ -77,7 +76,7 @@
       List<Predicate<ChangeData>> predicatesForOneChange =
           Lists.newArrayListWithCapacity(5);
       predicatesForOneChange.add(
-          not(new LegacyChangeIdPredicate(args, c.getId())));
+          not(new LegacyChangeIdPredicate(c.getId())));
       predicatesForOneChange.add(
           new ProjectPredicate(c.getProject().get()));
       predicatesForOneChange.add(
@@ -95,7 +94,7 @@
           if (!otherChange.getDest().equals(c.getDest())) {
             return false;
           }
-          SubmitType submitType = getSubmitType(otherChange, object);
+          SubmitType submitType = getSubmitType(object);
           if (submitType == null) {
             return false;
           }
@@ -108,42 +107,24 @@
           if (conflicts != null) {
             return conflicts;
           }
-          try {
-            Repository repo =
+          try (Repository repo =
                 args.repoManager.openRepository(otherChange.getProject());
-            try {
-              RevWalk rw = new RevWalk(repo) {
-                @Override
-                protected RevCommit createCommit(AnyObjectId id) {
-                  return new CodeReviewCommit(id);
-                }
-              };
-              try {
-                RevFlag canMergeFlag = rw.newFlag("CAN_MERGE");
-                CodeReviewCommit commit =
-                    (CodeReviewCommit) rw.parseCommit(changeDataCache.getTestAgainst());
-                SubmitStrategy strategy =
-                    args.submitStrategyFactory.create(submitType,
-                        db.get(), repo, rw, null, canMergeFlag,
-                        getAlreadyAccepted(repo, rw, commit),
-                        otherChange.getDest());
-                CodeReviewCommit otherCommit =
-                    (CodeReviewCommit) rw.parseCommit(other);
-                otherCommit.add(canMergeFlag);
-                conflicts = !strategy.dryRun(commit, otherCommit);
-                args.conflictsCache.put(conflictsKey, conflicts);
-                return conflicts;
-              } catch (MergeException e) {
-                throw new IllegalStateException(e);
-              } catch (NoSuchProjectException e) {
-                throw new IllegalStateException(e);
-              } finally {
-                rw.release();
-              }
-            } finally {
-              repo.close();
-            }
-          } catch (IOException e) {
+              RevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
+            RevFlag canMergeFlag = rw.newFlag("CAN_MERGE");
+            CodeReviewCommit commit =
+                (CodeReviewCommit) rw.parseCommit(changeDataCache.getTestAgainst());
+            SubmitStrategy strategy =
+                args.submitStrategyFactory.create(submitType,
+                    db.get(), repo, rw, null, canMergeFlag,
+                    getAlreadyAccepted(repo, rw, commit),
+                    otherChange.getDest());
+            CodeReviewCommit otherCommit =
+                (CodeReviewCommit) rw.parseCommit(other);
+            otherCommit.add(canMergeFlag);
+            conflicts = !strategy.dryRun(commit, otherCommit);
+            args.conflictsCache.put(conflictsKey, conflicts);
+            return conflicts;
+          } catch (MergeException | NoSuchProjectException | IOException e) {
             throw new IllegalStateException(e);
           }
         }
@@ -153,19 +134,12 @@
           return 5;
         }
 
-        private SubmitType getSubmitType(Change change, ChangeData cd) throws OrmException {
-          try {
-            final SubmitTypeRecord r =
-                args.changeControlGenericFactory.controlFor(change,
-                    args.userFactory.create(change.getOwner()))
-                    .getSubmitTypeRecord(db.get(), cd.currentPatchSet(), cd);
-            if (r.status != SubmitTypeRecord.Status.OK) {
-              return null;
-            }
-            return r.type;
-          } catch (NoSuchChangeException e) {
+        private SubmitType getSubmitType(ChangeData cd) throws OrmException {
+          SubmitTypeRecord r = new SubmitRuleEvaluator(cd).getSubmitType();
+          if (r.status != SubmitTypeRecord.Status.OK) {
             return null;
           }
+          return r.type;
         }
 
         private Set<RevCommit> getAlreadyAccepted(Repository repo, RevWalk rw,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
index 53d2bbd..ebb7389 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
@@ -33,7 +33,8 @@
   private final Arguments args;
   private final Account.Id accountId;
 
-  HasDraftByPredicate(Arguments args, Account.Id accountId) {
+  HasDraftByPredicate(Arguments args,
+      Account.Id accountId) {
     super(ChangeQueryBuilder.FIELD_DRAFTBY, accountId.toString());
     this.args = args;
     this.accountId = accountId;
@@ -41,20 +42,16 @@
 
   @Override
   public boolean match(final ChangeData object) throws OrmException {
-    for (PatchLineComment c : object.comments()) {
-      if (c.getStatus() == PatchLineComment.Status.DRAFT
-          && c.getAuthor().equals(accountId)) {
-        return true;
-      }
-    }
-    return false;
+    return !args.plcUtil
+        .draftByChangeAuthor(args.db.get(), object.notes(), accountId)
+        .isEmpty();
   }
 
   @Override
   public ResultSet<ChangeData> read() throws OrmException {
     Set<Change.Id> ids = new HashSet<>();
-    for (PatchLineComment sc : args.db.get().patchComments()
-        .draftByAuthor(accountId)) {
+    for (PatchLineComment sc :
+        args.plcUtil.draftByAuthor(args.db.get(), accountId)) {
       ids.add(sc.getKey().getParentKey().getParentKey().getParentKey());
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HashtagPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HashtagPredicate.java
new file mode 100644
index 0000000..ea591ec
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HashtagPredicate.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.server.change.HashtagsUtil;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gwtorm.server.OrmException;
+
+class HashtagPredicate extends IndexPredicate<ChangeData> {
+  HashtagPredicate(String hashtag) {
+    super(ChangeField.HASHTAG, HashtagsUtil.cleanupHashtag(hashtag));
+  }
+
+  @Override
+  public boolean match(final ChangeData object) throws OrmException {
+    for (String hashtag : object.notes().load().getHashtags()) {
+      if (hashtag.equalsIgnoreCase(getValue())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
+
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
new file mode 100644
index 0000000..e08847a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -0,0 +1,133 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import static com.google.gerrit.server.query.Predicate.and;
+import static com.google.gerrit.server.query.change.ChangeStatusPredicate.open;
+
+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.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import java.util.List;
+
+/**
+ * Execute a single query over changes, for use by Gerrit internals.
+ * <p>
+ * By default, visibility of returned changes is not enforced (unlike in {@link
+ * QueryProcessor}). The methods in this class are not typically used by
+ * user-facing paths, but rather by internal callers that need to process all
+ * matching results.
+ */
+public class InternalChangeQuery {
+  private static Predicate<ChangeData> ref(Branch.NameKey branch) {
+    return new RefPredicate(branch.get());
+  }
+
+  private static Predicate<ChangeData> change(Change.Key key) {
+    return new ChangeIdPredicate(key.get());
+  }
+
+  private static Predicate<ChangeData> project(Project.NameKey project) {
+    return new ProjectPredicate(project.get());
+  }
+
+  private static Predicate<ChangeData> status(Change.Status status) {
+    return new ChangeStatusPredicate(status);
+  }
+
+  private static Predicate<ChangeData> topic(String topic) {
+    return new TopicPredicate(topic);
+  }
+
+  private final QueryProcessor qp;
+
+  @Inject
+  InternalChangeQuery(QueryProcessor queryProcessor) {
+    qp = queryProcessor.enforceVisibility(false);
+  }
+
+  public InternalChangeQuery setLimit(int n) {
+    qp.setLimit(n);
+    return this;
+  }
+
+  public InternalChangeQuery enforceVisibility(boolean enforce) {
+    qp.enforceVisibility(enforce);
+    return this;
+  }
+
+  public List<ChangeData> byKey(Change.Key key) throws OrmException {
+    return byKeyPrefix(key.get());
+  }
+
+  public List<ChangeData> byKeyPrefix(String prefix) throws OrmException {
+    return query(new ChangeIdPredicate(prefix));
+  }
+
+  public List<ChangeData> byBranchKey(Branch.NameKey branch, Change.Key key)
+      throws OrmException {
+    return query(and(
+        ref(branch),
+        project(branch.getParentKey()),
+        change(key)));
+  }
+
+  public List<ChangeData> byProject(Project.NameKey project)
+      throws OrmException {
+    return query(project(project));
+  }
+
+  public List<ChangeData> submitted(Branch.NameKey branch) throws OrmException {
+    return query(and(
+        ref(branch),
+        project(branch.getParentKey()),
+        status(Change.Status.SUBMITTED)));
+  }
+
+  public List<ChangeData> allSubmitted() throws OrmException {
+    return query(status(Change.Status.SUBMITTED));
+  }
+
+  public List<ChangeData> byBranchOpen(Branch.NameKey branch)
+      throws OrmException {
+    return query(and(
+        ref(branch),
+        project(branch.getParentKey()),
+        open()));
+  }
+
+  public List<ChangeData> byProjectOpen(Project.NameKey project)
+      throws OrmException {
+    return query(and(project(project), open()));
+  }
+
+  public List<ChangeData> byTopicOpen(String topic)
+      throws OrmException {
+    return query(and(topic(topic), open()));
+  }
+
+  private List<ChangeData> query(Predicate<ChangeData> p) throws OrmException {
+    try {
+      return qp.queryChanges(p).changes();
+    } catch (QueryParseException e) {
+      throw new OrmException(e);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java
index 787b90e..6ef4ab6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java
@@ -14,20 +14,39 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.client.Change;
+import static com.google.gerrit.server.index.ChangeField.MERGEABLE;
+
 import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.FieldDef;
+import com.google.gerrit.server.index.FieldDef.FillArgs;
 import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.Schema;
 import com.google.gwtorm.server.OrmException;
 
 class IsMergeablePredicate extends IndexPredicate<ChangeData> {
-  IsMergeablePredicate() {
-    super(ChangeField.MERGEABLE, "1");
+  @SuppressWarnings("deprecation")
+  static FieldDef<ChangeData, ?> mergeableField(Schema<ChangeData> schema) {
+    if (schema == null) {
+      return ChangeField.LEGACY_MERGEABLE;
+    }
+    FieldDef<ChangeData, ?> f = schema.getFields().get(MERGEABLE.getName());
+    if (f != null) {
+      return f;
+    }
+    return schema.getFields().get(ChangeField.LEGACY_MERGEABLE.getName());
+  }
+
+  private final FillArgs args;
+
+  IsMergeablePredicate(Schema<ChangeData> schema,
+      FillArgs args) {
+    super(mergeableField(schema), "1");
+    this.args = args;
   }
 
   @Override
   public boolean match(ChangeData object) throws OrmException {
-    Change c = object.change();
-    return c != null && c.isMergeable();
+    return getValue().equals(getField().get(object, args));
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
index d25d5a5..b5bef07 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.query.OrPredicate;
 import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
@@ -36,12 +37,10 @@
     return user.toString();
   }
 
-  private static List<Predicate<ChangeData>> predicates(
-      Arguments args,
-      Set<Change.Id> ids) {
+  private static List<Predicate<ChangeData>> predicates(Set<Change.Id> ids) {
     List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(ids.size());
     for (Change.Id id : ids) {
-      r.add(new LegacyChangeIdPredicate(args, id));
+      r.add(new LegacyChangeIdPredicate(id));
     }
     return r;
   }
@@ -49,8 +48,12 @@
   private final Arguments args;
   private final CurrentUser user;
 
-  IsStarredByPredicate(Arguments args, CurrentUser user) {
-    super(predicates(args, user.getStarredChanges()));
+  IsStarredByPredicate(Arguments args) throws QueryParseException {
+    this(args, args.getIdentifiedUser());
+  }
+
+  private IsStarredByPredicate(Arguments args, IdentifiedUser user) {
+    super(predicates(user.getStarredChanges()));
     this.args = args;
     this.user = user;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
index c1e99a3..606f577 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -37,18 +37,17 @@
   private final CurrentUser user;
 
   IsWatchedByPredicate(ChangeQueryBuilder.Arguments args,
-      CurrentUser user,
-      boolean checkIsVisible) {
-    super(filters(args, user, checkIsVisible));
-    this.user = user;
+      boolean checkIsVisible) throws QueryParseException {
+    super(filters(args, checkIsVisible));
+    this.user = args.getCurrentUser();
   }
 
   private static List<Predicate<ChangeData>> filters(
       ChangeQueryBuilder.Arguments args,
-      CurrentUser user,
-      boolean checkIsVisible) {
+      boolean checkIsVisible) throws QueryParseException {
+    CurrentUser user = args.getCurrentUser();
     List<Predicate<ChangeData>> r = Lists.newArrayList();
-    ChangeQueryBuilder builder = new ChangeQueryBuilder(args, user);
+    ChangeQueryBuilder builder = new ChangeQueryBuilder(args);
     for (AccountProjectWatch w : user.getNotificationFilters()) {
       Predicate<ChangeData> f = null;
       if (w.getFilter() != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
index 9b5aae3..83364c3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -90,14 +90,14 @@
 
     try {
       LabelVote lv = LabelVote.parse(v);
-      parsed = new Parsed(lv.getLabel(), "=", lv.getValue());
+      parsed = new Parsed(lv.label(), "=", lv.value());
     } catch (IllegalArgumentException e) {
       // Try next format.
     }
 
     try {
       LabelVote lv = LabelVote.parseWithEquals(v);
-      parsed = new Parsed(lv.getLabel(), "=", lv.getValue());
+      parsed = new Parsed(lv.label(), "=", lv.value());
     } catch (IllegalArgumentException e) {
       // Try next format.
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
index 1cbb499..9ecc7b2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
@@ -17,21 +17,13 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.IndexPredicate;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
-import com.google.gwtorm.server.ListResultSet;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 
-import java.util.Collections;
-
-class LegacyChangeIdPredicate extends IndexPredicate<ChangeData> implements
-    ChangeDataSource {
-  private final Arguments args;
+/** Predicate over change number (aka legacy ID or Change.Id). */
+class LegacyChangeIdPredicate extends IndexPredicate<ChangeData> {
   private final Change.Id id;
 
-  LegacyChangeIdPredicate(Arguments args, Change.Id id) {
+  LegacyChangeIdPredicate(Change.Id id) {
     super(ChangeField.LEGACY_ID, ChangeQueryBuilder.FIELD_CHANGE, id.toString());
-    this.args = args;
     this.id = id;
   }
 
@@ -41,28 +33,7 @@
   }
 
   @Override
-  public ResultSet<ChangeData> read() throws OrmException {
-    Change c = args.db.get().changes().get(id);
-    if (c != null) {
-      return new ListResultSet<>(Collections.singletonList(
-          args.changeDataFactory.create(args.db.get(), c)));
-    } else {
-      return new ListResultSet<>(Collections.<ChangeData> emptyList());
-    }
-  }
-
-  @Override
-  public boolean hasChange() {
-    return true;
-  }
-
-  @Override
-  public int getCardinality() {
-    return 1;
-  }
-
-  @Override
   public int getCost() {
-    return ChangeCosts.IDS_MEMORY;
+    return 1;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LimitPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LimitPredicate.java
new file mode 100644
index 0000000..0e90ddf
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LimitPredicate.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_LIMIT;
+
+import com.google.gerrit.server.query.IntPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryBuilder;
+import com.google.gerrit.server.query.QueryParseException;
+
+public class LimitPredicate extends IntPredicate<ChangeData> {
+  @SuppressWarnings("unchecked")
+  public static Integer getLimit(Predicate<ChangeData> p) {
+    IntPredicate<?> ip = QueryBuilder.find(p, IntPredicate.class, FIELD_LIMIT);
+    return ip != null ? ip.intValue() : null;
+  }
+
+  public LimitPredicate(int limit) throws QueryParseException {
+    super(ChangeQueryBuilder.FIELD_LIMIT, limit);
+    if (limit <= 0) {
+      throw new QueryParseException("limit must be positive: " + limit);
+    }
+  }
+
+  @Override
+  public boolean match(ChangeData object) {
+    return true;
+  }
+
+  @Override
+  public int getCost() {
+    return 0;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
index 04bdb1e..0a16d02 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
 import com.google.gwtorm.server.OrmException;
 
 /**
@@ -27,12 +26,10 @@
  * body.
  */
 class MessagePredicate extends IndexPredicate<ChangeData> {
-  private final Arguments args;
   private final ChangeIndex index;
 
-  MessagePredicate(Arguments args, ChangeIndex index, String value) {
+  MessagePredicate(ChangeIndex index, String value) {
     super(ChangeField.COMMIT_MESSAGE, value);
-    this.args = args;
     this.index = index;
   }
 
@@ -40,7 +37,7 @@
   public boolean match(ChangeData object) throws OrmException {
     try {
       for (ChangeData cData : index.getSource(
-          Predicate.and(new LegacyChangeIdPredicate(args, object.getId()), this), 0, 1)
+          Predicate.and(new LegacyChangeIdPredicate(object.getId()), this), 0, 1)
           .read()) {
         if (cData.getId().equals(object.getId())) {
           return true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
new file mode 100644
index 0000000..52846ed
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -0,0 +1,404 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.data.ChangeAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
+import com.google.gerrit.server.data.QueryStatsAttribute;
+import com.google.gerrit.server.events.EventFactory;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gson.Gson;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.util.io.DisabledOutputStream;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Change query implementation that outputs to a stream in the style of an SSH
+ * command.
+ */
+public class OutputStreamQuery {
+  private static final Logger log =
+      LoggerFactory.getLogger(OutputStreamQuery.class);
+
+  private static final DateTimeFormatter dtf =
+      DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss zzz");
+
+  public static enum OutputFormat {
+    TEXT, JSON
+  }
+
+  private final ChangeQueryBuilder queryBuilder;
+  private final QueryProcessor queryProcessor;
+  private final EventFactory eventFactory;
+  private final TrackingFooters trackingFooters;
+  private final CurrentUser user;
+
+  private OutputFormat outputFormat = OutputFormat.TEXT;
+  private boolean includePatchSets;
+  private boolean includeCurrentPatchSet;
+  private boolean includeApprovals;
+  private boolean includeComments;
+  private boolean includeFiles;
+  private boolean includeCommitMessage;
+  private boolean includeDependencies;
+  private boolean includeSubmitRecords;
+  private boolean includeAllReviewers;
+
+  private OutputStream outputStream = DisabledOutputStream.INSTANCE;
+  private PrintWriter out;
+
+  @Inject
+  OutputStreamQuery(
+      ChangeQueryBuilder queryBuilder,
+      QueryProcessor queryProcessor,
+      EventFactory eventFactory,
+      TrackingFooters trackingFooters,
+      CurrentUser user) {
+    this.queryBuilder = queryBuilder;
+    this.queryProcessor = queryProcessor;
+    this.eventFactory = eventFactory;
+    this.trackingFooters = trackingFooters;
+    this.user = user;
+  }
+
+  void setLimit(int n) {
+    queryProcessor.setLimit(n);
+  }
+
+  public void setStart(int n) {
+    queryProcessor.setStart(n);
+  }
+
+  public void setIncludePatchSets(boolean on) {
+    includePatchSets = on;
+  }
+
+  public boolean getIncludePatchSets() {
+    return includePatchSets;
+  }
+
+  public void setIncludeCurrentPatchSet(boolean on) {
+    includeCurrentPatchSet = on;
+  }
+
+  public boolean getIncludeCurrentPatchSet() {
+    return includeCurrentPatchSet;
+  }
+
+  public void setIncludeApprovals(boolean on) {
+    includeApprovals = on;
+  }
+
+  public void setIncludeComments(boolean on) {
+    includeComments = on;
+  }
+
+  public void setIncludeFiles(boolean on) {
+    includeFiles = on;
+  }
+
+  public boolean getIncludeFiles() {
+    return includeFiles;
+  }
+
+  public void setIncludeDependencies(boolean on) {
+    includeDependencies = on;
+  }
+
+  public boolean getIncludeDependencies() {
+    return includeDependencies;
+  }
+
+  public void setIncludeCommitMessage(boolean on) {
+    includeCommitMessage = on;
+  }
+
+  public void setIncludeSubmitRecords(boolean on) {
+    includeSubmitRecords = on;
+  }
+
+  public void setIncludeAllReviewers(boolean on) {
+    includeAllReviewers = on;
+  }
+
+  public void setOutput(OutputStream out, OutputFormat fmt) {
+    this.outputStream = out;
+    this.outputFormat = fmt;
+  }
+
+  public void query(String queryString) throws IOException {
+    out = new PrintWriter( //
+        new BufferedWriter( //
+            new OutputStreamWriter(outputStream, "UTF-8")));
+    try {
+      if (queryProcessor.isDisabled()) {
+        ErrorMessage m = new ErrorMessage();
+        m.message = "query disabled";
+        show(m);
+        return;
+      }
+
+      try {
+        final QueryStatsAttribute stats = new QueryStatsAttribute();
+        stats.runTimeMilliseconds = TimeUtil.nowMs();
+
+        QueryResult results =
+            queryProcessor.queryChanges(queryBuilder.parse(queryString));
+        ChangeAttribute c = null;
+        for (ChangeData d : results.changes()) {
+          ChangeControl cc = d.changeControl().forUser(user);
+
+          LabelTypes labelTypes = cc.getLabelTypes();
+          c = eventFactory.asChangeAttribute(d.change());
+          eventFactory.extend(c, d.change());
+
+          if (!trackingFooters.isEmpty()) {
+            eventFactory.addTrackingIds(c,
+                trackingFooters.extract(d.commitFooters()));
+          }
+
+          if (includeAllReviewers) {
+            eventFactory.addAllReviewers(c, d.notes());
+          }
+
+          if (includeSubmitRecords) {
+            eventFactory.addSubmitRecords(c, new SubmitRuleEvaluator(d)
+                .setAllowClosed(true)
+                .setAllowDraft(true)
+                .canSubmit());
+          }
+
+          if (includeCommitMessage) {
+            eventFactory.addCommitMessage(c, d.commitMessage());
+          }
+
+          if (includePatchSets) {
+            if (includeFiles) {
+              eventFactory.addPatchSets(c, d.patches(),
+                includeApprovals ? d.approvals().asMap() : null,
+                includeFiles, d.change(), labelTypes);
+            } else {
+              eventFactory.addPatchSets(c, d.patches(),
+                  includeApprovals ? d.approvals().asMap() : null,
+                  labelTypes);
+            }
+          }
+
+          if (includeCurrentPatchSet) {
+            PatchSet current = d.currentPatchSet();
+            if (current != null) {
+              c.currentPatchSet = eventFactory.asPatchSetAttribute(current);
+              eventFactory.addApprovals(c.currentPatchSet,
+                  d.currentApprovals(), labelTypes);
+
+              if (includeFiles) {
+                eventFactory.addPatchSetFileNames(c.currentPatchSet,
+                    d.change(), d.currentPatchSet());
+              }
+            }
+          }
+
+          if (includeComments) {
+            eventFactory.addComments(c, d.messages());
+            if (includePatchSets) {
+              for (PatchSetAttribute attribute : c.patchSets) {
+                eventFactory.addPatchSetComments(attribute,  d.publishedComments());
+              }
+            }
+          }
+
+          if (includeDependencies) {
+            eventFactory.addDependencies(c, d.change());
+          }
+
+          show(c);
+        }
+
+        stats.rowCount = results.changes().size();
+        stats.moreChanges = results.moreChanges();
+        stats.runTimeMilliseconds =
+            TimeUtil.nowMs() - stats.runTimeMilliseconds;
+        show(stats);
+      } catch (OrmException err) {
+        log.error("Cannot execute query: " + queryString, err);
+
+        ErrorMessage m = new ErrorMessage();
+        m.message = "cannot query database";
+        show(m);
+
+      } catch (QueryParseException e) {
+        ErrorMessage m = new ErrorMessage();
+        m.message = e.getMessage();
+        show(m);
+      }
+    } finally {
+      try {
+        out.flush();
+      } finally {
+        out = null;
+      }
+    }
+  }
+
+  private void show(Object data) {
+    switch (outputFormat) {
+      default:
+      case TEXT:
+        if (data instanceof ChangeAttribute) {
+          out.print("change ");
+          out.print(((ChangeAttribute) data).id);
+          out.print("\n");
+          showText(data, 1);
+        } else {
+          showText(data, 0);
+        }
+        out.print('\n');
+        break;
+
+      case JSON:
+        out.print(new Gson().toJson(data));
+        out.print('\n');
+        break;
+    }
+  }
+
+  private void showText(Object data, int depth) {
+    for (Field f : fieldsOf(data.getClass())) {
+      Object val;
+      try {
+        val = f.get(data);
+      } catch (IllegalArgumentException err) {
+        continue;
+      } catch (IllegalAccessException err) {
+        continue;
+      }
+      if (val == null) {
+        continue;
+      }
+
+      showField(f.getName(), val, depth);
+    }
+  }
+
+  private String indent(int spaces) {
+    if (spaces == 0) {
+      return "";
+    } else {
+      return String.format("%" + spaces + "s", " ");
+    }
+  }
+
+  private void showField(String field, Object value, int depth) {
+    final int spacesDepthRatio = 2;
+    String indent = indent(depth * spacesDepthRatio);
+    out.print(indent);
+    out.print(field);
+    out.print(':');
+    if (value instanceof String && ((String) value).contains("\n")) {
+      out.print(' ');
+      // Idention for multi-line text is
+      // current depth indetion + length of field + length of ": "
+      indent = indent(indent.length() + field.length() + spacesDepthRatio);
+      out.print(((String) value).replaceAll("\n", "\n" + indent).trim());
+      out.print('\n');
+    } else if (value instanceof Long && isDateField(field)) {
+      out.print(' ');
+      out.print(dtf.print(((Long) value) * 1000L));
+      out.print('\n');
+    } else if (isPrimitive(value)) {
+      out.print(' ');
+      out.print(value);
+      out.print('\n');
+    } else if (value instanceof Collection) {
+      out.print('\n');
+      boolean firstElement = true;
+      for (Object thing : ((Collection<?>) value)) {
+        // The name of the collection was initially printed at the beginning
+        // of this routine.  Beginning at the second sub-element, reprint
+        // the collection name so humans can separate individual elements
+        // with less strain and error.
+        //
+        if (firstElement) {
+          firstElement = false;
+        } else {
+          out.print(indent);
+          out.print(field);
+          out.print(":\n");
+        }
+        if (isPrimitive(thing)) {
+          out.print(' ');
+          out.print(value);
+          out.print('\n');
+        } else {
+          showText(thing, depth + 1);
+        }
+      }
+    } else {
+      out.print('\n');
+      showText(value, depth + 1);
+    }
+  }
+
+  private static boolean isPrimitive(Object value) {
+    return value instanceof String //
+        || value instanceof Number //
+        || value instanceof Boolean //
+        || value instanceof Enum;
+  }
+
+  private static boolean isDateField(String name) {
+    return "lastUpdated".equals(name) //
+        || "grantedOn".equals(name) //
+        || "timestamp".equals(name) //
+        || "createdOn".equals(name);
+  }
+
+  private List<Field> fieldsOf(Class<?> type) {
+    List<Field> r = new ArrayList<>();
+    if (type.getSuperclass() != null) {
+      r.addAll(fieldsOf(type.getSuperclass()));
+    }
+    r.addAll(Arrays.asList(type.getDeclaredFields()));
+    return r;
+  }
+
+  static class ErrorMessage {
+    public final String type = "error";
+    public String message;
+  }
+}
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 e411cf9..7afd934 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
@@ -20,7 +20,5 @@
 public interface Paginated {
   int limit();
 
-  ResultSet<ChangeData> restart(ChangeData last) throws OrmException;
-
   ResultSet<ChangeData> restart(int start) throws OrmException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
index 253f719..2cabfc5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
@@ -17,7 +17,6 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.common.ProjectInfo;
 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.project.ListChildProjects;
 import com.google.gerrit.server.project.ProjectCache;
@@ -33,15 +32,15 @@
 class ParentProjectPredicate extends OrPredicate<ChangeData> {
   private final String value;
 
-  ParentProjectPredicate(Provider<ReviewDb> dbProvider,
-      ProjectCache projectCache, Provider<ListChildProjects> listChildProjects,
-      Provider<CurrentUser> self, String value) {
-    super(predicates(dbProvider, projectCache, listChildProjects, self, value));
+  ParentProjectPredicate(ProjectCache projectCache,
+      Provider<ListChildProjects> listChildProjects, Provider<CurrentUser> self,
+      String value) {
+    super(predicates(projectCache, listChildProjects, self, value));
     this.value = value;
   }
 
   private static List<Predicate<ChangeData>> predicates(
-      Provider<ReviewDb> dbProvider, ProjectCache projectCache,
+      ProjectCache projectCache,
       Provider<ListChildProjects> listChildProjects,
       Provider<CurrentUser> self, String value) {
     ProjectState projectState = projectCache.get(new Project.NameKey(value));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
index 4808b47..299f0f1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
@@ -15,7 +15,8 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.common.collect.Lists;
-import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestReadView;
@@ -23,7 +24,6 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.ChangeJson;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -31,7 +31,6 @@
 
 import org.kohsuke.args4j.Option;
 
-import java.util.BitSet;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.List;
@@ -40,9 +39,9 @@
 
 public class QueryChanges implements RestReadView<TopLevelResource> {
   private final ChangeJson json;
+  private final ChangeQueryBuilder qb;
   private final QueryProcessor imp;
   private final Provider<CurrentUser> user;
-  private boolean reverse;
   private EnumSet<ListChangesOption> options;
 
   @Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", usage = "Query string")
@@ -63,31 +62,18 @@
     options.addAll(ListChangesOption.fromBits(Integer.parseInt(hex, 16)));
   }
 
-  @Option(name = "-P", metaVar = "SORTKEY", usage = "Previous changes before SORTKEY")
-  public void setSortKeyAfter(String key) {
-    // Querying for the prior page of changes requires sortkey_after predicate.
-    // Changes are shown most recent->least recent. The previous page of
-    // results contains changes that were updated after the given key.
-    imp.setSortkeyAfter(key);
-    reverse = true;
-  }
-
-  @Option(name = "-N", metaVar = "SORTKEY", usage = "Next changes after SORTKEY")
-  public void setSortKeyBefore(String key) {
-    // Querying for the next page of changes requires sortkey_before predicate.
-    // Changes are shown most recent->least recent. The next page contains
-    // changes that were updated before the given key.
-    imp.setSortkeyBefore(key);
-  }
-
   @Option(name = "--start", aliases = {"-S"}, metaVar = "CNT", usage = "Number of changes to skip")
   public void setStart(int start) {
     imp.setStart(start);
   }
 
   @Inject
-  QueryChanges(ChangeJson json, QueryProcessor qp, Provider<CurrentUser> user) {
+  QueryChanges(ChangeJson json,
+      ChangeQueryBuilder qb,
+      QueryProcessor qp,
+      Provider<CurrentUser> user) {
     this.json = json;
+    this.qb = qb;
     this.imp = qp;
     this.user = user;
 
@@ -154,30 +140,13 @@
   private List<List<ChangeInfo>> query0() throws OrmException,
       QueryParseException {
     int cnt = queries.size();
-    BitSet more = new BitSet(cnt);
-    List<List<ChangeData>> data = imp.queryChanges(queries);
-    for (int n = 0; n < cnt; n++) {
-      List<ChangeData> changes = data.get(n);
-      if (imp.getLimit() > 0 && changes.size() > imp.getLimit()) {
-        if (reverse) {
-          changes = changes.subList(1, changes.size());
-        } else {
-          changes = changes.subList(0, imp.getLimit());
-        }
-        data.set(n, changes);
-        more.set(n, true);
-      }
-    }
-
-    List<List<ChangeInfo>> res = json.addOptions(options).formatList2(data);
+    List<QueryResult> results = imp.queryChanges(qb.parse(queries));
+    List<List<ChangeInfo>> res = json.addOptions(options)
+        .formatQueryResults(results);
     for (int n = 0; n < cnt; n++) {
       List<ChangeInfo> info = res.get(n);
-      if (more.get(n) && !info.isEmpty()) {
-        if (reverse) {
-          info.get(0)._moreChanges = true;
-        } else {
-          info.get(info.size() - 1)._moreChanges = true;
-        }
+      if (results.get(n).moreChanges()) {
+        info.get(info.size() - 1)._moreChanges = true;
       }
     }
     return res;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
index 7927cbb..51d971d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
@@ -14,553 +14,184 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.common.base.Objects;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.query.change.ChangeStatusPredicate.open;
+
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.SubmitRecord;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.config.TrackingFooters;
-import com.google.gerrit.server.data.ChangeAttribute;
-import com.google.gerrit.server.data.PatchSetAttribute;
-import com.google.gerrit.server.data.QueryStatsAttribute;
-import com.google.gerrit.server.events.EventFactory;
+import com.google.gerrit.server.index.IndexConfig;
+import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
-import com.google.gerrit.server.util.TimeUtil;
-import com.google.gson.Gson;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.eclipse.jgit.util.io.DisabledOutputStream;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.lang.reflect.Field;
-import java.text.SimpleDateFormat;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
 import java.util.List;
 
 public class QueryProcessor {
-  private static final Logger log =
-      LoggerFactory.getLogger(QueryProcessor.class);
-
-  private final Comparator<ChangeData> cmpAfter =
-      new Comparator<ChangeData>() {
-        @Override
-        public int compare(ChangeData a, ChangeData b) {
-          try {
-            return a.change().getSortKey().compareTo(b.change().getSortKey());
-          } catch (OrmException e) {
-            return 0;
-          }
-        }
-      };
-
-  private final Comparator<ChangeData> cmpBefore =
-      new Comparator<ChangeData>() {
-        @Override
-        public int compare(ChangeData a, ChangeData b) {
-          try {
-            return b.change().getSortKey().compareTo(a.change().getSortKey());
-          } catch (OrmException e) {
-            return 0;
-          }
-        }
-      };
-
-  public static enum OutputFormat {
-    TEXT, JSON
-  }
-
-  private final Gson gson = new Gson();
-  private final SimpleDateFormat sdf =
-      new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz");
-
-  private final EventFactory eventFactory;
-  private final ChangeQueryBuilder queryBuilder;
-  private final ChangeQueryRewriter queryRewriter;
   private final Provider<ReviewDb> db;
-  private final TrackingFooters trackingFooters;
-  private final CurrentUser user;
-  private final int maxLimit;
+  private final Provider<CurrentUser> userProvider;
+  private final ChangeControl.GenericFactory changeControlFactory;
+  private final ChangeQueryRewriter queryRewriter;
+  private final IndexConfig indexConfig;
 
-  private OutputFormat outputFormat = OutputFormat.TEXT;
-  private int limit;
+  private int limitFromCaller;
   private int start;
-  private String sortkeyAfter;
-  private String sortkeyBefore;
-  private boolean includePatchSets;
-  private boolean includeCurrentPatchSet;
-  private boolean includeApprovals;
-  private boolean includeComments;
-  private boolean includeFiles;
-  private boolean includeCommitMessage;
-  private boolean includeDependencies;
-  private boolean includeSubmitRecords;
-  private boolean includeAllReviewers;
-
-  private OutputStream outputStream = DisabledOutputStream.INSTANCE;
-  private PrintWriter out;
-  private boolean moreResults;
+  private boolean enforceVisibility = true;
 
   @Inject
-  QueryProcessor(EventFactory eventFactory,
-      ChangeQueryBuilder.Factory queryBuilder, CurrentUser currentUser,
-      ChangeQueryRewriter queryRewriter, Provider<ReviewDb> db,
-      TrackingFooters trackingFooters) {
-    this.eventFactory = eventFactory;
-    this.queryBuilder = queryBuilder.create(currentUser);
-    this.queryRewriter = queryRewriter;
+  QueryProcessor(Provider<ReviewDb> db,
+      Provider<CurrentUser> userProvider,
+      ChangeControl.GenericFactory changeControlFactory,
+      ChangeQueryRewriter queryRewriter,
+      IndexConfig indexConfig) {
     this.db = db;
-    this.trackingFooters = trackingFooters;
-    this.user = currentUser;
-    this.maxLimit = currentUser.getCapabilities()
-      .getRange(GlobalCapability.QUERY_LIMIT)
-      .getMax();
-    this.moreResults = false;
+    this.userProvider = userProvider;
+    this.changeControlFactory = changeControlFactory;
+    this.queryRewriter = queryRewriter;
+    this.indexConfig = indexConfig;
   }
 
-  int getLimit() {
-    return limit;
+  public QueryProcessor enforceVisibility(boolean enforce) {
+    enforceVisibility = enforce;
+    return this;
   }
 
-  void setLimit(int n) {
-    limit = n;
+  public QueryProcessor setLimit(int n) {
+    limitFromCaller = n;
+    return this;
   }
 
-  public void setStart(int n) {
+  public QueryProcessor setStart(int n) {
     start = n;
-  }
-
-  void setSortkeyAfter(String sortkey) {
-    sortkeyAfter = sortkey;
-  }
-
-  void setSortkeyBefore(String sortkey) {
-    sortkeyBefore = sortkey;
-  }
-
-  public void setIncludePatchSets(boolean on) {
-    includePatchSets = on;
-  }
-
-  public boolean getIncludePatchSets() {
-    return includePatchSets;
-  }
-
-  public void setIncludeCurrentPatchSet(boolean on) {
-    includeCurrentPatchSet = on;
-  }
-
-  public boolean getIncludeCurrentPatchSet() {
-    return includeCurrentPatchSet;
-  }
-
-  public void setIncludeApprovals(boolean on) {
-    includeApprovals = on;
-  }
-
-  public void setIncludeComments(boolean on) {
-    includeComments = on;
-  }
-
-  public void setIncludeFiles(boolean on) {
-    includeFiles = on;
-  }
-
-  public boolean getIncludeFiles() {
-    return includeFiles;
-  }
-
-  public void setIncludeDependencies(boolean on) {
-    includeDependencies = on;
-  }
-
-  public boolean getIncludeDependencies() {
-    return includeDependencies;
-  }
-
-  public void setIncludeCommitMessage(boolean on) {
-    includeCommitMessage = on;
-  }
-
-  public void setIncludeSubmitRecords(boolean on) {
-    includeSubmitRecords = on;
-  }
-
-  public void setIncludeAllReviewers(boolean on) {
-    includeAllReviewers = on;
-  }
-
-  public void setOutput(OutputStream out, OutputFormat fmt) {
-    this.outputStream = out;
-    this.outputFormat = fmt;
+    return this;
   }
 
   /**
-   * Query for changes that match the query string.
+   * Query for changes that match a structured query.
+   *
+   * @see #queryChanges(List)
+   * @param query the query.
+   * @return results of the query.
+   */
+  public QueryResult queryChanges(Predicate<ChangeData> query)
+      throws OrmException, QueryParseException {
+    return queryChanges(ImmutableList.of(query)).get(0);
+  }
+
+  /*
+   * Perform multiple queries over a list of query strings.
    * <p>
    * If a limit was specified using {@link #setLimit(int)} this method may
    * return up to {@code limit + 1} results, allowing the caller to determine if
    * there are more than {@code limit} matches and suggest to its own caller
-   * that the query could be retried with {@link #setSortkeyBefore(String)}.
+   * that the query could be retried with {@link #setStart(int)}.
+   *
+   * @param queries the queries.
+   * @return results of the queries, one list per input query.
    */
-  public List<ChangeData> queryChanges(String queryString)
+  public List<QueryResult> queryChanges(List<Predicate<ChangeData>> queries)
       throws OrmException, QueryParseException {
-    return queryChanges(ImmutableList.of(queryString)).get(0);
+    return queryChanges(null, queries);
   }
 
-  /**
-   * Query for changes that match the query string.
-   * <p>
-   * If a limit was specified using {@link #setLimit(int)} this method may
-   * return up to {@code limit + 1} results, allowing the caller to determine if
-   * there are more than {@code limit} matches and suggest to its own caller
-   * that the query could be retried with {@link #setSortkeyBefore(String)}.
-   */
-  public List<List<ChangeData>> queryChanges(List<String> queries)
+  static {
+    // In addition to this assumption, this queryChanges assumes the basic
+    // rewrites do not touch visibleto predicates either.
+    checkState(
+        !IsVisibleToPredicate.class.isAssignableFrom(IndexPredicate.class),
+        "QueryProcessor assumes visibleto is not used by the index rewriter.");
+  }
+
+  private List<QueryResult> queryChanges(List<String> queryStrings,
+      List<Predicate<ChangeData>> queries)
       throws OrmException, QueryParseException {
-    final Predicate<ChangeData> visibleToMe = queryBuilder.is_visible();
+    Predicate<ChangeData> visibleToMe = enforceVisibility
+        ? new IsVisibleToPredicate(db, changeControlFactory, userProvider.get())
+        : null;
     int cnt = queries.size();
 
     // Parse and rewrite all queries.
-    List<Integer> limits = Lists.newArrayListWithCapacity(cnt);
-    List<ChangeDataSource> sources = Lists.newArrayListWithCapacity(cnt);
-    for (String query : queries) {
-      Predicate<ChangeData> q = parseQuery(query, visibleToMe);
-      Predicate<ChangeData> s = queryRewriter.rewrite(q, start);
+    List<Integer> limits = new ArrayList<>(cnt);
+    List<Predicate<ChangeData>> predicates = new ArrayList<>(cnt);
+    List<ChangeDataSource> sources = new ArrayList<>(cnt);
+    for (Predicate<ChangeData> q : queries) {
+      int limit = getEffectiveLimit(q);
+      limits.add(limit);
+
+      // Always bump limit by 1, even if this results in exceeding the permitted
+      // max for this user. The only way to see if there are more changes is to
+      // ask for one more result from the query.
+      if (limit == getBackendSupportedLimit()) {
+        limit--;
+      }
+      Predicate<ChangeData> s = queryRewriter.rewrite(q, start, limit + 1);
       if (!(s instanceof ChangeDataSource)) {
-        q = Predicate.and(queryBuilder.status_open(), q);
-        s = queryRewriter.rewrite(q, start);
+        q = Predicate.and(open(), q);
+        s = queryRewriter.rewrite(q, start, limit);
       }
       if (!(s instanceof ChangeDataSource)) {
         throw new QueryParseException("invalid query: " + s);
       }
-
-      // Don't trust QueryRewriter to have left the visible predicate.
-      AndSource a = new AndSource(ImmutableList.of(s, visibleToMe), start);
-      limits.add(limit(q));
-      sources.add(a);
+      if (enforceVisibility) {
+        s = new AndSource(ImmutableList.of(s, visibleToMe), start);
+      }
+      predicates.add(s);
+      sources.add((ChangeDataSource) s);
     }
 
     // Run each query asynchronously, if supported.
-    List<ResultSet<ChangeData>> matches = Lists.newArrayListWithCapacity(cnt);
+    List<ResultSet<ChangeData>> matches = new ArrayList<>(cnt);
     for (ChangeDataSource s : sources) {
       matches.add(s.read());
     }
 
-    List<List<ChangeData>> out = Lists.newArrayListWithCapacity(cnt);
+    List<QueryResult> out = new ArrayList<>(cnt);
     for (int i = 0; i < cnt; i++) {
-      List<ChangeData> results = matches.get(i).toList();
-      if (sortkeyAfter != null) {
-        Collections.sort(results, cmpAfter);
-      } else if (sortkeyBefore != null) {
-        Collections.sort(results, cmpBefore);
-      }
-      if (results.size() > maxLimit) {
-        moreResults = true;
-      }
-      int limit = limits.get(i);
-      if (limit < results.size()) {
-        results = results.subList(0, limit);
-      }
-      if (sortkeyAfter != null) {
-        Collections.reverse(results);
-      }
-      out.add(results);
+      out.add(QueryResult.create(
+          queryStrings != null ? queryStrings.get(i) : null,
+          predicates.get(i),
+          limits.get(i),
+          matches.get(i).toList()));
     }
     return out;
   }
 
-  public void query(String queryString) throws IOException {
-    out = new PrintWriter( //
-        new BufferedWriter( //
-            new OutputStreamWriter(outputStream, "UTF-8")));
-    try {
-      if (isDisabled()) {
-        ErrorMessage m = new ErrorMessage();
-        m.message = "query disabled";
-        show(m);
-        return;
-      }
-
-      try {
-        final QueryStatsAttribute stats = new QueryStatsAttribute();
-        stats.runTimeMilliseconds = TimeUtil.nowMs();
-
-        List<ChangeData> results = queryChanges(queryString);
-        ChangeAttribute c = null;
-        for (ChangeData d : results) {
-          ChangeControl cc = d.changeControl().forUser(user);
-
-          LabelTypes labelTypes = cc.getLabelTypes();
-          c = eventFactory.asChangeAttribute(d.change());
-          eventFactory.extend(c, d.change());
-
-          if (!trackingFooters.isEmpty()) {
-            eventFactory.addTrackingIds(c,
-                trackingFooters.extract(d.commitFooters()));
-          }
-
-          if (includeAllReviewers) {
-            eventFactory.addAllReviewers(c, d.notes());
-          }
-
-          if (includeSubmitRecords) {
-            PatchSet.Id psId = d.change().currentPatchSetId();
-            PatchSet patchSet = db.get().patchSets().get(psId);
-            List<SubmitRecord> submitResult = cc.canSubmit( //
-                db.get(), patchSet, null, false, true, true);
-            eventFactory.addSubmitRecords(c, submitResult);
-          }
-
-          if (includeCommitMessage) {
-            eventFactory.addCommitMessage(c, d.commitMessage());
-          }
-
-          if (includePatchSets) {
-            if (includeFiles) {
-              eventFactory.addPatchSets(c, d.patches(),
-                includeApprovals ? d.approvals().asMap() : null,
-                includeFiles, d.change(), labelTypes);
-            } else {
-              eventFactory.addPatchSets(c, d.patches(),
-                  includeApprovals ? d.approvals().asMap() : null,
-                  labelTypes);
-            }
-          }
-
-          if (includeCurrentPatchSet) {
-            PatchSet current = d.currentPatchSet();
-            if (current != null) {
-              c.currentPatchSet = eventFactory.asPatchSetAttribute(current);
-              eventFactory.addApprovals(c.currentPatchSet,
-                  d.currentApprovals(), labelTypes);
-
-              if (includeFiles) {
-                eventFactory.addPatchSetFileNames(c.currentPatchSet,
-                    d.change(), d.currentPatchSet());
-              }
-            }
-          }
-
-          if (includeComments) {
-            eventFactory.addComments(c, d.messages());
-            if (includePatchSets) {
-              for (PatchSetAttribute attribute : c.patchSets) {
-                eventFactory.addPatchSetComments(attribute,  d.comments());
-              }
-            }
-          }
-
-          if (includeDependencies) {
-            eventFactory.addDependencies(c, d.change());
-          }
-
-          show(c);
-        }
-
-        stats.rowCount = results.size();
-        if (moreResults) {
-          stats.resumeSortKey = c.sortKey;
-        }
-        stats.runTimeMilliseconds =
-            TimeUtil.nowMs() - stats.runTimeMilliseconds;
-        show(stats);
-      } catch (OrmException err) {
-        log.error("Cannot execute query: " + queryString, err);
-
-        ErrorMessage m = new ErrorMessage();
-        m.message = "cannot query database";
-        show(m);
-
-      } catch (QueryParseException e) {
-        ErrorMessage m = new ErrorMessage();
-        m.message = e.getMessage();
-        show(m);
-      } catch (NoSuchChangeException e) {
-        log.error("Missing change: " + e.getMessage(), e);
-        ErrorMessage m = new ErrorMessage();
-        m.message = "missing change " + e.getMessage();
-        show(m);
-      }
-    } finally {
-      try {
-        out.flush();
-      } finally {
-        out = null;
-      }
-    }
-  }
-
   boolean isDisabled() {
-    return maxLimit <= 0;
+    return getPermittedLimit() <= 0;
   }
 
-  private int limit(Predicate<ChangeData> s) {
-    int n = Objects.firstNonNull(ChangeQueryBuilder.getLimit(s), maxLimit);
-    return limit > 0 ? Math.min(n, limit) + 1 : n + 1;
-  }
-
-  private Predicate<ChangeData> parseQuery(String queryString,
-      final Predicate<ChangeData> visibleToMe) throws QueryParseException {
-    Predicate<ChangeData> q = queryBuilder.parse(queryString);
-    if (queryBuilder.supportsSortKey() && !ChangeQueryBuilder.hasSortKey(q)) {
-      if (sortkeyBefore != null) {
-        q = Predicate.and(q, queryBuilder.sortkey_before(sortkeyBefore));
-      } else if (sortkeyAfter != null) {
-        q = Predicate.and(q, queryBuilder.sortkey_after(sortkeyAfter));
-      } else {
-        q = Predicate.and(q, queryBuilder.sortkey_before("z"));
-      }
+  private int getPermittedLimit() {
+    if (enforceVisibility) {
+      return userProvider.get().getCapabilities()
+        .getRange(GlobalCapability.QUERY_LIMIT)
+        .getMax();
     }
-    return Predicate.and(q,
-        queryBuilder.limit(limit > 0 ? Math.min(limit, maxLimit) + 1 : maxLimit),
-        visibleToMe);
+    return Integer.MAX_VALUE;
   }
 
-  private void show(Object data) {
-    switch (outputFormat) {
-      default:
-      case TEXT:
-        if (data instanceof ChangeAttribute) {
-          out.print("change ");
-          out.print(((ChangeAttribute) data).id);
-          out.print("\n");
-          showText(data, 1);
-        } else {
-          showText(data, 0);
-        }
-        out.print('\n');
-        break;
+  private int getBackendSupportedLimit() {
+    return indexConfig.maxLimit();
+  }
 
-      case JSON:
-        out.print(gson.toJson(data));
-        out.print('\n');
-        break;
+  private int getEffectiveLimit(Predicate<ChangeData> p) {
+    List<Integer> possibleLimits = new ArrayList<>(4);
+    possibleLimits.add(getBackendSupportedLimit());
+    possibleLimits.add(getPermittedLimit());
+    if (limitFromCaller > 0) {
+      possibleLimits.add(limitFromCaller);
     }
-  }
-
-  private void showText(Object data, int depth) {
-    for (Field f : fieldsOf(data.getClass())) {
-      Object val;
-      try {
-        val = f.get(data);
-      } catch (IllegalArgumentException err) {
-        continue;
-      } catch (IllegalAccessException err) {
-        continue;
-      }
-      if (val == null) {
-        continue;
-      }
-
-      showField(f.getName(), val, depth);
+    Integer limitFromPredicate = LimitPredicate.getLimit(p);
+    if (limitFromPredicate != null) {
+      possibleLimits.add(limitFromPredicate);
     }
-  }
-
-  private String indent(int spaces) {
-    if (spaces == 0) {
-      return "";
-    } else {
-      return String.format("%" + spaces + "s", " ");
-    }
-  }
-
-  private void showField(String field, Object value, int depth) {
-    final int spacesDepthRatio = 2;
-    String indent = indent(depth * spacesDepthRatio);
-    out.print(indent);
-    out.print(field);
-    out.print(':');
-    if (value instanceof String && ((String) value).contains("\n")) {
-      out.print(' ');
-      // Idention for multi-line text is
-      // current depth indetion + length of field + length of ": "
-      indent = indent(indent.length() + field.length() + spacesDepthRatio);
-      out.print(((String) value).replaceAll("\n", "\n" + indent).trim());
-      out.print('\n');
-    } else if (value instanceof Long && isDateField(field)) {
-      out.print(' ');
-      out.print(sdf.format(new Date(((Long) value) * 1000L)));
-      out.print('\n');
-    } else if (isPrimitive(value)) {
-      out.print(' ');
-      out.print(value);
-      out.print('\n');
-    } else if (value instanceof Collection) {
-      out.print('\n');
-      boolean firstElement = true;
-      for (Object thing : ((Collection<?>) value)) {
-        // The name of the collection was initially printed at the beginning
-        // of this routine.  Beginning at the second sub-element, reprint
-        // the collection name so humans can separate individual elements
-        // with less strain and error.
-        //
-        if (firstElement) {
-          firstElement = false;
-        } else {
-          out.print(indent);
-          out.print(field);
-          out.print(":\n");
-        }
-        if (isPrimitive(thing)) {
-          out.print(' ');
-          out.print(value);
-          out.print('\n');
-        } else {
-          showText(thing, depth + 1);
-        }
-      }
-    } else {
-      out.print('\n');
-      showText(value, depth + 1);
-    }
-  }
-
-  private static boolean isPrimitive(Object value) {
-    return value instanceof String //
-        || value instanceof Number //
-        || value instanceof Boolean //
-        || value instanceof Enum;
-  }
-
-  private static boolean isDateField(String name) {
-    return "lastUpdated".equals(name) //
-        || "grantedOn".equals(name) //
-        || "timestamp".equals(name) //
-        || "createdOn".equals(name);
-  }
-
-  private List<Field> fieldsOf(Class<?> type) {
-    List<Field> r = new ArrayList<>();
-    if (type.getSuperclass() != null) {
-      r.addAll(fieldsOf(type.getSuperclass()));
-    }
-    r.addAll(Arrays.asList(type.getDeclaredFields()));
-    return r;
-  }
-
-  static class ErrorMessage {
-    public final String type = "error";
-    public String message;
+    return Ordering.natural().min(possibleLimits);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryResult.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryResult.java
new file mode 100644
index 0000000..a93f7ac
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryResult.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.query.Predicate;
+
+import java.util.List;
+
+/** Results of a query over changes. */
+@AutoValue
+public abstract class QueryResult {
+  static QueryResult create(@Nullable String query,
+      Predicate<ChangeData> predicate, int limit, List<ChangeData> changes) {
+    boolean moreChanges;
+    if (changes.size() > limit) {
+      moreChanges = true;
+      changes = changes.subList(0, limit);
+    } else {
+      moreChanges = false;
+    }
+    return new AutoValue_QueryResult(query, predicate, changes, moreChanges);
+  }
+
+  /**
+   * @return the original query string, or null if the query was created
+   *     programmatically.
+   */
+  @Nullable public abstract String query();
+
+  /**
+   * @return the predicate after all rewriting and other modification by the
+   *     query subsystem.
+   */
+  public abstract Predicate<ChangeData> predicate();
+
+  /** @return the query results. */
+  public abstract List<ChangeData> changes();
+
+  /**
+   * @return whether the query could be retried with
+   *     {@link QueryProcessor#setStart(int)} to produce more results. Never
+   *     true if {@link #changes()} is empty.
+   */
+  public abstract boolean moreChanges();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexPathPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
index d073002..049aa40 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
@@ -16,76 +16,21 @@
 
 import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.RegexPredicate;
+import com.google.gerrit.server.util.RegexListSearcher;
 import com.google.gwtorm.server.OrmException;
 
-import dk.brics.automaton.Automaton;
-import dk.brics.automaton.RegExp;
-import dk.brics.automaton.RunAutomaton;
-
-import java.util.Collections;
 import java.util.List;
 
 class RegexPathPredicate extends RegexPredicate<ChangeData> {
-  private final RunAutomaton pattern;
-
-  private final String prefixBegin;
-  private final String prefixEnd;
-  private final int prefixLen;
-  private final boolean prefixOnly;
-
-  RegexPathPredicate(String fieldName, String re) {
+  RegexPathPredicate(String re) {
     super(ChangeField.PATH, re);
-
-    if (re.startsWith("^")) {
-      re = re.substring(1);
-    }
-
-    if (re.endsWith("$") && !re.endsWith("\\$")) {
-      re = re.substring(0, re.length() - 1);
-    }
-
-    Automaton automaton = new RegExp(re).toAutomaton();
-    prefixBegin = automaton.getCommonPrefix();
-    prefixLen = prefixBegin.length();
-
-    if (0 < prefixLen) {
-      char max = (char) (prefixBegin.charAt(prefixLen - 1) + 1);
-      prefixEnd = prefixBegin.substring(0, prefixLen - 1) + max;
-      prefixOnly = re.equals(prefixBegin + ".*");
-    } else {
-      prefixEnd = "";
-      prefixOnly = false;
-    }
-
-    pattern = prefixOnly ? null : new RunAutomaton(automaton);
   }
 
   @Override
   public boolean match(ChangeData object) throws OrmException {
     List<String> files = object.currentFilePaths();
     if (files != null) {
-      int begin, end;
-
-      if (0 < prefixLen) {
-        begin = find(files, prefixBegin);
-        end = find(files, prefixEnd);
-      } else {
-        begin = 0;
-        end = files.size();
-      }
-
-      if (prefixOnly) {
-        return begin < end;
-      }
-
-      while (begin < end) {
-        if (pattern.run(files.get(begin++))) {
-          return true;
-        }
-      }
-
-      return false;
-
+      return RegexListSearcher.ofStrings(getValue()).hasMatch(files);
     } else {
       // The ChangeData can't do expensive lookups right now. Bypass
       // them and include the result anyway. We might be able to do
@@ -95,11 +40,6 @@
     }
   }
 
-  private static int find(List<String> files, String p) {
-    int r = Collections.binarySearch(files, p);
-    return r < 0 ? -(r + 1) : r;
-  }
-
   @Override
   public int getCost() {
     return 1;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
index 7d5f1dc..3a9604f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.RegexPredicate;
-import com.google.gerrit.server.index.Schema;
 import com.google.gwtorm.server.OrmException;
 
 import dk.brics.automaton.RegExp;
@@ -25,8 +25,8 @@
 class RegexTopicPredicate extends RegexPredicate<ChangeData> {
   private final RunAutomaton pattern;
 
-  RegexTopicPredicate(Schema<ChangeData> schema, String re) {
-    super(TopicPredicate.topicField(schema), re);
+  RegexTopicPredicate(String re) {
+    super(ChangeField.TOPIC, re);
 
     if (re.startsWith("^")) {
       re = re.substring(1);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.java
index fd57a44..0947fae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RevWalkPredicate.java
@@ -102,18 +102,9 @@
 
     Arguments args = new Arguments(patchSet, revision, objectId, change, projectName);
 
-    try {
-      final Repository repo = repoManager.openRepository(projectName);
-      try {
-        final RevWalk rw = new RevWalk(repo);
-        try {
-          return match(repo, rw, args);
-        } finally {
-          rw.release();
-        }
-      } finally {
-        repo.close();
-      }
+    try (Repository repo = repoManager.openRepository(projectName);
+        RevWalk rw = new RevWalk(repo)) {
+      return match(repo, rw, args);
     } catch (RepositoryNotFoundException e) {
       log.error("Repository \"" + projectName.get() + "\" unknown.", e);
     } catch (IOException e) {
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
deleted file mode 100644
index 6fa11fd..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java
+++ /dev/null
@@ -1,139 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.gerrit.server.index.ChangeField.SORTKEY;
-
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.index.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> {
-  public static boolean hasSortKeyField(Schema<ChangeData> schema) {
-    return sortkeyFieldOrNull(schema) != null;
-  }
-
-  @SuppressWarnings("deprecation")
-  private static long parseSortKey(Schema<ChangeData> schema, String value) {
-    FieldDef<ChangeData, ?> field = schema.getFields().get(SORTKEY.getName());
-    if (field == SORTKEY) {
-      return ChangeUtil.parseSortKey(value);
-    } else {
-      return ChangeField.legacyParseSortKey(value);
-    }
-  }
-
-  @SuppressWarnings("deprecation")
-  private static FieldDef<ChangeData, ?> sortkeyFieldOrNull(
-      Schema<ChangeData> schema) {
-    if (schema == null) {
-      return ChangeField.LEGACY_SORTKEY;
-    }
-    FieldDef<ChangeData, ?> f = schema.getFields().get(SORTKEY.getName());
-    if (f != null) {
-      return f;
-    }
-    return schema.getFields().get(ChangeField.LEGACY_SORTKEY.getName());
-  }
-
-  private static FieldDef<ChangeData, ?> sortkeyField(Schema<ChangeData> schema) {
-    return checkNotNull(
-        sortkeyFieldOrNull(schema),
-        "schema missing sortkey field, found: %s", schema);
-  }
-
-  protected final Schema<ChangeData> schema;
-  protected final Provider<ReviewDb> dbProvider;
-
-  SortKeyPredicate(Schema<ChangeData> schema, Provider<ReviewDb> dbProvider,
-      String name, String value) {
-    super(sortkeyField(schema), name, value);
-    this.schema = schema;
-    this.dbProvider = dbProvider;
-  }
-
-  @Override
-  public int getCost() {
-    return 1;
-  }
-
-  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(@Nullable Schema<ChangeData> schema, Provider<ReviewDb> dbProvider,
-        String value) {
-      super(schema, dbProvider, "sortkey_before", value);
-    }
-
-    @Override
-    public long getMinValue(Schema<ChangeData> schema) {
-      return 0;
-    }
-
-    @Override
-    public long getMaxValue(Schema<ChangeData> schema) {
-      return parseSortKey(schema, getValue());
-    }
-
-    @Override
-    public boolean match(ChangeData cd) throws OrmException {
-      Change change = cd.change();
-      return change != null && change.getSortKey().compareTo(getValue()) < 0;
-    }
-
-    @Override
-    public Before copy(String newValue) {
-      return new Before(schema, dbProvider, newValue);
-    }
-  }
-
-  public static class After extends SortKeyPredicate {
-    After(@Nullable Schema<ChangeData> schema, Provider<ReviewDb> dbProvider,
-        String value) {
-      super(schema, dbProvider, "sortkey_after", value);
-    }
-
-    @Override
-    public long getMinValue(Schema<ChangeData> schema) {
-      return parseSortKey(schema, getValue());
-    }
-
-    @Override
-    public long getMaxValue(Schema<ChangeData> schema) {
-      return Long.MAX_VALUE;
-    }
-
-    @Override
-    public boolean match(ChangeData cd) throws OrmException {
-      Change change = cd.change();
-      return change != null && change.getSortKey().compareTo(getValue()) > 0;
-    }
-
-    @Override
-    public After copy(String newValue) {
-      return new After(schema, dbProvider, newValue);
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
index 7196c9f..07a6714 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
@@ -18,26 +18,12 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 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;
 
 class TopicPredicate extends IndexPredicate<ChangeData> {
-  @SuppressWarnings("deprecation")
-  static FieldDef<ChangeData, ?> topicField(Schema<ChangeData> schema) {
-    if (schema == null) {
-      return ChangeField.LEGACY_TOPIC;
-    }
-    FieldDef<ChangeData, ?> f = schema.getFields().get(TOPIC.getName());
-    if (f != null) {
-      return f;
-    }
-    return schema.getFields().get(ChangeField.LEGACY_TOPIC.getName());
-  }
-
-  TopicPredicate(Schema<ChangeData> schema, String topic) {
-    super(topicField(schema), topic);
+  TopicPredicate(String topic) {
+    super(ChangeField.TOPIC, topic);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
index 9e60a4225..4f2a8d7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
@@ -18,13 +18,14 @@
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.IndexPredicate;
-import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 
+import org.eclipse.jgit.revwalk.FooterLine;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.List;
 
 class TrackingIdPredicate extends IndexPredicate<ChangeData> {
   private static final Logger log = LoggerFactory.getLogger(TrackingIdPredicate.class);
@@ -41,9 +42,10 @@
     Change c = object.change();
     if (c != null) {
       try {
-        return trackingFooters.extract(object.commitFooters())
-            .values().contains(getValue());
-      } catch (NoSuchChangeException | IOException e) {
+        List<FooterLine> footers = object.commitFooters();
+        return footers != null && trackingFooters.extract(
+            object.commitFooters()).values().contains(getValue());
+      } catch (IOException e) {
         log.warn("Cannot extract footers from " + c.getChangeId(), e);
       }
     }
@@ -52,8 +54,6 @@
 
   @Override
   public int getCost() {
-    return ChangeCosts.cost(
-        ChangeCosts.TR_SCAN,
-        ChangeCosts.CARD_TRACKING_IDS);
+    return 1;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index df49a01..1eefbf9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -18,6 +18,8 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.PROJECT_OWNERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.common.data.AccessSection;
@@ -28,7 +30,7 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
-import com.google.gerrit.extensions.common.InheritableBoolean;
+import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -54,6 +56,7 @@
   private final GitRepositoryManager mgr;
   private final AllProjectsName allProjectsName;
   private final PersonIdent serverUser;
+  private String message;
 
   private GroupReference admin;
   private GroupReference batch;
@@ -85,6 +88,11 @@
     return this;
   }
 
+  public AllProjectsCreator setCommitMessage(String message) {
+    this.message = message;
+    return this;
+  }
+
   public void create() throws IOException, ConfigInvalidException {
     Repository git = null;
     try {
@@ -118,7 +126,9 @@
         git);
     md.getCommitBuilder().setAuthor(serverUser);
     md.getCommitBuilder().setCommitter(serverUser);
-    md.setMessage("Initialized Gerrit Code Review " + Version.getVersion());
+    md.setMessage(MoreObjects.firstNonNull(
+        Strings.emptyToNull(message),
+        "Initialized Gerrit Code Review " + Version.getVersion()));
 
     ProjectConfig config = ProjectConfig.read(md);
     Project p = config.getProject();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
index 49ac452..30ebaa7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.config.ConfigSection;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
 import com.google.gwtorm.jdbc.SimpleDataSource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -46,18 +45,15 @@
     LifecycleListener {
   public static final int DEFAULT_POOL_LIMIT = 8;
 
-  private final SitePaths site;
   private final Config cfg;
   private final Context ctx;
   private final DataSourceType dst;
   private DataSource ds;
 
   @Inject
-  protected DataSourceProvider(SitePaths site,
-      @GerritServerConfig Config cfg,
+  protected DataSourceProvider(@GerritServerConfig Config cfg,
       Context ctx,
       DataSourceType dst) {
-    this.site = site;
     this.cfg = cfg;
     this.ctx = ctx;
     this.dst = dst;
@@ -66,7 +62,7 @@
   @Override
   public synchronized DataSource get() {
     if (ds == null) {
-      ds = open(site, cfg, ctx, dst);
+      ds = open(cfg, ctx, dst);
     }
     return ds;
   }
@@ -90,8 +86,8 @@
     SINGLE_USER, MULTI_USER
   }
 
-  private DataSource open(final SitePaths site, final Config cfg,
-      final Context context, final DataSourceType dst) {
+  private DataSource open(final Config cfg, final Context context,
+      final DataSourceType dst) {
     ConfigSection dbs = new ConfigSection(cfg, "database");
     String driver = dbs.optional("driver");
     if (Strings.isNullOrEmpty(driver)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
index 298c0d8..daf1d4d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -46,23 +46,19 @@
   private final PersonIdent serverUser;
   private final DataSourceType dataSourceType;
 
-  private final int versionNbr;
-
   private AccountGroup admin;
   private AccountGroup batch;
 
   @Inject
   public SchemaCreator(SitePaths site,
-      @Current SchemaVersion version,
       AllProjectsCreator ap,
       AllUsersCreator auc,
       @GerritPersonIdent PersonIdent au,
       DataSourceType dst) {
-    this(site.site_path, version, ap, auc, au, dst);
+    this(site.site_path, ap, auc, au, dst);
   }
 
   public SchemaCreator(@SitePath File site,
-      @Current SchemaVersion version,
       AllProjectsCreator ap,
       AllUsersCreator auc,
       @GerritPersonIdent PersonIdent au,
@@ -72,21 +68,17 @@
     allUsersCreator = auc;
     serverUser = au;
     dataSourceType = dst;
-    versionNbr = version.getVersionNbr();
   }
 
   public void create(final ReviewDb db) throws OrmException, IOException,
       ConfigInvalidException {
     final JdbcSchema jdbc = (JdbcSchema) db;
-    final JdbcExecutor e = new JdbcExecutor(jdbc);
-    try {
+    try (JdbcExecutor e = new JdbcExecutor(jdbc)) {
       jdbc.updateSchema(e);
-    } finally {
-      e.close();
     }
 
     final CurrentSchemaVersion sVer = CurrentSchemaVersion.create();
-    sVer.versionNbr = versionNbr;
+    sVer.versionNbr = SchemaVersion.getBinaryVersion();
     db.schemaVersion().insert(Collections.singleton(sVer));
 
     initSystemConfig(db);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.java
index aaf4607..6faf148 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.java
@@ -32,8 +32,6 @@
 public class SchemaModule extends FactoryModule {
   @Override
   protected void configure() {
-    install(new SchemaVersion.Module());
-
     bind(PersonIdent.class).annotatedWith(GerritPersonIdent.class)
       .toProvider(GerritPersonIdentProvider.class);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
index 0cea4bc..2b9d4b4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
@@ -17,13 +17,23 @@
 import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
 import com.google.gerrit.reviewdb.client.SystemConfig;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
 import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
 import com.google.inject.Provider;
+import com.google.inject.Stage;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.PersonIdent;
 
 import java.io.IOException;
 import java.sql.SQLException;
@@ -37,12 +47,46 @@
   private final Provider<SchemaVersion> updater;
 
   @Inject
-  SchemaUpdater(final SchemaFactory<ReviewDb> schema, final SitePaths site,
-      final SchemaCreator creator, @Current final Provider<SchemaVersion> update) {
+  SchemaUpdater(SchemaFactory<ReviewDb> schema,
+      SitePaths site,
+      SchemaCreator creator,
+      Injector parent) {
     this.schema = schema;
     this.site = site;
     this.creator = creator;
-    this.updater = update;
+    this.updater = buildInjector(parent).getProvider(SchemaVersion.class);
+  }
+
+  private static Injector buildInjector(final Injector parent) {
+    // Use DEVELOPMENT mode to allow lazy initialization of the
+    // graph. This avoids touching ancient schema versions that
+    // are behind this installation's current version.
+    return Guice.createInjector(Stage.DEVELOPMENT, new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(SchemaVersion.class).to(SchemaVersion.C);
+
+        for (Key<?> k : new Key<?>[]{
+            Key.get(PersonIdent.class, GerritPersonIdent.class),
+            Key.get(String.class, AnonymousCowardName.class),
+            }) {
+          rebind(parent, k);
+        }
+
+        for (Class<?> c : new Class<?>[] {
+            AllProjectsName.class,
+            AllUsersCreator.class,
+            GitRepositoryManager.class,
+            SitePaths.class,
+            }) {
+          rebind(parent, Key.get(c));
+        }
+      }
+
+      private <T> void rebind(Injector parent, Key<T> c) {
+        bind(c).toProvider(parent.getProvider(c));
+      }
+    });
   }
 
   public void update(final UpdateUI ui) throws OrmException {
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 11479cc..945baa8 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
@@ -21,9 +21,9 @@
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.StatementExecutor;
-import com.google.inject.AbstractModule;
 import com.google.inject.Provider;
 
+import java.sql.PreparedStatement;
 import java.sql.SQLException;
 import java.sql.Statement;
 import java.util.Collections;
@@ -32,13 +32,10 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_98> C = Schema_98.class;
+  public static final Class<Schema_107> C = Schema_107.class;
 
-  public static class Module extends AbstractModule {
-    @Override
-    protected void configure() {
-      bind(SchemaVersion.class).annotatedWith(Current.class).to(C);
-    }
+  public static int getBinaryVersion() {
+    return guessVersion(C);
   }
 
   private final Provider<? extends SchemaVersion> prior;
@@ -57,12 +54,6 @@
     return Integer.parseInt(n);
   }
 
-  protected SchemaVersion(final Provider<? extends SchemaVersion> prior,
-      final int versionNbr) {
-    this.prior = prior;
-    this.versionNbr = versionNbr;
-  }
-
   /** @return the {@link CurrentSchemaVersion#versionNbr} this step targets. */
   public final int getVersionNbr() {
     return versionNbr;
@@ -88,20 +79,23 @@
     migrateData(pending, ui, curr, db);
 
     JdbcSchema s = (JdbcSchema) db;
-    JdbcExecutor e = new JdbcExecutor(s);
-    try {
-      final List<String> pruneList = Lists.newArrayList();
-      s.pruneSchema(new StatementExecutor() {
-        public void execute(String sql) {
-          pruneList.add(sql);
-        }
-      });
+    final List<String> pruneList = Lists.newArrayList();
+    s.pruneSchema(new StatementExecutor() {
+      @Override
+      public void execute(String sql) {
+        pruneList.add(sql);
+      }
 
+      @Override
+      public void close() {
+        // Do nothing.
+      }
+    });
+
+    try (JdbcExecutor e = new JdbcExecutor(s)) {
       if (!pruneList.isEmpty()) {
         ui.pruneSchema(e, pruneList);
       }
-    } finally {
-      e.close();
     }
   }
 
@@ -122,15 +116,18 @@
     }
 
     JdbcSchema s = (JdbcSchema) db;
-    JdbcExecutor e = new JdbcExecutor(s);
-    try {
+    try (JdbcExecutor e = new JdbcExecutor(s)) {
       s.updateSchema(e);
-    } finally {
-      e.close();
     }
   }
 
-  /** Invoke before updateSchema adds new columns/tables. */
+  /**
+   * Invoked before updateSchema adds new columns/tables.
+   *
+   * @param db open database handle.
+   * @throws OrmException if a Gerrit-specific exception occurred.
+   * @throws SQLException if an underlying SQL exception occurred.
+   */
   protected void preUpdateSchema(ReviewDb db) throws OrmException, SQLException {
   }
 
@@ -148,6 +145,11 @@
   /**
    * Invoked between updateSchema (adds new columns/tables) and pruneSchema
    * (removes deleted columns/tables).
+   *
+   * @param db open database handle.
+   * @param ui interface for interacting with the user.
+   * @throws OrmException if a Gerrit-specific exception occurred.
+   * @throws SQLException if an underlying SQL exception occurred.
    */
   protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
   }
@@ -160,36 +162,43 @@
   }
 
   /** Rename an existing table. */
-  protected void renameTable(ReviewDb db, String from, String to)
+  protected static void renameTable(ReviewDb db, String from, String to)
       throws OrmException {
-    final JdbcSchema s = (JdbcSchema) db;
-    final JdbcExecutor e = new JdbcExecutor(s);
-    try {
+    JdbcSchema s = (JdbcSchema) db;
+    try (JdbcExecutor e = new JdbcExecutor(s)) {
       s.renameTable(e, from, to);
-    } finally {
-      e.close();
     }
   }
 
   /** Rename an existing column. */
-  protected void renameColumn(ReviewDb db, String table, String from, String to)
+  protected static void renameColumn(ReviewDb db, String table, String from, String to)
       throws OrmException {
-    final JdbcSchema s = (JdbcSchema) db;
-    final JdbcExecutor e = new JdbcExecutor(s);
-    try {
+    JdbcSchema s = (JdbcSchema) db;
+    try (JdbcExecutor e = new JdbcExecutor(s)) {
       s.renameField(e, table, from, to);
-    } finally {
-      e.close();
     }
   }
 
   /** Execute an SQL statement. */
-  protected void execute(ReviewDb db, String sql) throws SQLException {
-    Statement s = ((JdbcSchema) db).getConnection().createStatement();
-    try {
+  protected static void execute(ReviewDb db, String sql) throws SQLException {
+    try (Statement s = newStatement(db)) {
       s.execute(sql);
-    } finally {
-      s.close();
     }
   }
+
+  /** Open a new single statement. */
+  protected static Statement newStatement(ReviewDb db) throws SQLException {
+    return ((JdbcSchema) db).getConnection().createStatement();
+  }
+
+  /** Open a new prepared statement. */
+  protected static PreparedStatement prepareStatement(ReviewDb db, String sql)
+      throws SQLException {
+    return ((JdbcSchema) db).getConnection().prepareStatement(sql);
+  }
+
+  /** Open a new statement executor. */
+  protected static JdbcExecutor newExecutor(ReviewDb db) throws OrmException {
+    return new JdbcExecutor(((JdbcSchema) db).getConnection());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
index 2e12f5c..591601d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
@@ -23,7 +23,6 @@
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
-import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 
 /** Validates the current schema version. */
@@ -40,24 +39,20 @@
   private final SchemaFactory<ReviewDb> schema;
   private final SitePaths site;
 
-  @Current
-  private final Provider<SchemaVersion> version;
-
   @Inject
   public SchemaVersionCheck(SchemaFactory<ReviewDb> schemaFactory,
-      final SitePaths site,
-      @Current Provider<SchemaVersion> version) {
+      SitePaths site) {
     this.schema = schemaFactory;
     this.site = site;
-    this.version = version;
   }
 
+  @Override
   public void start() {
     try {
       final ReviewDb db = schema.open();
       try {
         final CurrentSchemaVersion currentVer = getSchemaVersion(db);
-        final int expectedVer = version.get().getVersionNbr();
+        final int expectedVer = SchemaVersion.getBinaryVersion();
 
         if (currentVer == null) {
           throw new ProvisionException("Schema not yet initialized."
@@ -84,6 +79,7 @@
     }
   }
 
+  @Override
   public void stop() {
   }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_100.java
similarity index 66%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_100.java
index 407b7c7..0902194 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_100.java
@@ -12,10 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.server.schema;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_100 extends SchemaVersion {
+  @Inject
+  Schema_100(Provider<Schema_99> prior) {
+    super(prior);
+  }
+
+  // No database migration; merges are rechecked on reindex.
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_101.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_101.java
new file mode 100644
index 0000000..4ef0d96
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_101.java
@@ -0,0 +1,150 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.common.base.Joiner;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcExecutor;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.schema.ColumnModel;
+import com.google.gwtorm.schema.RelationModel;
+import com.google.gwtorm.schema.java.JavaSchemaModel;
+import com.google.gwtorm.schema.sql.DialectPostgreSQL;
+import com.google.gwtorm.schema.sql.SqlDialect;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.StatementExecutor;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TreeMap;
+
+public class Schema_101 extends SchemaVersion {
+
+  private static class PrimaryKey {
+    String oldNameInDb;
+    List<String> cols;
+  }
+
+  private Connection conn;
+  private SqlDialect dialect;
+
+  @Inject
+  Schema_101(Provider<Schema_100> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui)
+      throws OrmException, SQLException {
+    conn = ((JdbcSchema) db).getConnection();
+    dialect = ((JdbcSchema) db).getDialect();
+    Map<String, PrimaryKey> corrections = findPKUpdates();
+    if (corrections.isEmpty()) {
+      return;
+    }
+
+    ui.message("Wrong Primary Key Column Order Detected");
+    ui.message("The following tables are affected:");
+    ui.message(Joiner.on(", ").join(corrections.keySet()));
+    ui.message("fixing primary keys...");
+    try (JdbcExecutor executor = new JdbcExecutor(conn)) {
+      for (Map.Entry<String, PrimaryKey> c : corrections.entrySet()) {
+        ui.message(String.format("  table: %s ... ", c.getKey()));
+        recreatePK(executor, c.getKey(), c.getValue(), ui);
+        ui.message("done");
+      }
+      ui.message("done");
+    }
+  }
+
+  private Map<String, PrimaryKey> findPKUpdates()
+      throws OrmException, SQLException {
+    Map<String, PrimaryKey> corrections = new TreeMap<>();
+    DatabaseMetaData meta = conn.getMetaData();
+    JavaSchemaModel jsm = new JavaSchemaModel(ReviewDb.class);
+    for (RelationModel rm : jsm.getRelations()) {
+      String tableName = rm.getRelationName();
+      List<String> expectedPKCols = relationPK(rm);
+      PrimaryKey actualPK = dbTablePK(meta, tableName);
+      if (!expectedPKCols.equals(actualPK.cols)) {
+        actualPK.cols = expectedPKCols;
+        corrections.put(tableName, actualPK);
+      }
+    }
+    return corrections;
+  }
+
+  private List<String> relationPK(RelationModel rm) {
+    Collection<ColumnModel> cols = rm.getPrimaryKeyColumns();
+    List<String> pk = new ArrayList<>(cols.size());
+    for (ColumnModel cm : cols) {
+      pk.add(cm.getColumnName().toLowerCase(Locale.US));
+    }
+    return pk;
+  }
+
+  private PrimaryKey dbTablePK(DatabaseMetaData meta, String tableName)
+      throws SQLException {
+    if (meta.storesUpperCaseIdentifiers()) {
+      tableName = tableName.toUpperCase();
+    } else if (meta.storesLowerCaseIdentifiers()) {
+      tableName = tableName.toLowerCase();
+    }
+
+    try (ResultSet cols = meta.getPrimaryKeys(null, null, tableName)) {
+      PrimaryKey pk = new PrimaryKey();
+      Map<Short, String> seqToName = new TreeMap<>();
+      while (cols.next()) {
+        seqToName.put(cols.getShort("KEY_SEQ"), cols.getString("COLUMN_NAME"));
+        if (pk.oldNameInDb == null) {
+          pk.oldNameInDb = cols.getString("PK_NAME");
+        }
+      }
+
+      pk.cols = new ArrayList<>(seqToName.size());
+      for (String name : seqToName.values()) {
+        pk.cols.add(name.toLowerCase(Locale.US));
+      }
+      return pk;
+    }
+  }
+
+  private void recreatePK(StatementExecutor executor, String tableName,
+      PrimaryKey pk, UpdateUI ui) throws OrmException {
+    if (pk.oldNameInDb == null) {
+      ui.message(String.format(
+          "warning: primary key for table %s didn't exist ... ", tableName));
+    } else {
+      if (dialect instanceof DialectPostgreSQL) {
+        // postgresql doesn't support the ALTER TABLE foo DROP PRIMARY KEY form
+        executor.execute("ALTER TABLE " + tableName + " DROP CONSTRAINT "
+            + pk.oldNameInDb);
+      } else {
+        executor.execute("ALTER TABLE " + tableName + " DROP PRIMARY KEY");
+      }
+    }
+    executor.execute("ALTER TABLE " + tableName
+        + " ADD PRIMARY KEY(" + Joiner.on(",").join(pk.cols) + ")");
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_102.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_102.java
new file mode 100644
index 0000000..bcb3e1a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_102.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.schema.sql.DialectPostgreSQL;
+import com.google.gwtorm.schema.sql.SqlDialect;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.StatementExecutor;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+public class Schema_102 extends SchemaVersion {
+  @Inject
+  Schema_102(Provider<Schema_101> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui)
+      throws OrmException, SQLException {
+    JdbcSchema schema = (JdbcSchema) db;
+    SqlDialect dialect = schema.getDialect();
+    try (StatementExecutor e = newExecutor(db)) {
+      // Drop left over indexes that were missed to be removed in schema 84.
+      // See "Delete SQL index support" commit for more details:
+      // d4ae3a16d5e1464574bd04f429a63eb9c02b3b43
+      Pattern pattern =
+          Pattern.compile("^changes_(allOpen|allClosed|byBranchClosed)$",
+              Pattern.CASE_INSENSITIVE);
+      String table = "changes";
+      Set<String> listIndexes = dialect.listIndexes(
+          schema.getConnection(), table);
+      for (String index : listIndexes) {
+        if (pattern.matcher(index).matches()) {
+          dialect.dropIndex(e, table, index);
+        }
+      }
+
+      dialect.dropIndex(e, table, "changes_byProjectOpen");
+      if (dialect instanceof DialectPostgreSQL) {
+        e.execute("CREATE INDEX changes_byProjectOpen"
+            + " ON " + table + " (dest_project_name, last_updated_on)"
+            + " WHERE open = 'Y'");
+      } else {
+        e.execute("CREATE INDEX changes_byProjectOpen"
+            + " ON " + table + " (open, dest_project_name, last_updated_on)");
+      }
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SubmitType.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_103.java
similarity index 69%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SubmitType.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_103.java
index 95a9693..60a5213 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SubmitType.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_103.java
@@ -12,12 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.common;
+package com.google.gerrit.server.schema;
 
-public enum SubmitType {
-  FAST_FORWARD_ONLY,
-  MERGE_IF_NECESSARY,
-  REBASE_IF_NECESSARY,
-  MERGE_ALWAYS,
-  CHERRY_PICK
-}
\ No newline at end of file
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_103 extends SchemaVersion {
+  @Inject
+  Schema_103(Provider<Schema_102> prior) {
+    super(prior);
+  }
+
+  // Adds originalSubject column
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Current.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_104.java
similarity index 64%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Current.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_104.java
index b16c977..bebdaca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Current.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_104.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// Copyright (C) 2015 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,14 +14,14 @@
 
 package com.google.gerrit.server.schema;
 
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
 
-import com.google.inject.BindingAnnotation;
+public class Schema_104 extends SchemaVersion {
+  @Inject
+  Schema_104(Provider<Schema_103> prior) {
+    super(prior);
+  }
 
-import java.lang.annotation.Retention;
-
-/** Indicates the {@link SchemaVersion} is the current one. */
-@Retention(RUNTIME)
-@BindingAnnotation
-public @interface Current {
+  // Remove old change screen
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_105.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_105.java
new file mode 100644
index 0000000..74f0cf5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_105.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.schema.sql.SqlDialect;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.StatementExecutor;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class Schema_105 extends SchemaVersion {
+  private static final String TABLE = "changes";
+
+  @Inject
+  Schema_105(Provider<Schema_104> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui)
+      throws SQLException, OrmException {
+    JdbcSchema schema = (JdbcSchema) db;
+    SqlDialect dialect = schema.getDialect();
+
+    Map<String, OrmException> errors = new HashMap<>();
+    try (StatementExecutor e = newExecutor(db)) {
+      for (String index : listChangesIndexes(schema)) {
+        ui.message("Dropping index " + index + " on table " + TABLE);
+        try {
+          dialect.dropIndex(e, TABLE, index);
+        } catch (OrmException err) {
+          errors.put(index, err);
+        }
+      }
+    }
+
+    for (String index : listChangesIndexes(schema)) {
+      String msg = "Failed to drop index " + index;
+      OrmException err = errors.get(index);
+      if (err != null) {
+        msg += ": " + err.getMessage();
+      }
+      ui.message(msg);
+    }
+  }
+
+  private Set<String> listChangesIndexes(JdbcSchema schema)
+      throws SQLException {
+    // List of all changes indexes ever created or dropped, found with the
+    // following command:
+    //   find g* -name \*.sql | xargs git log -i -p -S' index changes_' | grep -io ' index changes_\w*' | cut -d' ' -f3 | tr A-Z a-z | sort -u
+    // Used rather than listIndexes as we're not sure whether it might include
+    // primary key indexes.
+    Set<String> allChanges = ImmutableSet.of(
+        "changes_allclosed",
+        "changes_allopen",
+        "changes_bybranchclosed",
+        "changes_byownerclosed",
+        "changes_byowneropen",
+        "changes_byproject",
+        "changes_byprojectopen",
+        "changes_key",
+        "changes_submitted");
+    return Sets.intersection(
+        schema.getDialect().listIndexes(schema.getConnection(), TABLE),
+        allChanges);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_106.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_106.java
new file mode 100644
index 0000000..ef7e291
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_106.java
@@ -0,0 +1,104 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.SortedSet;
+
+public class Schema_106 extends SchemaVersion {
+  private final GitRepositoryManager repoManager;
+  private final PersonIdent serverUser;
+
+  @Inject
+  Schema_106(Provider<Schema_105> prior,
+      GitRepositoryManager repoManager,
+      @GerritPersonIdent PersonIdent serverUser) {
+    super(prior);
+    this.repoManager = repoManager;
+    this.serverUser = serverUser;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
+    if (!(repoManager instanceof LocalDiskRepositoryManager)) {
+      return;
+    }
+
+    ui.message("listing all repositories ...");
+    SortedSet<Project.NameKey> repoList = repoManager.list();
+    ui.message("done");
+
+    ui.message(String.format("creating reflog files for %s branches ...",
+        RefNames.REFS_CONFIG));
+    for (Project.NameKey project : repoList) {
+      try {
+        Repository repo = repoManager.openRepository(project);
+        try {
+          File metaConfigLog =
+              new File(repo.getDirectory(), "logs/" + RefNames.REFS_CONFIG);
+          if (metaConfigLog.exists()) {
+            continue;
+          }
+
+          if (!metaConfigLog.getParentFile().mkdirs()
+              || !metaConfigLog.createNewFile()) {
+            throw new IOException(String.format(
+                "Failed to create reflog for %s in repository %s",
+                RefNames.REFS_CONFIG, project));
+          }
+
+          ObjectId metaConfigId = repo.resolve(RefNames.REFS_CONFIG);
+          if (metaConfigId != null) {
+            try (PrintWriter writer =
+                new PrintWriter(metaConfigLog, UTF_8.name())) {
+              writer.print(ObjectId.zeroId().name());
+              writer.print(" ");
+              writer.print(metaConfigId.name());
+              writer.print(" ");
+              writer.print(serverUser.toExternalString());
+              writer.print("\t");
+              writer.print("create reflog");
+              writer.println();
+            }
+          }
+        } finally {
+          repo.close();
+        }
+      } catch (IOException e) {
+        ui.message(String.format("ERROR: Failed to create reflog file for the"
+            + " %s branch in repository %s", RefNames.REFS_CONFIG, project.get()));
+      }
+    }
+    ui.message("done");
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_107.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_107.java
new file mode 100644
index 0000000..13ab09a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_107.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class Schema_107 extends SchemaVersion {
+
+  @Inject
+  Schema_107(Provider<Schema_106> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      stmt.executeUpdate("UPDATE accounts set mute_common_path_prefixes = 'Y'");
+    } finally {
+      stmt.close();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_87.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_87.java
index 451f5ed..8f4028d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_87.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_87.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -51,18 +50,14 @@
 
   private Set<AccountGroup.Id> scanSystemGroups(ReviewDb db)
       throws SQLException {
-    JdbcSchema s = (JdbcSchema) db;
-    Statement stmt = s.getConnection().createStatement();
-    try {
-      ResultSet rs =
-          stmt.executeQuery("SELECT group_id FROM account_groups WHERE group_type = 'SYSTEM'");
+    try (Statement stmt = newStatement(db);
+        ResultSet rs = stmt.executeQuery(
+          "SELECT group_id FROM account_groups WHERE group_type = 'SYSTEM'")) {
       Set<AccountGroup.Id> ids = new HashSet<>();
       while (rs.next()) {
         ids.add(new AccountGroup.Id(rs.getInt(1)));
       }
       return ids;
-    } finally {
-      stmt.close();
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_89.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_89.java
index 34f6b60..a818e0d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_89.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_89.java
@@ -14,17 +14,15 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.common.collect.ImmutableList;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.DialectMySQL;
 import com.google.gwtorm.schema.sql.SqlDialect;
 import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.StatementExecutor;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
 import java.sql.SQLException;
-import java.sql.Statement;
 
 public class Schema_89 extends SchemaVersion {
   @Inject
@@ -36,19 +34,11 @@
   protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException,
       SQLException {
     SqlDialect dialect = ((JdbcSchema) db).getDialect();
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      for (String name : ImmutableList.of(
-          "patch_set_approvals_openByUser",
-          "patch_set_approvals_closedByU")) {
-        if (dialect instanceof DialectMySQL) {
-          stmt.executeUpdate("DROP INDEX " + name + " ON patch_set_approvals");
-        } else {
-          stmt.executeUpdate("DROP INDEX " + name);
-        }
-      }
-    } finally {
-      stmt.close();
+    try (StatementExecutor e = newExecutor(db)) {
+      dialect.dropIndex(e, "patch_set_approvals",
+          "patch_set_approvals_openByUser");
+      dialect.dropIndex(e, "patch_set_approvals",
+          "patch_set_approvals_closedByU");
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_90.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_90.java
index c117509..8f1fc5d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_90.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_90.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.schema;
 
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -30,11 +29,8 @@
 
   @Override
   protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
+    try (Statement stmt = newStatement(db)) {
       stmt.executeUpdate("UPDATE accounts set size_bar_in_change_table = 'Y'");
-    } finally {
-      stmt.close();
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_94.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_94.java
index 3d45274..02f78ca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_94.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_94.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.schema;
 
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -30,7 +29,7 @@
 
   @Override
   protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
-    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement()) {
+    try (Statement stmt = newStatement(db)) {
       stmt.execute("CREATE INDEX patch_sets_byRevision"
           + " ON patch_sets (revision)");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_98.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_98.java
index aea5abc..752dcd8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_98.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_98.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.schema;
 
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -32,13 +31,10 @@
   protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
     ui.message("Migrate user preference showUserInReview to "
         + "reviewCategoryStrategy");
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
+    try (Statement stmt = newStatement(db)) {
       stmt.executeUpdate("UPDATE accounts SET "
           + "REVIEW_CATEGORY_STRATEGY='NAME' "
           + "WHERE (SHOW_USER_IN_REVIEW='Y')");
-    } finally {
-      stmt.close();
     }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_99.java
similarity index 72%
copy from gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_99.java
index 407b7c7..b7fab7f 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_99.java
@@ -12,10 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.extensions.api.projects;
+package com.google.gerrit.server.schema;
 
-public enum ProjectState {
-  ACTIVE,
-  READ_ONLY,
-  HIDDEN
-}
\ No newline at end of file
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_99 extends SchemaVersion {
+  @Inject
+  Schema_99(Provider<Schema_98> prior) {
+    super(prior);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java
index d03cb3e..2347f87 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java
@@ -36,6 +36,7 @@
   private final List<String> commands;
 
   static final ScriptRunner NOOP = new ScriptRunner(null, null) {
+    @Override
     void run(final ReviewDb db) {
     }
   };
@@ -51,11 +52,10 @@
 
   void run(final ReviewDb db) throws OrmException {
     try {
-      final JdbcSchema schema = (JdbcSchema)db;
+      final JdbcSchema schema = (JdbcSchema) db;
       final Connection c = schema.getConnection();
       final SqlDialect dialect = schema.getDialect();
-      final Statement stmt = c.createStatement();
-      try {
+      try (Statement stmt = c.createStatement()) {
         for (String sql : commands) {
           try {
             if (!dialect.isStatementDelimiterSupported()) {
@@ -66,8 +66,6 @@
             throw new OrmException("Error in " + name + ":\n" + sql, e);
           }
         }
-      } finally {
-        stmt.close();
       }
     } catch (SQLException e) {
       throw new OrmException("Cannot run statements for " + name, e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/DefaultSecureStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/DefaultSecureStore.java
index e0e8237..b852217 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/DefaultSecureStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/DefaultSecureStore.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.securestore;
 
 import com.google.gerrit.common.FileUtil;
-import com.google.gerrit.extensions.annotations.Export;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -27,12 +26,11 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 
 @Singleton
-@Export(DefaultSecureStore.NAME)
-public class DefaultSecureStore implements SecureStore {
-  public static final String NAME = "default";
-
+public class DefaultSecureStore extends SecureStore {
   private final FileBasedConfig sec;
 
   @Inject
@@ -47,14 +45,15 @@
   }
 
   @Override
-  public String get(String section, String subsection, String name) {
-    return sec.getString(section, subsection, name);
+  public String[] getList(String section, String subsection, String name) {
+    return sec.getStringList(section, subsection, name);
   }
 
   @Override
-  public void set(String section, String subsection, String name, String value) {
-    if (value != null) {
-      sec.setString(section, subsection, name, value);
+  public void setList(String section, String subsection, String name,
+      List<String> values) {
+    if (values != null) {
+      sec.setStringList(section, subsection, name, values);
     } else {
       sec.unset(section, subsection, name);
     }
@@ -67,6 +66,22 @@
     save();
   }
 
+  @Override
+  public Iterable<EntryKey> list() {
+    List<EntryKey> result = new ArrayList<>();
+    for (String section : sec.getSections()) {
+      for (String subsection : sec.getSubsections(section)) {
+        for (String name : sec.getNames(section, subsection)) {
+          result.add(new EntryKey(section, subsection, name));
+        }
+      }
+      for (String name : sec.getNames(section)) {
+        result.add(new EntryKey(section, null, name));
+      }
+    }
+    return result;
+  }
+
   private void save() {
     try {
       saveSecure(sec);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStore.java
index 3fe00f4..2a0086e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStore.java
@@ -14,14 +14,115 @@
 
 package com.google.gerrit.server.securestore;
 
-import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.common.collect.Lists;
 
-@ExtensionPoint
-public interface SecureStore {
+import java.util.List;
 
-  String get(String section, String subsection, String name);
+/**
+ * Abstract class for providing new SecureStore implementation for Gerrit.
+ *
+ * SecureStore is responsible for storing sensitive data like passwords in a
+ * secure manner.
+ *
+ * It is implementator's responsibility to encrypt and store values.
+ *
+ * To deploy new SecureStore one needs to provide a jar file with explicitly one
+ * class that extends {@code SecureStore} and put it in Gerrit server. Then run:
+ *
+ * `java -jar gerrit.war SwitchSecureStore -d $gerrit_site --new-secure-store-lib
+ *  $path_to_new_secure_store.jar`
+ *
+ * on stopped Gerrit instance.
+ */
+public abstract class SecureStore {
+  /**
+   * Describes {@link SecureStore} entry
+   */
+  public static class EntryKey {
+    public final String name;
+    public final String section;
+    public final String subsection;
 
-  void set(String section, String subsection, String name, String value);
+    /**
+     * Creates EntryKey.
+     *
+     * @param section
+     * @param subsection
+     * @param name
+     */
+    public EntryKey(String section, String subsection, String name) {
+      this.name = name;
+      this.section = section;
+      this.subsection = subsection;
+    }
+  }
 
-  void unset(String section, String subsection, String name);
+  /**
+   * Extract decrypted value of stored property from SecureStore or {@code null}
+   * when property was not found.
+   *
+   * @param section
+   * @param subsection
+   * @param name
+   * @return decrypted String value or {@code null} if not found
+   */
+  public final String get(String section, String subsection, String name) {
+    String[] values = getList(section, subsection, name);
+    if (values != null && values.length > 0) {
+      return values[0];
+    }
+    return null;
+  }
+
+  /**
+   * Extract list of values from SecureStore and decrypt every value in that
+   * list or {@code null} when property was not found.
+   *
+   * @param section
+   * @param subsection
+   * @param name
+   * @return decrypted list of string values or {@code null}
+   */
+  public abstract String[] getList(String section, String subsection, String name);
+
+  /**
+   * Store single value in SecureStore.
+   *
+   * This method is responsible for encrypting value and storing it.
+   *
+   * @param section
+   * @param subsection
+   * @param name
+   * @param value plain text value
+   */
+  public final void set(String section, String subsection, String name, String value) {
+    setList(section, subsection, name, Lists.newArrayList(value));
+  }
+
+  /**
+   * Store list of values in SecureStore.
+   *
+   * This method is responsible for encrypting all values in the list and storing them.
+   *
+   * @param section
+   * @param subsection
+   * @param name
+   * @param values list of plain text values
+   */
+  public abstract void setList(String section, String subsection, String name, List<String> values);
+
+  /**
+   * Remove value for given {@code section}, {@code subsection} and {@code name}
+   * from SecureStore.
+   *
+   * @param section
+   * @param subsection
+   * @param name
+   */
+  public abstract void unset(String section, String subsection, String name);
+
+  /**
+   * @return list of stored entries.
+   */
+  public abstract Iterable<EntryKey> list();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreClassName.java b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreClassName.java
new file mode 100644
index 0000000..07635bd
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreClassName.java
@@ -0,0 +1,12 @@
+package com.google.gerrit.server.securestore;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface SecureStoreClassName {
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreData.java b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreData.java
deleted file mode 100644
index b925105..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreData.java
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright (C) 2014 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.securestore;
-
-import com.google.common.base.Objects;
-
-import java.io.File;
-import java.net.URL;
-import java.net.URLClassLoader;
-
-public class SecureStoreData {
-  public final File pluginFile;
-  public final String storeName;
-  public final String className;
-
-  public SecureStoreData(String pluginName, String className, File jarFile,
-      String storeName) {
-    this.className = className;
-    this.pluginFile = jarFile;
-    this.storeName = String.format("%s/%s", pluginName, storeName);
-  }
-
-  public String getStoreName() {
-    return storeName;
-  }
-
-  public Class<? extends SecureStore> load() {
-    return load(pluginFile);
-  }
-
-  @SuppressWarnings("unchecked")
-  public Class<? extends SecureStore> load(File pluginFile) {
-    try {
-      URL[] pluginJarUrls = new URL[] {pluginFile.toURI().toURL()};
-      ClassLoader currentCL = Thread.currentThread().getContextClassLoader();
-      final URLClassLoader newClassLoader =
-          new URLClassLoader(pluginJarUrls, currentCL);
-      Thread.currentThread().setContextClassLoader(newClassLoader);
-      return (Class<? extends SecureStore>) newClassLoader.loadClass(className);
-    } catch (Exception e) {
-      throw new SecureStoreException(String.format(
-          "Cannot load secure store implementation for %s", storeName), e);
-    }
-  }
-
-  @Override
-  public String toString() {
-    return Objects.toStringHelper(this).add("storeName", storeName)
-        .add("className", className).add("file", pluginFile).toString();
-  }
-
-  @Override
-  public boolean equals(Object obj) {
-    if (obj instanceof SecureStoreData) {
-      SecureStoreData o = (SecureStoreData) obj;
-      return storeName.equals(o.storeName);
-    }
-    return false;
-  }
-
-  @Override
-  public int hashCode() {
-    return Objects.hashCode(storeName);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreProvider.java
new file mode 100644
index 0000000..e830590
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreProvider.java
@@ -0,0 +1,71 @@
+// 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.securestore;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.SiteLibraryLoaderUtil;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+
+@Singleton
+public class SecureStoreProvider implements Provider<SecureStore> {
+  private static final Logger log = LoggerFactory
+      .getLogger(SecureStoreProvider.class);
+
+  private final File libdir;
+  private final Injector injector;
+  private final String className;
+
+  @Inject
+  protected SecureStoreProvider(
+      Injector injector,
+      SitePaths sitePaths,
+      @Nullable @SecureStoreClassName String className) {
+    this.injector = injector;
+    this.libdir = sitePaths.lib_dir;
+    this.className = className;
+  }
+
+  @Override
+  public synchronized SecureStore get() {
+    return injector.getInstance(getSecureStoreImpl());
+  }
+
+  @SuppressWarnings("unchecked")
+  private Class<? extends SecureStore> getSecureStoreImpl() {
+    if (Strings.isNullOrEmpty(className)) {
+      return DefaultSecureStore.class;
+    }
+
+    SiteLibraryLoaderUtil.loadSiteLib(libdir);
+    try {
+      return (Class<? extends SecureStore>) Class.forName(className);
+    } catch (ClassNotFoundException e) {
+      String msg =
+          String.format("Cannot load secure store class: %s", className);
+      log.error(msg, e);
+      throw new RuntimeException(msg, e);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/HostPlatform.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/HostPlatform.java
index ea4b7f9..c585270 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/HostPlatform.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/HostPlatform.java
@@ -28,6 +28,7 @@
   private static final boolean computeWin32() {
     final String osDotName =
         AccessController.doPrivileged(new PrivilegedAction<String>() {
+          @Override
           public String run() {
             return System.getProperty("os.name");
           }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java
index 13bc766..2e01613 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java
@@ -16,17 +16,18 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 
-import com.google.common.base.Objects;
+import com.google.auto.value.AutoValue;
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 
 /** A single vote on a label, consisting of a label name and a value. */
-public class LabelVote {
+@AutoValue
+public abstract class LabelVote {
   public static LabelVote parse(String text) {
     checkArgument(!Strings.isNullOrEmpty(text), "Empty label vote");
     if (text.charAt(0) == '-') {
-      return new LabelVote(text.substring(1), (short) 0);
+      return create(text.substring(1), (short) 0);
     }
     short sign = 0;
     int i;
@@ -43,9 +44,9 @@
       }
     }
     if (sign == 0) {
-      return new LabelVote(text, (short) 1);
+      return create(text, (short) 1);
     }
-    return new LabelVote(text.substring(0, i),
+    return create(text.substring(0, i),
         (short)(sign * Short.parseShort(text.substring(i + 1))));
   }
 
@@ -53,59 +54,40 @@
     checkArgument(!Strings.isNullOrEmpty(text), "Empty label vote");
     int e = text.lastIndexOf('=');
     checkArgument(e >= 0, "Label vote missing '=': %s", text);
-    return new LabelVote(text.substring(0, e),
+    return create(text.substring(0, e),
         Short.parseShort(text.substring(e + 1), text.length()));
   }
 
-  private final String name;
-  private final short value;
-
-  public LabelVote(String name, short value) {
-    this.name = LabelType.checkNameInternal(name);
-    this.value = value;
+  public static LabelVote create(String label, short value) {
+    return new AutoValue_LabelVote(LabelType.checkNameInternal(label), value);
   }
 
-  public LabelVote(PatchSetApproval psa) {
-    this(psa.getLabel(), psa.getValue());
+  public static LabelVote create(PatchSetApproval psa) {
+    return create(psa.getLabel(), psa.getValue());
   }
 
-  public String getLabel() {
-    return name;
-  }
-
-  public short getValue() {
-    return value;
-  }
+  public abstract String label();
+  public abstract short value();
 
   public String format() {
-    if (value == (short) 0) {
-      return '-' + name;
-    } else if (value < 0) {
-      return name + value;
+    if (value() == (short) 0) {
+      return '-' + label();
+    } else if (value() < 0) {
+      return label() + value();
     } else {
-      return name + '+' + value;
+      return label() + '+' + value();
     }
   }
 
   public String formatWithEquals() {
-    if (value <= (short) 0) {
-      return name + '=' + value;
+    if (value() <= (short) 0) {
+      return label() + '=' + value();
     } else {
-      return name + "=+" + value;
+      return label() + "=+" + value();
     }
   }
 
   @Override
-  public boolean equals(Object o) {
-    if (o instanceof LabelVote) {
-      LabelVote l = (LabelVote) o;
-      return Objects.equal(name, l.name)
-          && value == l.value;
-    }
-    return false;
-  }
-
-  @Override
   public String toString() {
     return format();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ManualRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ManualRequestContext.java
new file mode 100644
index 0000000..0ef4a18
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ManualRequestContext.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Provider;
+import com.google.inject.util.Providers;
+
+/**
+ * Closeable version of a {@link RequestContext} with manually-specified
+ * providers.
+ */
+public class ManualRequestContext implements RequestContext, AutoCloseable {
+  private final CurrentUser user;
+  private final Provider<ReviewDb> db;
+  private final ThreadLocalRequestContext requestContext;
+  private final RequestContext old;
+
+  ManualRequestContext(CurrentUser user, SchemaFactory<ReviewDb> schemaFactory,
+      ThreadLocalRequestContext requestContext) throws OrmException {
+    this.user = user;
+    this.db = Providers.of(schemaFactory.open());
+    this.requestContext = requestContext;
+    old = requestContext.setContext(this);
+  }
+
+  @Override
+  public CurrentUser getCurrentUser() {
+    return user;
+  }
+
+  @Override
+  public Provider<ReviewDb> getReviewDbProvider() {
+    return db;
+  }
+
+  @Override
+  public void close() {
+    requestContext.setContext(old);
+    db.get().close();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/OneOffRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/OneOffRequestContext.java
new file mode 100644
index 0000000..6feb182
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/OneOffRequestContext.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.InternalUser;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/**
+ * Helper to create one-off request contexts.
+ * <p>
+ * Each call to {@link #open()} opens a new {@link ReviewDb}, so this class
+ * should only be used in a bounded try/finally block.
+ * <p>
+ * The user in the request context is {@link InternalUser}.
+ */
+@Singleton
+public class OneOffRequestContext {
+  private final InternalUser.Factory userFactory;
+  private final SchemaFactory<ReviewDb> schemaFactory;
+  private final ThreadLocalRequestContext requestContext;
+
+  @Inject
+  OneOffRequestContext(InternalUser.Factory userFactory,
+      SchemaFactory<ReviewDb> schemaFactory,
+      ThreadLocalRequestContext requestContext) {
+    this.userFactory = userFactory;
+    this.schemaFactory = schemaFactory;
+    this.requestContext = requestContext;
+  }
+
+  public ManualRequestContext open() throws OrmException {
+    return new ManualRequestContext(userFactory.create(),
+        schemaFactory, requestContext);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RegexListSearcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RegexListSearcher.java
new file mode 100644
index 0000000..4b0fd35
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RegexListSearcher.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.primitives.Chars;
+
+import dk.brics.automaton.Automaton;
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
+
+import java.util.Collections;
+import java.util.List;
+
+/** Helper to search sorted lists for elements matching a regex. */
+public abstract class RegexListSearcher<T> implements Function<T, String> {
+  public static RegexListSearcher<String> ofStrings(String re) {
+    return new RegexListSearcher<String>(re) {
+      @Override
+      public String apply(String in) {
+        return in;
+      }
+    };
+  }
+
+  private final RunAutomaton pattern;
+
+  private final String prefixBegin;
+  private final String prefixEnd;
+  private final int prefixLen;
+  private final boolean prefixOnly;
+
+  public RegexListSearcher(String re) {
+    if (re.startsWith("^")) {
+      re = re.substring(1);
+    }
+
+    if (re.endsWith("$") && !re.endsWith("\\$")) {
+      re = re.substring(0, re.length() - 1);
+    }
+
+    Automaton automaton = new RegExp(re).toAutomaton();
+    prefixBegin = automaton.getCommonPrefix();
+    prefixLen = prefixBegin.length();
+
+    if (0 < prefixLen) {
+      char max = Chars.checkedCast(prefixBegin.charAt(prefixLen - 1) + 1);
+      prefixEnd = prefixBegin.substring(0, prefixLen - 1) + max;
+      prefixOnly = re.equals(prefixBegin + ".*");
+    } else {
+      prefixEnd = "";
+      prefixOnly = false;
+    }
+
+    pattern = prefixOnly ? null : new RunAutomaton(automaton);
+  }
+
+  public Iterable<T> search(List<T> list) {
+    checkNotNull(list);
+    int begin, end;
+
+    if (0 < prefixLen) {
+      // Assumes many consecutive elements may have the same prefix, so the cost
+      // of two binary searches is less than iterating to find the endpoints.
+      begin = find(list, prefixBegin);
+      end = find(list, prefixEnd);
+    } else {
+      begin = 0;
+      end = list.size();
+    }
+
+    if (prefixOnly) {
+      return begin < end ? list.subList(begin, end) : ImmutableList.<T> of();
+    }
+
+    return Iterables.filter(
+        list.subList(begin, end),
+        new Predicate<T>() {
+          @Override
+          public boolean apply(T in) {
+            return pattern.run(RegexListSearcher.this.apply(in));
+          }
+        });
+  }
+
+  public boolean hasMatch(List<T> list) {
+    return !Iterables.isEmpty(search(list));
+  }
+
+  private int find(List<T> list, String p) {
+    int r = Collections.binarySearch(Lists.transform(list, this), p);
+    return r < 0 ? -(r + 1) : r;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ServerRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ServerRequestContext.java
index 6730e30..2b6b86e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ServerRequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ServerRequestContext.java
@@ -17,9 +17,9 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.InternalUser;
+import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
-import com.google.inject.Inject;
 
 /** RequestContext with an InternalUser making the internals visible. */
 public class ServerRequestContext implements RequestContext {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
index f8bad77..d1b1da4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.util;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.errors.NotSignedInException;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -48,7 +48,7 @@
       @Provides
       RequestContext provideRequestContext(
           @Named(FALLBACK) RequestContext fallback) {
-        return Objects.firstNonNull(local.get(), fallback);
+        return MoreObjects.firstNonNull(local.get(), fallback);
       }
 
       @Provides
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/validators/HashtagValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/validators/HashtagValidationListener.java
new file mode 100644
index 0000000..c1d509e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/validators/HashtagValidationListener.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.validators;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.reviewdb.client.Change;
+
+import java.util.Set;
+
+/**
+ * Listener to provide validation of hashtag changes.
+ */
+@ExtensionPoint
+public interface HashtagValidationListener {
+  /**
+   * Invoked by Gerrit before hashtags are changed.
+   *
+   * @param change the change on which the hashtags are changed
+   * @param toAdd the hashtags to be added
+   * @param toRemove the hashtags to be removed
+   * @throws ValidationException if validation fails
+   */
+  public void validateHashtags(Change change, Set<String> toAdd,
+      Set<String> toRemove) throws ValidationException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java
new file mode 100644
index 0000000..5b3f158
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.validators;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.mail.EmailHeader;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Listener to provide validation on outgoing email notification.
+ */
+@ExtensionPoint
+public interface OutgoingEmailValidationListener {
+  /**
+   * Arguments supplied to validateOutgoingEmail.
+   */
+  public static class Args {
+    // in arguments
+    public String messageClass;
+
+    // in/out arguments
+    public Address smtpFromAddress;
+    public Set<Address> smtpRcptTo;
+    public String body;
+    public Map<String, EmailHeader> headers;
+  }
+
+  /**
+   * Outgoing e-mail validation.
+   *
+   * Invoked by Gerrit just before an e-mail is sent, after all e-mail templates
+   * have been applied.
+   *
+   * Plugins may modify the following fields in args:
+   * - smtpFromAddress
+   * - smtpRcptTo
+   * - body
+   * - headers
+   *
+   * @param args E-mail properties. Some are mutable.
+   * @throws ValidationException if validation fails.
+   */
+  public void validateOutgoingEmail(OutgoingEmailValidationListener.Args args)
+      throws ValidationException;
+}
diff --git a/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java b/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
index 8e91262..f0806a5 100644
--- a/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
@@ -43,10 +43,6 @@
           StoredValues.CHANGE_CONTROL.get(engine).getLabelTypes();
 
       for (PatchSetApproval a : cd.currentApprovals()) {
-        if (a.getValue() == 0) {
-          continue;
-        }
-
         LabelType t = types.byLabel(a.getLabelId());
         if (t == null) {
           continue;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java b/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java
index 2c7949c..509faf0 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java
@@ -73,11 +73,10 @@
     PatchList pl = StoredValues.PATCH_LIST.get(engine);
     Repository repo = StoredValues.REPOSITORY.get(engine);
 
-    final ObjectReader reader = repo.newObjectReader();
-    final RevTree aTree;
-    final RevTree bTree;
-    try {
-      final RevWalk rw = new RevWalk(reader);
+    try (ObjectReader reader = repo.newObjectReader();
+        RevWalk rw = new RevWalk(reader)) {
+      final RevTree aTree;
+      final RevTree bTree;
       final RevCommit bCommit = rw.parseCommit(pl.getNewId());
 
       if (pl.getOldId() != null) {
@@ -129,8 +128,6 @@
       }
     } catch (IOException err) {
       throw new JavaException(this, 1, err);
-    } finally {
-      reader.release();
     }
 
     return engine.fail();
@@ -161,4 +158,4 @@
     }
     return new Text(reader.open(tw.getObjectId(0), Constants.OBJ_BLOB));
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_stats_3.java b/gerrit-server/src/main/java/gerrit/PRED_commit_stats_3.java
index daf9948..83878be 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_stats_3.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_stats_3.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.rules.StoredValues;
 import com.google.gerrit.server.patch.PatchList;
+
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.Operation;
 import com.googlecode.prolog_cafe.lang.Predicate;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
index dd2a008..6d0dd0f 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
-import com.google.gerrit.server.project.ChangeControl;
 
 import com.googlecode.prolog_cafe.lang.EvaluationException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
@@ -47,8 +46,11 @@
     engine.setB0();
     Term a1 = arg1.dereference();
 
-    ChangeControl cControl = StoredValues.CHANGE_CONTROL.get(engine);
-    CurrentUser curUser = cControl.getCurrentUser();
+    CurrentUser curUser = StoredValues.CURRENT_USER.getOrNull(engine);
+    if (curUser == null) {
+      throw new EvaluationException(
+          "Current user not available in this rule type");
+    }
     Term resultTerm;
 
     if (curUser.isIdentifiedUser()) {
@@ -67,4 +69,4 @@
     }
     return cont;
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/gerrit/PRED_project_default_submit_type_1.java b/gerrit-server/src/main/java/gerrit/PRED_project_default_submit_type_1.java
index a471450..824c6ef 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_project_default_submit_type_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_project_default_submit_type_1.java
@@ -14,7 +14,7 @@
 
 package gerrit;
 
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.rules.StoredValues;
 import com.google.gerrit.server.project.ChangeControl;
 
diff --git a/gerrit-server/src/main/prolog/gerrit_common.pl b/gerrit-server/src/main/prolog/gerrit_common.pl
index 4738d15..2a45819 100644
--- a/gerrit-server/src/main/prolog/gerrit_common.pl
+++ b/gerrit-server/src/main/prolog/gerrit_common.pl
@@ -343,7 +343,7 @@
     ( is_all_ok(Ls) -> T = ok(S) ; T = not_ready(S) ),
     filter_submit_results(Filter, In, [T | Tmp], Out).
 filter_submit_results(Filter, [_ | In], Tmp, Out) :-
-   filter_submit_results(Filter, In, Tmp, Out), 
+   filter_submit_results(Filter, In, Tmp, Out),
    !
    .
 filter_submit_results(Filter, [], Out, Out).
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties
index 9eb7d9b..9c48292 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties
@@ -1,11 +1,13 @@
 accessDatabase = Access Database
 administrateServer = Administrate Server
+batchChangesLimit = Batch Changes Limit
 createAccount = Create Account
 createGroup = Create Group
 createProject = Create Project
 emailReviewers = Email Reviewers
 flushCaches = Flush Caches
 killTask = Kill Task
+modifyAccount = Modify Account
 priority = Priority
 queryLimit = Query Limit
 runAs = Run As
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm
index 1d5b33a..4fd9a23 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm
@@ -31,6 +31,11 @@
 ## The ChangeSubject.vm template will determine the contents of the email
 ## subject line for ALL emails related to changes.
 ##
+## Optionally $change.originalSubject can be used for the first subject
+## in a change. This allows subject based email clients such as GMail
+## to thread comments together even if subsequent patch sets change the
+## first line of the commit message.
+##
 #macro(ellipsis $length $str)
 #if($str.length() > $length)#set($length = $length - 3)${str.substring(0,$length)}...#else$str#end
 #end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mime-types.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties
similarity index 72%
rename from gerrit-server/src/main/resources/com/google/gerrit/server/mime-types.properties
rename to gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties
index 817790f..3d8d271 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mime-types.properties
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -5,21 +5,28 @@
 cl = text/x-common-lisp
 coffee = text/x-coffeescript
 cs = text/x-csharp
+cpp = text/x-c++src
 cxx = text/x-c++src
 d = text/x-d
+dart = application/dart
 defs = text/x-python
 diff = text/x-diff
+Dockerfile = text/x-dockerfile
 dtd = application/xml-dtd
 el = text/x-common-lisp
 erl = text/x-erlang
+frag = x-shader/x-fragment
 gitmodules = text/x-ini
+glsl = x-shader/x-vertex
 go = text/x-go
 groovy = text/x-groovy
 hs = text/x-haskell
-hxx = text/x-c++hdr
+hpp = text/x-c++src
+hxx = text/x-c++src
 lisp = text/x-common-lisp
 lsp = text/x-common-lisp
 lua = text/x-lua
+m = text/x-objectivec
 md = text/x-markdown
 patch = text/x-diff
 php = text/x-php
@@ -31,9 +38,14 @@
 py = text/x-python
 r = text/r-src
 rb = text/x-ruby
+rng = application/xml
+rst = text/x-rst
 scala = text/x-scala
+soy = text/x-soy
 st = text/x-stsrc
+stex = text/x-stex
 v = text/x-verilog
+vert = x-shader/x-vertex
 vh = text/x-verilog
 vm = text/velocity
 yaml = text/x-yaml
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 1ed35be..f5dca09 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,11 +14,14 @@
 
 package com.google.gerrit.rules;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.common.data.Permission.LABEL;
 import static com.google.gerrit.server.project.Util.allow;
 import static com.google.gerrit.server.project.Util.category;
 import static com.google.gerrit.server.project.Util.value;
+import static org.junit.Assert.fail;
 
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -27,13 +30,21 @@
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.Util;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
 import com.google.inject.AbstractModule;
 
+import com.googlecode.prolog_cafe.compiler.CompileException;
+import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
+import com.googlecode.prolog_cafe.lang.Prolog;
+import com.googlecode.prolog_cafe.lang.StructureTerm;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
+
+import org.eclipse.jgit.lib.Config;
 import org.junit.Before;
 import org.junit.Test;
 
+import java.io.PushbackReader;
+import java.io.StringReader;
 import java.util.Arrays;
 
 public class GerritCommonTest extends PrologTestCase {
@@ -56,6 +67,9 @@
     load("gerrit", "gerrit_common_test.pl", new AbstractModule() {
       @Override
       protected void configure() {
+        Config cfg = new Config();
+        cfg.setInt("rules", null, "reductionLimit", 1300);
+        cfg.setInt("rules", null, "compileReductionLimit", (int) 1e6);
         bind(PrologEnvironment.Args.class).toInstance(
             new PrologEnvironment.Args(
                 null,
@@ -63,7 +77,8 @@
                 null,
                 null,
                 null,
-                null));
+                null,
+                cfg));
       }
     });
 
@@ -92,4 +107,30 @@
   public void testGerritCommon() {
     runPrologBasedTests();
   }
+
+  @Test
+  public void testReductionLimit() throws CompileException {
+    PrologEnvironment env = envFactory.create(machine);
+    setUpEnvironment(env);
+    env.setEnabled(Prolog.Feature.IO, true);
+
+    String script = "loopy :- b(5).\n"
+        + "b(N) :- N > 0, !, S = N - 1, b(S).\n"
+        + "b(_) :- true.\n";
+
+    SymbolTerm nameTerm = SymbolTerm.create("testReductionLimit");
+    JavaObjectTerm inTerm = new JavaObjectTerm(
+        new PushbackReader(new StringReader(script), Prolog.PUSHBACK_SIZE));
+    if (!env.execute(Prolog.BUILTIN, "consult_stream", nameTerm, inTerm)) {
+      throw new CompileException("Cannot consult " + nameTerm);
+    }
+
+    try {
+      env.once(Prolog.BUILTIN, "call", new StructureTerm(":",
+          SymbolTerm.create("user"), SymbolTerm.create("loopy")));
+      fail("long running loop did not abort with ReductionLimitException");
+    } catch (ReductionLimitException e) {
+      assertThat(e.getMessage()).isEqualTo("exceeded reduction limit of 1300");
+    }
+  }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
index c697400..aaab173 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
@@ -14,7 +14,11 @@
 
 package com.google.gerrit.rules;
 
-import com.google.gerrit.server.util.TimeUtil;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.gerrit.common.TimeUtil;
 import com.google.inject.Guice;
 import com.google.inject.Module;
 
@@ -39,10 +43,6 @@
 import java.util.Arrays;
 import java.util.List;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
 
 /** Base class for any tests written in Prolog. */
 public abstract class PrologTestCase {
@@ -52,8 +52,8 @@
   private boolean hasSetup;
   private boolean hasTeardown;
   private List<Term> tests;
-  private PrologMachineCopy machine;
-  private PrologEnvironment.Factory envFactory;
+  protected PrologMachineCopy machine;
+  protected PrologEnvironment.Factory envFactory;
 
   protected void load(String pkg, String prologResource, Module... modules)
       throws CompileException, IOException {
@@ -82,6 +82,11 @@
     machine = PrologMachineCopy.save(env);
   }
 
+  /**
+   * Set up the Prolog environment.
+   *
+   * @param env Prolog environment.
+   */
   protected void setUpEnvironment(PrologEnvironment env) {
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java
index 0bbec8a..b4d6e96 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server;
 
-import org.junit.Test;
-
 import static org.junit.Assert.assertEquals;
 
+import org.junit.Test;
+
 public class StringUtilTest {
   /**
    * Test the boundary condition that the first character of a string
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/account/UniversalGroupBackendTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/account/UniversalGroupBackendTest.java
new file mode 100644
index 0000000..3cec25c
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/account/UniversalGroupBackendTest.java
@@ -0,0 +1,148 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
+import static com.google.gerrit.server.group.SystemGroupBackend.PROJECT_OWNERS;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.getCurrentArguments;
+import static org.easymock.EasyMock.not;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.server.StandardKeyEncoder;
+
+import org.easymock.IAnswer;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Set;
+
+public class UniversalGroupBackendTest {
+  static {
+    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
+  }
+
+  private static final AccountGroup.UUID OTHER_UUID =
+      new AccountGroup.UUID("other");
+
+  private UniversalGroupBackend backend;
+  private IdentifiedUser user;
+
+  private DynamicSet<GroupBackend> backends;
+
+  @Before
+  public void setup() {
+    user = createNiceMock(IdentifiedUser.class);
+    replay(user);
+    backends = new DynamicSet<>();
+    backends.add(new SystemGroupBackend());
+    backend = new UniversalGroupBackend(backends);
+  }
+
+  @Test
+  public void testHandles() {
+    assertTrue(backend.handles(ANONYMOUS_USERS));
+    assertTrue(backend.handles(PROJECT_OWNERS));
+    assertFalse(backend.handles(OTHER_UUID));
+  }
+
+  @Test
+  public void testGet() {
+    assertEquals("Registered Users",
+        backend.get(REGISTERED_USERS).getName());
+    assertEquals("Project Owners",
+        backend.get(PROJECT_OWNERS).getName());
+    assertNull(backend.get(OTHER_UUID));
+  }
+
+  @Test
+  public void testSuggest() {
+    assertTrue(backend.suggest("X", null).isEmpty());
+    assertEquals(1, backend.suggest("project", null).size());
+    assertEquals(1, backend.suggest("reg", null).size());
+  }
+
+  @Test
+  public void testSytemGroupMemberships() {
+    GroupMembership checker = backend.membershipsOf(user);
+    assertTrue(checker.contains(REGISTERED_USERS));
+    assertFalse(checker.contains(OTHER_UUID));
+    assertFalse(checker.contains(PROJECT_OWNERS));
+  }
+
+  @Test
+  public void testKnownGroups() {
+    GroupMembership checker = backend.membershipsOf(user);
+    Set<UUID> knownGroups = checker.getKnownGroups();
+    assertEquals(2, knownGroups.size());
+    assertTrue(knownGroups.contains(REGISTERED_USERS));
+    assertTrue(knownGroups.contains(ANONYMOUS_USERS));
+  }
+
+  @Test
+  public void testOtherMemberships() {
+    final AccountGroup.UUID handled = new AccountGroup.UUID("handled");
+    final AccountGroup.UUID notHandled = new AccountGroup.UUID("not handled");
+    final IdentifiedUser member = createNiceMock(IdentifiedUser.class);
+    final IdentifiedUser notMember = createNiceMock(IdentifiedUser.class);
+
+    GroupBackend backend = createMock(GroupBackend.class);
+    expect(backend.handles(handled)).andStubReturn(true);
+    expect(backend.handles(not(eq(handled)))).andStubReturn(false);
+    expect(backend.membershipsOf(anyObject(IdentifiedUser.class)))
+        .andStubAnswer(new IAnswer<GroupMembership>() {
+          @Override
+          public GroupMembership answer() throws Throwable {
+            Object[] args = getCurrentArguments();
+            GroupMembership membership = createMock(GroupMembership.class);
+            expect(membership.contains(eq(handled))).andStubReturn(args[0] == member);
+            expect(membership.contains(not(eq(notHandled)))).andStubReturn(false);
+            replay(membership);
+            return membership;
+          }
+        });
+    replay(member, notMember, backend);
+
+    backends = new DynamicSet<>();
+    backends.add(backend);
+    backend = new UniversalGroupBackend(backends);
+
+    GroupMembership checker =
+        backend.membershipsOf(member);
+    assertFalse(checker.contains(REGISTERED_USERS));
+    assertFalse(checker.contains(OTHER_UUID));
+    assertTrue(checker.contains(handled));
+    assertFalse(checker.contains(notHandled));
+    checker = backend.membershipsOf(notMember);
+    assertFalse(checker.contains(handled));
+    assertFalse(checker.contains(notHandled));
+  }
+
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
index a30fa92..45ac043 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
@@ -23,10 +23,13 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
-import com.google.gerrit.common.changes.Side;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -37,22 +40,27 @@
 import com.google.gerrit.reviewdb.client.CommentRange;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.PatchLineCommentAccess;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.FakeRealm;
 import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.DisableReverseDnsLookup;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitModule;
@@ -62,22 +70,23 @@
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.util.TimeUtil;
-import com.google.gerrit.testutil.TestChanges;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gerrit.testutil.FakeAccountCache;
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.gerrit.testutil.TestChanges;
 import com.google.gwtorm.server.ListResultSet;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
+import com.google.inject.Inject;
 import com.google.inject.Injector;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
 import com.google.inject.TypeLiteral;
 import com.google.inject.util.Providers;
 
 import org.easymock.IAnswer;
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.junit.Before;
@@ -101,77 +110,56 @@
 
   @ConfigSuite.Config
   public static @GerritServerConfig Config noteDbEnabled() {
-    @GerritServerConfig Config cfg = new Config();
-    cfg.setBoolean("notedb", null, "write", true);
-    cfg.setBoolean("notedb", "publishedComments", "read", true);
-    return cfg;
+    return NotesMigration.allEnabledConfig();
   }
 
   private Injector injector;
+  private ReviewDb db;
   private Project.NameKey project;
-  private InMemoryRepositoryManager repoManager;
-  private PatchLineCommentsUtil plcUtil;
   private RevisionResource revRes1;
   private RevisionResource revRes2;
   private PatchLineComment plc1;
   private PatchLineComment plc2;
   private PatchLineComment plc3;
+  private PatchLineComment plc4;
+  private PatchLineComment plc5;
   private IdentifiedUser changeOwner;
 
+  @Inject private AllUsersNameProvider allUsers;
+  @Inject private Comments comments;
+  @Inject private DraftComments drafts;
+  @Inject private GetComment getComment;
+  @Inject private IdentifiedUser.GenericFactory userFactory;
+  @Inject private InMemoryRepositoryManager repoManager;
+  @Inject private NotesMigration migration;
+  @Inject private PatchLineCommentsUtil plcUtil;
+
   @Before
   public void setUp() throws Exception {
     @SuppressWarnings("unchecked")
-    final DynamicMap<RestView<CommentResource>> views =
+    final DynamicMap<RestView<CommentResource>> commentViews =
         createMock(DynamicMap.class);
-    final TypeLiteral<DynamicMap<RestView<CommentResource>>> viewsType =
+    final TypeLiteral<DynamicMap<RestView<CommentResource>>> commentViewsType =
         new TypeLiteral<DynamicMap<RestView<CommentResource>>>() {};
-    final AccountInfo.Loader.Factory alf =
-        createMock(AccountInfo.Loader.Factory.class);
-    final ReviewDb db = createMock(ReviewDb.class);
+    @SuppressWarnings("unchecked")
+    final DynamicMap<RestView<DraftCommentResource>> draftViews =
+        createMock(DynamicMap.class);
+    final TypeLiteral<DynamicMap<RestView<DraftCommentResource>>> draftViewsType =
+        new TypeLiteral<DynamicMap<RestView<DraftCommentResource>>>() {};
+
+    final AccountLoader.Factory alf =
+        createMock(AccountLoader.Factory.class);
+    db = createMock(ReviewDb.class);
     final FakeAccountCache accountCache = new FakeAccountCache();
     final PersonIdent serverIdent = new PersonIdent(
         "Gerrit Server", "noreply@gerrit.com", TimeUtil.nowTs(), TZ);
     project = new Project.NameKey("test-project");
-    repoManager = new InMemoryRepositoryManager();
-
-    @SuppressWarnings("unused")
-    InMemoryRepository repo = repoManager.createRepository(project);
-
-    AbstractModule mod = new AbstractModule() {
-      @Override
-      protected void configure() {
-        bind(viewsType).toInstance(views);
-        bind(AccountInfo.Loader.Factory.class).toInstance(alf);
-        bind(ReviewDb.class).toProvider(Providers.<ReviewDb>of(db));
-        bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(config);
-        bind(ProjectCache.class).toProvider(Providers.<ProjectCache> of(null));
-        install(new GitModule());
-        bind(GitRepositoryManager.class).toInstance(repoManager);
-        bind(CapabilityControl.Factory.class)
-            .toProvider(Providers.<CapabilityControl.Factory> of(null));
-        bind(String.class).annotatedWith(AnonymousCowardName.class)
-            .toProvider(AnonymousCowardNameProvider.class);
-        bind(String.class).annotatedWith(CanonicalWebUrl.class)
-            .toInstance("http://localhost:8080/");
-        bind(GroupBackend.class).to(SystemGroupBackend.class).in(SINGLETON);
-        bind(AccountCache.class).toInstance(accountCache);
-        bind(GitReferenceUpdated.class)
-            .toInstance(GitReferenceUpdated.DISABLED);
-        bind(PersonIdent.class).annotatedWith(GerritPersonIdent.class)
-          .toInstance(serverIdent);
-      }
-    };
-
-    injector = Guice.createInjector(mod);
-
-    NotesMigration migration = injector.getInstance(NotesMigration.class);
-    plcUtil = new PatchLineCommentsUtil(migration);
 
     Account co = new Account(new Account.Id(1), TimeUtil.nowTs());
     co.setFullName("Change Owner");
     co.setPreferredEmail("change@owner.com");
     accountCache.put(co);
-    Account.Id ownerId = co.getId();
+    final Account.Id ownerId = co.getId();
 
     Account ou = new Account(new Account.Id(2), TimeUtil.nowTs());
     ou.setFullName("Other Account");
@@ -179,21 +167,62 @@
     accountCache.put(ou);
     Account.Id otherUserId = ou.getId();
 
-    IdentifiedUser.GenericFactory userFactory =
-        injector.getInstance(IdentifiedUser.GenericFactory.class);
+    AbstractModule mod = new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(commentViewsType).toInstance(commentViews);
+        bind(draftViewsType).toInstance(draftViews);
+        bind(AccountLoader.Factory.class).toInstance(alf);
+        bind(ReviewDb.class).toInstance(db);
+        bind(Realm.class).to(FakeRealm.class);
+        bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(config);
+        bind(ProjectCache.class).toProvider(Providers.<ProjectCache> of(null));
+        install(new GitModule());
+        bind(GitRepositoryManager.class).to(InMemoryRepositoryManager.class);
+        bind(InMemoryRepositoryManager.class)
+            .toInstance(new InMemoryRepositoryManager());
+        bind(CapabilityControl.Factory.class)
+            .toProvider(Providers.<CapabilityControl.Factory> of(null));
+        bind(String.class).annotatedWith(AnonymousCowardName.class)
+            .toProvider(AnonymousCowardNameProvider.class);
+        bind(String.class).annotatedWith(CanonicalWebUrl.class)
+            .toInstance("http://localhost:8080/");
+        bind(Boolean.class).annotatedWith(DisableReverseDnsLookup.class)
+            .toInstance(Boolean.FALSE);
+        bind(GroupBackend.class).to(SystemGroupBackend.class).in(SINGLETON);
+        bind(AccountCache.class).toInstance(accountCache);
+        bind(GitReferenceUpdated.class)
+            .toInstance(GitReferenceUpdated.DISABLED);
+        bind(PersonIdent.class).annotatedWith(GerritPersonIdent.class)
+          .toInstance(serverIdent);
+      }
+
+      @Provides
+      @Singleton
+      CurrentUser getCurrentUser(IdentifiedUser.GenericFactory userFactory) {
+        return userFactory.create(ownerId);
+      }
+    };
+
+    injector = Guice.createInjector(mod);
+    injector.injectMembers(this);
+
+    repoManager.createRepository(project);
     changeOwner = userFactory.create(ownerId);
     IdentifiedUser otherUser = userFactory.create(otherUserId);
 
-    AccountInfo.Loader accountLoader = createMock(AccountInfo.Loader.class);
+    AccountLoader accountLoader = createMock(AccountLoader.class);
     accountLoader.fill();
     expectLastCall().anyTimes();
     expect(accountLoader.get(ownerId))
-        .andReturn(new AccountInfo(ownerId)).anyTimes();
+        .andReturn(new AccountInfo(ownerId.get())).anyTimes();
     expect(accountLoader.get(otherUserId))
-        .andReturn(new AccountInfo(otherUserId)).anyTimes();
+        .andReturn(new AccountInfo(otherUserId.get())).anyTimes();
     expect(alf.create(true)).andReturn(accountLoader).anyTimes();
     replay(accountLoader, alf);
 
+    repoManager.createRepository(allUsers.get());
+
     PatchLineCommentAccess plca = createMock(PatchLineCommentAccess.class);
     expect(db.patchComments()).andReturn(plca).anyTimes();
 
@@ -203,7 +232,7 @@
     PatchSet.Id psId2 = new PatchSet.Id(change.getId(), 2);
     PatchSet ps2 = new PatchSet(psId2);
 
-    long timeBase = TimeUtil.nowMs();
+    long timeBase = TimeUtil.roundToSecond(TimeUtil.nowTs()).getTime();
     plc1 = newPatchLineComment(psId1, "Comment1", null,
         "FileOne.txt", Side.REVISION, 3, ownerId, timeBase,
         "First Comment", new CommentRange(1, 2, 3, 4));
@@ -216,69 +245,122 @@
         "FileOne.txt", Side.PARENT, 3, ownerId, timeBase + 2000,
         "First Parent Comment",  new CommentRange(1, 2, 3, 4));
     plc3.setRevId(new RevId("CDEFCDEFCDEFCDEFCDEFCDEFCDEFCDEFCDEFCDEF"));
+    plc4 = newPatchLineComment(psId2, "Comment4", null, "FileOne.txt",
+        Side.REVISION, 3, ownerId, timeBase + 3000, "Second Comment",
+        new CommentRange(1, 2, 3, 4), Status.DRAFT);
+    plc4.setRevId(new RevId("BCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDE"));
+    plc5 = newPatchLineComment(psId2, "Comment5", null, "FileOne.txt",
+        Side.REVISION, 5, ownerId, timeBase + 4000, "Third Comment",
+        new CommentRange(3, 4, 5, 6), Status.DRAFT);
+    plc5.setRevId(new RevId("BCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDE"));
 
     List<PatchLineComment> commentsByOwner = Lists.newArrayList();
     commentsByOwner.add(plc1);
     commentsByOwner.add(plc3);
     List<PatchLineComment> commentsByReviewer = Lists.newArrayList();
     commentsByReviewer.add(plc2);
+    List<PatchLineComment> drafts = Lists.newArrayList();
+    drafts.add(plc4);
+    drafts.add(plc5);
 
     plca.upsert(commentsByOwner);
     expectLastCall().anyTimes();
     plca.upsert(commentsByReviewer);
     expectLastCall().anyTimes();
+    plca.upsert(drafts);
+    expectLastCall().anyTimes();
 
     expect(plca.publishedByPatchSet(psId1))
         .andAnswer(results(plc1, plc2, plc3)).anyTimes();
     expect(plca.publishedByPatchSet(psId2))
         .andAnswer(results()).anyTimes();
+    expect(plca.draftByPatchSetAuthor(psId1, ownerId))
+        .andAnswer(results()).anyTimes();
+    expect(plca.draftByPatchSetAuthor(psId2, ownerId))
+        .andAnswer(results(plc4, plc5)).anyTimes();
+    expect(plca.byChange(change.getId()))
+        .andAnswer(results(plc1, plc2, plc3, plc4, plc5)).anyTimes();
     replay(db, plca);
 
     ChangeUpdate update = newUpdate(change, changeOwner);
     update.setPatchSetId(psId1);
-    plcUtil.addPublishedComments(db, update, commentsByOwner);
+    plcUtil.upsertComments(db, update, commentsByOwner);
     update.commit();
 
     update = newUpdate(change, otherUser);
     update.setPatchSetId(psId1);
-    plcUtil.addPublishedComments(db, update, commentsByReviewer);
+    plcUtil.upsertComments(db, update, commentsByReviewer);
+    update.commit();
+
+    update = newUpdate(change, changeOwner);
+    update.setPatchSetId(psId2);
+    plcUtil.upsertComments(db, update, drafts);
     update.commit();
 
     ChangeControl ctl = stubChangeControl(change);
-    revRes1 = new RevisionResource(new ChangeResource(ctl), ps1);
-    revRes2 = new RevisionResource(new ChangeResource(ctl), ps2);
+    revRes1 = new RevisionResource(new ChangeResource(ctl, null), ps1);
+    revRes2 = new RevisionResource(new ChangeResource(ctl, null), ps2);
   }
 
   private ChangeControl stubChangeControl(Change c) throws OrmException {
-    return TestChanges.stubChangeControl(repoManager, c, changeOwner);
+    return TestChanges.stubChangeControl(
+        repoManager, migration, c, allUsers, changeOwner);
   }
 
   private Change newChange() {
-    return TestChanges.newChange(project, changeOwner);
+    return TestChanges.newChange(project, changeOwner.getAccountId());
   }
 
   private ChangeUpdate newUpdate(Change c, final IdentifiedUser user) throws Exception {
-    return TestChanges.newUpdate(injector, repoManager, c, user);
+    return TestChanges.newUpdate(
+        injector, repoManager, migration, c, allUsers, user);
   }
 
   @Test
   public void testListComments() throws Exception {
     // test ListComments for patch set 1
-    assertListComments(injector, revRes1, ImmutableMap.of(
+    assertListComments(revRes1, ImmutableMap.of(
         "FileOne.txt", Lists.newArrayList(plc3, plc1, plc2)));
 
     // test ListComments for patch set 2
-    assertListComments(injector, revRes2,
+    assertListComments(revRes2,
         Collections.<String, ArrayList<PatchLineComment>>emptyMap());
   }
 
   @Test
   public void testGetComment() throws Exception {
     // test GetComment for existing comment
-    assertGetComment(injector, revRes1, plc1, plc1.getKey().get());
+    assertGetComment(revRes1, plc1, plc1.getKey().get());
 
     // test GetComment for non-existent comment
-    assertGetComment(injector, revRes1, null, "BadComment");
+    assertGetComment(revRes1, null, "BadComment");
+  }
+
+  @Test
+  public void testListDrafts() throws Exception {
+    // test ListDrafts for patch set 1
+    assertListDrafts(revRes1,
+        Collections.<String, ArrayList<PatchLineComment>> emptyMap());
+
+    // test ListDrafts for patch set 2
+    assertListDrafts(revRes2, ImmutableMap.of(
+        "FileOne.txt", Lists.newArrayList(plc4, plc5)));
+  }
+
+  @Test
+  public void testPatchLineCommentsUtilByCommentStatus() throws OrmException {
+    List<PatchLineComment> publishedActual =
+        plcUtil.publishedByChange(db, revRes2.getNotes());
+    List<PatchLineComment> draftActual =
+        plcUtil.draftByChange(db, revRes2.getNotes());
+    List<PatchLineComment> publishedExpected =
+        Lists.newArrayList(plc1, plc2, plc3);
+    List<PatchLineComment> draftExpected =
+        Lists.newArrayList(plc4, plc5);
+    assertEquals(publishedExpected.size(), publishedActual.size());
+    assertEquals(draftExpected.size(), draftActual.size());
+    assertEquals(publishedExpected, publishedActual);
+    assertEquals(draftExpected, draftActual);
   }
 
   private static IAnswer<ResultSet<PatchLineComment>> results(
@@ -290,17 +372,15 @@
       }};
   }
 
-  private static void assertGetComment(Injector inj, RevisionResource res,
-      PatchLineComment expected, String uuid) throws Exception {
-    GetComment getComment = inj.getInstance(GetComment.class);
-    Comments comments = inj.getInstance(Comments.class);
+  private void assertGetComment(RevisionResource res, PatchLineComment expected,
+      String uuid) throws Exception {
     try {
       CommentResource commentRes = comments.parse(res, IdString.fromUrl(uuid));
       if (expected == null) {
         fail("Expected no comment");
       }
       CommentInfo actual = getComment.apply(commentRes);
-      assertComment(expected, actual);
+      assertComment(expected, actual, true);
     } catch (ResourceNotFoundException e) {
       if (expected != null) {
         fail("Expected to find comment");
@@ -308,9 +388,8 @@
     }
   }
 
-  private static void assertListComments(Injector inj, RevisionResource res,
+  private void assertListComments(RevisionResource res,
       Map<String, ArrayList<PatchLineComment>> expected) throws Exception {
-    Comments comments = inj.getInstance(Comments.class);
     RestReadView<RevisionResource> listView =
         (RestReadView<RevisionResource>) comments.list();
     @SuppressWarnings("unchecked")
@@ -325,28 +404,57 @@
       assertNotNull(actualComments);
       assertEquals(expectedComments.size(), actualComments.size());
       for (int i = 0; i < expectedComments.size(); i++) {
-        assertComment(expectedComments.get(i), actualComments.get(i));
+        assertComment(expectedComments.get(i), actualComments.get(i), true);
       }
     }
   }
 
-  private static void assertComment(PatchLineComment plc, CommentInfo ci) {
+  private void assertListDrafts(RevisionResource res,
+      Map<String, ArrayList<PatchLineComment>> expected) throws Exception {
+    RestReadView<RevisionResource> listView =
+        (RestReadView<RevisionResource>) drafts.list();
+    @SuppressWarnings("unchecked")
+    Map<String, List<CommentInfo>> actual =
+        (Map<String, List<CommentInfo>>) listView.apply(res);
+    assertNotNull(actual);
+    assertEquals(expected.size(), actual.size());
+    assertEquals(expected.keySet(), actual.keySet());
+    for (Map.Entry<String, ArrayList<PatchLineComment>> entry : expected.entrySet()) {
+      List<PatchLineComment> expectedComments = entry.getValue();
+      List<CommentInfo> actualComments = actual.get(entry.getKey());
+      assertNotNull(actualComments);
+      assertEquals(expectedComments.size(), actualComments.size());
+      for (int i = 0; i < expectedComments.size(); i++) {
+        assertComment(expectedComments.get(i), actualComments.get(i), false);
+      }
+    }
+  }
+
+  private static void assertComment(PatchLineComment plc, CommentInfo ci,
+      boolean isPublished) {
     assertEquals(plc.getKey().get(), ci.id);
     assertEquals(plc.getParentUuid(), ci.inReplyTo);
     assertEquals(plc.getMessage(), ci.message);
-    assertNotNull(ci.author);
-    assertEquals(plc.getAuthor(), ci.author._id);
+    if (isPublished) {
+      assertNotNull(ci.author);
+      assertEquals(plc.getAuthor(), new Account.Id(ci.author._accountId));
+    }
     assertEquals(plc.getLine(), (int) ci.line);
     assertEquals(plc.getSide() == 0 ? Side.PARENT : Side.REVISION,
-        Objects.firstNonNull(ci.side, Side.REVISION));
-    assertEquals(TimeUtil.roundTimestampToSecond(plc.getWrittenOn()),
-        TimeUtil.roundTimestampToSecond(ci.updated));
-    assertEquals(plc.getRange(), ci.range);
+        MoreObjects.firstNonNull(ci.side, Side.REVISION));
+    assertEquals(TimeUtil.roundToSecond(plc.getWrittenOn()),
+        TimeUtil.roundToSecond(ci.updated));
+    assertEquals(plc.getWrittenOn(), ci.updated);
+    assertEquals(plc.getRange().getStartLine(), ci.range.startLine);
+    assertEquals(plc.getRange().getStartCharacter(), ci.range.startCharacter);
+    assertEquals(plc.getRange().getEndLine(), ci.range.endLine);
+    assertEquals(plc.getRange().getEndCharacter(), ci.range.endCharacter);
   }
 
   private static PatchLineComment newPatchLineComment(PatchSet.Id psId,
       String uuid, String inReplyToUuid, String filename, Side side, int line,
-      Account.Id authorId, long millis, String message, CommentRange range) {
+      Account.Id authorId, long millis, String message, CommentRange range,
+      PatchLineComment.Status status) {
     Patch.Key p = new Patch.Key(psId, filename);
     PatchLineComment.Key id = new PatchLineComment.Key(p, uuid);
     PatchLineComment plc =
@@ -354,8 +462,15 @@
     plc.setMessage(message);
     plc.setRange(range);
     plc.setSide(side == Side.PARENT ? (short) 0 : (short) 1);
-    plc.setStatus(Status.PUBLISHED);
+    plc.setStatus(status);
     plc.setWrittenOn(new Timestamp(millis));
     return plc;
   }
+
+  private static PatchLineComment newPatchLineComment(PatchSet.Id psId,
+      String uuid, String inReplyToUuid, String filename, Side side, int line,
+      Account.Id authorId, long millis, String message, CommentRange range) {
+    return newPatchLineComment(psId, uuid, inReplyToUuid, filename, side, line,
+        authorId, millis, message, range, Status.PUBLISHED);
+  }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java
new file mode 100644
index 0000000..358620f
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java
@@ -0,0 +1,449 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testutil.TestChanges.newChange;
+import static com.google.gerrit.testutil.TestChanges.newPatchSet;
+import static java.util.Collections.singleton;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.changes.FixInput;
+import com.google.gerrit.extensions.common.ProblemInfo;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.InternalUser;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.testutil.FakeAccountByEmailCache;
+import com.google.gerrit.testutil.InMemoryDatabase;
+import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.gerrit.testutil.TestChanges;
+import com.google.inject.util.Providers;
+
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+public class ConsistencyCheckerTest {
+  private InMemoryDatabase schemaFactory;
+  private ReviewDb db;
+  private InMemoryRepositoryManager repoManager;
+  private ConsistencyChecker checker;
+
+  private TestRepository<InMemoryRepository> repo;
+  private Project.NameKey project;
+  private Account.Id userId;
+  private RevCommit tip;
+
+  @Before
+  public void setUp() throws Exception {
+    FakeAccountByEmailCache accountCache = new FakeAccountByEmailCache();
+    schemaFactory = InMemoryDatabase.newDatabase();
+    schemaFactory.create();
+    db = schemaFactory.open();
+    repoManager = new InMemoryRepositoryManager();
+    checker = new ConsistencyChecker(
+        Providers.<ReviewDb> of(db),
+        repoManager,
+        Providers.<CurrentUser> of(new InternalUser(null)),
+        Providers.of(new PersonIdent("server", "noreply@example.com")),
+        new PatchSetInfoFactory(repoManager, accountCache));
+    project = new Project.NameKey("repo");
+    repo = new TestRepository<>(repoManager.createRepository(project));
+    userId = new Account.Id(1);
+    accountCache.putAny(userId);
+    db.accounts().insert(singleton(new Account(userId, TimeUtil.nowTs())));
+    tip = repo.branch("master").commit().create();
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    if (db != null) {
+      db.close();
+    }
+    if (schemaFactory != null) {
+      InMemoryDatabase.drop(schemaFactory);
+    }
+  }
+
+  @Test
+  public void validNewChange() throws Exception {
+    Change c = insertChange();
+    insertPatchSet(c);
+    incrementPatchSet(c);
+    insertPatchSet(c);
+    assertProblems(c);
+  }
+
+  @Test
+  public void validMergedChange() throws Exception {
+    Change c = insertChange();
+    c.setStatus(Change.Status.MERGED);
+    insertPatchSet(c);
+    incrementPatchSet(c);
+
+    incrementPatchSet(c);
+    RevCommit commit2 = repo.branch(c.currentPatchSetId().toRefName()).commit()
+        .parent(tip).create();
+    PatchSet ps2 = newPatchSet(c.currentPatchSetId(), commit2, userId);
+    db.patchSets().insert(singleton(ps2));
+
+    repo.branch(c.getDest().get()).update(commit2);
+    assertProblems(c);
+  }
+
+  @Test
+  public void missingOwner() throws Exception {
+    Change c = newChange(project, new Account.Id(2));
+    db.changes().insert(singleton(c));
+    RevCommit commit = repo.branch(c.currentPatchSetId().toRefName()).commit()
+        .parent(tip).create();
+    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
+    db.patchSets().insert(singleton(ps));
+
+    assertProblems(c, "Missing change owner: 2");
+  }
+
+  @Test
+  public void missingRepo() throws Exception {
+    Change c = newChange(new Project.NameKey("otherproject"), userId);
+    db.changes().insert(singleton(c));
+    insertMissingPatchSet(c, "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    assertProblems(c, "Destination repository not found: otherproject");
+  }
+
+  @Test
+  public void invalidRevision() throws Exception {
+    Change c = insertChange();
+
+    db.patchSets().insert(singleton(newPatchSet(c.currentPatchSetId(),
+            "fooooooooooooooooooooooooooooooooooooooo", userId)));
+    incrementPatchSet(c);
+    insertPatchSet(c);
+
+    assertProblems(c,
+        "Invalid revision on patch set 1:"
+        + " fooooooooooooooooooooooooooooooooooooooo");
+  }
+
+  // No test for ref existing but object missing; InMemoryRepository won't let
+  // us do such a thing.
+
+  @Test
+  public void patchSetObjectAndRefMissing() throws Exception {
+    Change c = insertChange();
+    PatchSet ps = newPatchSet(c.currentPatchSetId(),
+        ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), userId);
+    db.patchSets().insert(singleton(ps));
+
+    assertProblems(c,
+        "Ref missing: " + ps.getId().toRefName(),
+        "Object missing: patch set 1: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+  }
+
+  @Test
+  public void patchSetObjectAndRefMissingWithFix() throws Exception {
+    Change c = insertChange();
+    PatchSet ps = newPatchSet(c.currentPatchSetId(),
+        ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), userId);
+    db.patchSets().insert(singleton(ps));
+
+    String refName = ps.getId().toRefName();
+    List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo("Ref missing: " + refName);
+    assertThat(p.status).isNull();
+  }
+
+  @Test
+  public void patchSetRefMissing() throws Exception {
+    Change c = insertChange();
+    PatchSet ps = insertPatchSet(c);
+    String refName = ps.getId().toRefName();
+    repo.update("refs/other/foo", ObjectId.fromString(ps.getRevision().get()));
+    deleteRef(refName);
+
+    assertProblems(c, "Ref missing: " + refName);
+  }
+
+  @Test
+  public void patchSetRefMissingWithFix() throws Exception {
+    Change c = insertChange();
+    PatchSet ps = insertPatchSet(c);
+    String refName = ps.getId().toRefName();
+    repo.update("refs/other/foo", ObjectId.fromString(ps.getRevision().get()));
+    deleteRef(refName);
+
+    List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo("Ref missing: " + refName);
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
+    assertThat(p.outcome).isEqualTo("Repaired patch set ref");
+
+    assertThat(repo.getRepository().getRef(refName).getObjectId().name())
+        .isEqualTo(ps.getRevision().get());
+  }
+
+  @Test
+  public void patchSetObjectAndRefMissingWithDeletingPatchSet()
+      throws Exception {
+    Change c = insertChange();
+    PatchSet ps1 = insertPatchSet(c);
+    incrementPatchSet(c);
+    PatchSet ps2 = insertMissingPatchSet(c,
+        "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+
+    FixInput fix = new FixInput();
+    fix.deletePatchSetIfCommitMissing = true;
+    List<ProblemInfo> problems = checker.check(c, fix).problems();
+    assertThat(problems).hasSize(2);
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo("Ref missing: " + ps2.getId().toRefName());
+    assertThat(p.status).isNull();
+    p = problems.get(1);
+    assertThat(p.message).isEqualTo(
+        "Object missing: patch set 2: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
+    assertThat(p.outcome).isEqualTo("Deleted patch set");
+
+    c = db.changes().get(c.getId());
+    assertThat(c.currentPatchSetId().get()).isEqualTo(1);
+    assertThat(db.patchSets().get(ps1.getId())).isNotNull();
+    assertThat(db.patchSets().get(ps2.getId())).isNull();
+  }
+
+  @Test
+  public void patchSetMultipleObjectsMissingWithDeletingPatchSets()
+      throws Exception {
+    Change c = insertChange();
+    PatchSet ps1 = insertPatchSet(c);
+
+    incrementPatchSet(c);
+    PatchSet ps2 = insertMissingPatchSet(c,
+        "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+
+    incrementPatchSet(c);
+    PatchSet ps3 = insertPatchSet(c);
+
+    incrementPatchSet(c);
+    PatchSet ps4 = insertMissingPatchSet(c,
+        "c0ffeeeec0ffeeeec0ffeeeec0ffeeeec0ffeeee");
+
+    FixInput fix = new FixInput();
+    fix.deletePatchSetIfCommitMissing = true;
+    List<ProblemInfo> problems = checker.check(c, fix).problems();
+    assertThat(problems).hasSize(4);
+
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo("Ref missing: " + ps4.getId().toRefName());
+    assertThat(p.status).isNull();
+
+    p = problems.get(1);
+    assertThat(p.message).isEqualTo(
+        "Object missing: patch set 4: c0ffeeeec0ffeeeec0ffeeeec0ffeeeec0ffeeee");
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
+    assertThat(p.outcome).isEqualTo("Deleted patch set");
+
+    p = problems.get(2);
+    assertThat(p.message).isEqualTo("Ref missing: " + ps2.getId().toRefName());
+    assertThat(p.status).isNull();
+
+    p = problems.get(3);
+    assertThat(p.message).isEqualTo(
+        "Object missing: patch set 2: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
+    assertThat(p.outcome).isEqualTo("Deleted patch set");
+
+    c = db.changes().get(c.getId());
+    assertThat(c.currentPatchSetId().get()).isEqualTo(3);
+    assertThat(db.patchSets().get(ps1.getId())).isNotNull();
+    assertThat(db.patchSets().get(ps2.getId())).isNull();
+    assertThat(db.patchSets().get(ps3.getId())).isNotNull();
+    assertThat(db.patchSets().get(ps4.getId())).isNull();
+  }
+
+  @Test
+  public void onlyPatchSetObjectMissingWithFix() throws Exception {
+    Change c = insertChange();
+    PatchSet ps1 = insertMissingPatchSet(c,
+        "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+
+    FixInput fix = new FixInput();
+    fix.deletePatchSetIfCommitMissing = true;
+    List<ProblemInfo> problems = checker.check(c, fix).problems();
+    assertThat(problems).hasSize(2);
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo("Ref missing: " + ps1.getId().toRefName());
+    assertThat(p.status).isNull();
+    p = problems.get(1);
+    assertThat(p.message).isEqualTo(
+        "Object missing: patch set 1: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIX_FAILED);
+    assertThat(p.outcome)
+        .isEqualTo("Cannot delete patch set; no patch sets would remain");
+
+    c = db.changes().get(c.getId());
+    assertThat(c.currentPatchSetId().get()).isEqualTo(1);
+    assertThat(db.patchSets().get(ps1.getId())).isNotNull();
+  }
+
+  @Test
+  public void currentPatchSetMissing() throws Exception {
+    Change c = insertChange();
+    assertProblems(c, "Current patch set 1 not found");
+  }
+
+  @Test
+  public void duplicatePatchSetRevisions() throws Exception {
+    Change c = insertChange();
+    PatchSet ps1 = insertPatchSet(c);
+    String rev = ps1.getRevision().get();
+    incrementPatchSet(c);
+    PatchSet ps2 = insertMissingPatchSet(c, rev);
+    updatePatchSetRef(ps2);
+
+    assertProblems(c,
+        "Multiple patch sets pointing to " + rev + ": [1, 2]");
+  }
+
+  @Test
+  public void missingDestRef() throws Exception {
+    RefUpdate ru = repo.getRepository().updateRef("refs/heads/master");
+    ru.setForceUpdate(true);
+    assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+    Change c = insertChange();
+    RevCommit commit = repo.commit().create();
+    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
+    updatePatchSetRef(ps);
+    db.patchSets().insert(singleton(ps));
+
+    assertProblems(c, "Destination ref not found (may be new branch): master");
+  }
+
+  @Test
+  public void mergedChangeIsNotMerged() throws Exception {
+    Change c = insertChange();
+    c.setStatus(Change.Status.MERGED);
+    PatchSet ps = insertPatchSet(c);
+    String rev = ps.getRevision().get();
+
+    assertProblems(c,
+        "Patch set 1 (" + rev + ") is not merged into destination ref"
+        + " master (" + tip.name() + "), but change status is MERGED");
+  }
+
+  @Test
+  public void newChangeIsMerged() throws Exception {
+    Change c = insertChange();
+    RevCommit commit = repo.branch(c.currentPatchSetId().toRefName()).commit()
+        .parent(tip).create();
+    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
+    db.patchSets().insert(singleton(ps));
+    repo.branch(c.getDest().get()).update(commit);
+
+    assertProblems(c,
+        "Patch set 1 (" + commit.name() + ") is merged into destination ref"
+        + " master (" + commit.name() + "), but change status is NEW");
+  }
+
+  @Test
+  public void newChangeIsMergedWithFix() throws Exception {
+    Change c = insertChange();
+    RevCommit commit = repo.branch(c.currentPatchSetId().toRefName()).commit()
+        .parent(tip).create();
+    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
+    db.patchSets().insert(singleton(ps));
+    repo.branch(c.getDest().get()).update(commit);
+
+    List<ProblemInfo> problems = checker.check(c, new FixInput()).problems();
+    assertThat(problems).hasSize(1);
+    ProblemInfo p = problems.get(0);
+    assertThat(p.message).isEqualTo(
+        "Patch set 1 (" + commit.name() + ") is merged into destination ref"
+        + " master (" + commit.name() + "), but change status is NEW");
+    assertThat(p.status).isEqualTo(ProblemInfo.Status.FIXED);
+    assertThat(p.outcome).isEqualTo("Marked change as merged");
+
+    c = db.changes().get(c.getId());
+    assertThat(c.getStatus()).isEqualTo(Change.Status.MERGED);
+    assertProblems(c);
+  }
+
+  private Change insertChange() throws Exception {
+    Change c = newChange(project, userId);
+    db.changes().insert(singleton(c));
+    return c;
+  }
+
+  private void incrementPatchSet(Change c) throws Exception {
+    TestChanges.incrementPatchSet(c);
+    db.changes().upsert(singleton(c));
+  }
+
+  private PatchSet insertPatchSet(Change c) throws Exception {
+    db.changes().upsert(singleton(c));
+    RevCommit commit = repo.branch(c.currentPatchSetId().toRefName()).commit()
+        .parent(tip).create();
+    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
+    updatePatchSetRef(ps);
+    db.patchSets().insert(singleton(ps));
+    return ps;
+  }
+
+  private PatchSet insertMissingPatchSet(Change c, String id) throws Exception {
+    PatchSet ps = newPatchSet(c.currentPatchSetId(),
+        ObjectId.fromString(id), userId);
+    db.patchSets().insert(singleton(ps));
+    return ps;
+  }
+
+  private void updatePatchSetRef(PatchSet ps) throws Exception {
+    repo.update(ps.getId().toRefName(),
+        ObjectId.fromString(ps.getRevision().get()));
+  }
+
+  private void deleteRef(String refName) throws Exception {
+    RefUpdate ru = repo.getRepository().updateRef(refName, true);
+    ru.setForceUpdate(true);
+    assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+  }
+
+  private void assertProblems(Change c, String... expected) {
+    assertThat(Lists.transform(checker.check(c).problems(),
+          new Function<ProblemInfo, String>() {
+            @Override
+            public String apply(ProblemInfo in) {
+              checkArgument(in.status == null,
+                  "Status is not null: " + in.message);
+              return in.message;
+            }
+          })).containsExactly((Object[]) expected);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/HashtagsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/HashtagsTest.java
new file mode 100644
index 0000000..d5b722c
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/HashtagsTest.java
@@ -0,0 +1,104 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Sets;
+
+import org.junit.Test;
+
+public class HashtagsTest {
+  @Test
+  public void emptyCommitMessage() throws Exception {
+    assertThat((Iterable<?>)HashtagsUtil.extractTags("")).isEmpty();
+  }
+
+  @Test
+  public void nullCommitMessage() throws Exception {
+    assertThat((Iterable<?>)HashtagsUtil.extractTags(null)).isEmpty();
+  }
+
+  @Test
+  public void noHashtags() throws Exception {
+    String commitMessage = "Subject\n\nLine 1\n\nLine 2";
+    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage)).isEmpty();
+  }
+
+  @Test
+  public void singleHashtag() throws Exception {
+    String commitMessage = "#Subject\n\nLine 1\n\nLine 2";
+    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+      .containsExactlyElementsIn(Sets.newHashSet("Subject"));
+  }
+
+  @Test
+  public void singleHashtagNumeric() throws Exception {
+    String commitMessage = "Subject\n\n#123\n\nLine 2";
+    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+      .containsExactlyElementsIn(Sets.newHashSet("123"));
+  }
+
+  @Test
+  public void multipleHashtags() throws Exception {
+    String commitMessage = "#Subject\n\n#Hashtag\n\nLine 2";
+    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+      .containsExactlyElementsIn(Sets.newHashSet("Subject", "Hashtag"));
+  }
+
+  @Test
+  public void repeatedHashtag() throws Exception {
+    String commitMessage = "#Subject\n\n#Hashtag1\n\n#Hashtag2\n\n#Hashtag1";
+    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+      .containsExactlyElementsIn(
+          Sets.newHashSet("Subject", "Hashtag1", "Hashtag2"));
+  }
+
+  @Test
+  public void multipleHashtagsNoSpaces() throws Exception {
+    String commitMessage = "Subject\n\n#Hashtag1#Hashtag2";
+    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+      .containsExactlyElementsIn(Sets.newHashSet("Hashtag1"));
+  }
+
+  @Test
+  public void hyphenatedHashtag() throws Exception {
+    String commitMessage = "Subject\n\n#Hyphenated-Hashtag";
+    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+      .containsExactlyElementsIn(Sets.newHashSet("Hyphenated-Hashtag"));
+  }
+
+  @Test
+  public void underscoredHashtag() throws Exception {
+    String commitMessage = "Subject\n\n#Underscored_Hashtag";
+    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+      .containsExactlyElementsIn(Sets.newHashSet("Underscored_Hashtag"));
+  }
+
+  @Test
+  public void hashtagsWithAccentedCharacters() throws Exception {
+    String commitMessage = "Jag #måste #öva på min #Svenska!\n\n"
+        + "Jag behöver en #läkare.";
+    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+      .containsExactlyElementsIn(
+          Sets.newHashSet("måste", "öva", "Svenska", "läkare"));
+  }
+
+  @Test
+  public void hashWithoutHashtag() throws Exception {
+    String commitMessage = "Subject\n\n# Text";
+    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage)).isEmpty();
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java
index 65eede6..4cd31ab 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java
@@ -63,6 +63,7 @@
 
   private RevWalk revWalk;
 
+  @Override
   @Before
   public void setUp() throws Exception {
     super.setUp();
@@ -85,6 +86,8 @@
 
      */
 
+    // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
+    @SuppressWarnings("resource")
     Git git = new Git(db);
     revWalk = new RevWalk(db);
     // Version 1.0
@@ -125,9 +128,10 @@
         .setAnnotated(true).call();
   }
 
+  @Override
   @After
   public void tearDown() throws Exception {
-    revWalk.release();
+    revWalk.close();
     super.tearDown();
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
index cc19811..480efb4 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.config;
 
-import org.junit.Test;
-
 import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.concurrent.TimeUnit.HOURS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -23,6 +21,8 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.junit.Assert.assertEquals;
 
+import org.junit.Test;
+
 import java.util.concurrent.TimeUnit;
 
 public class ConfigUtilTest {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/ScheduleConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/ScheduleConfigTest.java
index e6e15eb..d5f68cc 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/config/ScheduleConfigTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/ScheduleConfigTest.java
@@ -19,14 +19,11 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
 
-import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.joda.time.DateTime;
 import org.junit.Test;
 
-import java.text.MessageFormat;
 import java.util.concurrent.TimeUnit;
 
 public class ScheduleConfigTest {
@@ -59,43 +56,31 @@
   }
 
   @Test
-  public void testCustomKeys() throws ConfigInvalidException {
-    Config rc = readConfig(MessageFormat.format(
-            "[section \"subsection\"]\n{0} = {1}\n{2} = {3}\n",
-            "myStartTime", "01:00", "myInterval", "1h"));
+  public void testCustomKeys() {
+    Config rc = new Config();
+    rc.setString("a", "b", "i", "1h");
+    rc.setString("a", "b", "s", "01:00");
 
-    ScheduleConfig scheduleConfig;
+    ScheduleConfig s = new ScheduleConfig(rc, "a", "b", "i", "s", NOW);
+    assertEquals(ms(1, HOURS), s.getInterval());
+    assertEquals(ms(1, HOURS), s.getInitialDelay());
 
-    scheduleConfig = new ScheduleConfig(rc, "section",
-        "subsection", "myInterval", "myStartTime");
-    assertNotEquals(scheduleConfig.getInterval(), ScheduleConfig.MISSING_CONFIG);
-    assertNotEquals(scheduleConfig.getInitialDelay(), ScheduleConfig.MISSING_CONFIG);
-
-    scheduleConfig = new ScheduleConfig(rc, "section",
-        "subsection", "nonExistent", "myStartTime");
-    assertEquals(scheduleConfig.getInterval(), ScheduleConfig.MISSING_CONFIG);
-    assertEquals(scheduleConfig.getInitialDelay(), ScheduleConfig.MISSING_CONFIG);
+    s = new ScheduleConfig(rc, "a", "b", "myInterval", "myStart", NOW);
+    assertEquals(s.getInterval(), ScheduleConfig.MISSING_CONFIG);
+    assertEquals(s.getInitialDelay(), ScheduleConfig.MISSING_CONFIG);
   }
 
-  private static long initialDelay(String startTime, String interval)
-      throws ConfigInvalidException {
-    return config(startTime, interval).getInitialDelay();
+  private static long initialDelay(String startTime, String interval) {
+    return new ScheduleConfig(
+        config(startTime, interval),
+        "section", "subsection", NOW).getInitialDelay();
   }
 
-  private static ScheduleConfig config(String startTime, String interval)
-      throws ConfigInvalidException {
-    Config rc =
-        readConfig(MessageFormat.format(
-            "[section \"subsection\"]\nstartTime = {0}\ninterval = {1}\n",
-            startTime, interval));
-    return new ScheduleConfig(rc, "section", "subsection", NOW);
-  }
-
-  private static Config readConfig(String dat)
-      throws ConfigInvalidException {
-    Config config = new Config();
-    config.fromText(dat);
-    return config;
+  private static Config config(String startTime, String interval) {
+    Config rc = new Config();
+    rc.setString("section", "subsection", "startTime", startTime);
+    rc.setString("section", "subsection", "interval", interval);
+    return rc;
   }
 
   private static long ms(int cnt, TimeUnit unit) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/edit/ChangeEditTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/edit/ChangeEditTest.java
new file mode 100644
index 0000000..8c963bd
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/edit/ChangeEditTest.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.edit;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RefNames;
+
+import org.junit.Test;
+
+public class ChangeEditTest {
+  @Test
+  public void changeEditRef() throws Exception {
+    Account.Id accountId = new Account.Id(1000042);
+    Change.Id changeId = new Change.Id(56414);
+    PatchSet.Id psId = new PatchSet.Id(changeId, 50);
+    String refName = RefNames.refsEdit(accountId, changeId, psId);
+    assertEquals("refs/users/42/1000042/edit-56414/50", refName);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/events/EventTypesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/events/EventTypesTest.java
new file mode 100644
index 0000000..7eed35f
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/events/EventTypesTest.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.events;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class EventTypesTest {
+  public static class TestEvent extends Event {
+    public TestEvent() {
+      super("test-event");
+    }
+  }
+
+  public static class AnotherTestEvent extends Event {
+    public AnotherTestEvent() {
+      super("another-test-event");
+    }
+  }
+
+  @Test
+  public void testEventTypeRegistration() {
+    EventTypes.registerClass(new TestEvent());
+    EventTypes.registerClass(new AnotherTestEvent());
+    assertThat(EventTypes.getClass("test-event")).isEqualTo(TestEvent.class);
+    assertThat(EventTypes.getClass("another-test-event"))
+      .isEqualTo(AnotherTestEvent.class);
+  }
+
+  @Test
+  public void testGetClassForNonExistingType() {
+    Class<?> clazz = EventTypes.getClass("does-not-exist-event");
+    assertThat(clazz).isNull();
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupListTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupListTest.java
new file mode 100644
index 0000000..e741a91c
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/GroupListTest.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+public class GroupListTest {
+
+  private static final String TEXT =
+      "# UUID                                  \tGroup Name\n" + "#\n"
+          + "d96b998f8a66ff433af50befb975d0e2bb6e0999\tNon-Interactive Users\n"
+          + "ebe31c01aec2c9ac3b3c03e87a47450829ff4310\tAdministrators\n";
+
+  private GroupList groupList;
+
+  @Before
+  public void setup() throws IOException {
+    ValidationError.Sink sink = createNiceMock(ValidationError.Sink.class);
+    replay(sink);
+    groupList = GroupList.parse(TEXT, sink);
+  }
+
+  @Test
+  public void testByUUID() throws Exception {
+    AccountGroup.UUID uuid =
+        new AccountGroup.UUID("d96b998f8a66ff433af50befb975d0e2bb6e0999");
+
+    GroupReference groupReference = groupList.byUUID(uuid);
+
+    assertEquals(uuid, groupReference.getUUID());
+    assertEquals("Non-Interactive Users", groupReference.getName());
+  }
+
+  @Test
+  public void testPut() {
+    AccountGroup.UUID uuid = new AccountGroup.UUID("abc");
+    GroupReference groupReference = new GroupReference(uuid, "Hutzliputz");
+
+    groupList.put(uuid, groupReference);
+
+    assertEquals(3, groupList.references().size());
+    GroupReference found = groupList.byUUID(uuid);
+    assertEquals(groupReference, found);
+  }
+
+  @Test
+  public void testReferences() throws Exception {
+    Collection<GroupReference> result = groupList.references();
+
+    assertEquals(2, result.size());
+    AccountGroup.UUID uuid =
+        new AccountGroup.UUID("ebe31c01aec2c9ac3b3c03e87a47450829ff4310");
+    GroupReference expected = new GroupReference(uuid, "Administrators");
+
+    assertTrue(result.contains(expected));
+  }
+
+  @Test
+  public void testUUIDs() throws Exception {
+    Set<AccountGroup.UUID> result = groupList.uuids();
+
+    assertEquals(2, result.size());
+    AccountGroup.UUID expected =
+        new AccountGroup.UUID("ebe31c01aec2c9ac3b3c03e87a47450829ff4310");
+    assertTrue(result.contains(expected));
+  }
+
+  @Test
+  public void testValidationError() throws Exception {
+    ValidationError.Sink sink = createMock(ValidationError.Sink.class);
+    sink.error(anyObject(ValidationError.class));
+    expectLastCall().times(2);
+    replay(sink);
+    groupList = GroupList.parse(TEXT.replace("\t", "    "), sink);
+    verify(sink);
+  }
+
+  @Test
+  public void testRetainAll() throws Exception {
+    AccountGroup.UUID uuid =
+        new AccountGroup.UUID("d96b998f8a66ff433af50befb975d0e2bb6e0999");
+    groupList.retainUUIDs(Collections.singleton(uuid));
+
+    assertNotNull(groupList.byUUID(uuid));
+    assertNull(groupList.byUUID(new AccountGroup.UUID(
+        "ebe31c01aec2c9ac3b3c03e87a47450829ff4310")));
+  }
+
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java
index 40088e9..78a4c5c 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java
@@ -22,18 +22,18 @@
 import static org.junit.Assert.assertEquals;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
@@ -41,7 +41,6 @@
 import com.google.gerrit.server.git.LabelNormalizer.Result;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.schema.SchemaCreator;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gerrit.testutil.InMemoryDatabase;
 import com.google.gerrit.testutil.InMemoryModule;
 import com.google.inject.Guice;
@@ -112,7 +111,6 @@
         new Change.Id(1), userId,
         new Branch.NameKey(allProjects, "refs/heads/master"),
         TimeUtil.nowTs());
-    ChangeUtil.computeSortKey(change);
     PatchSetInfo ps = new PatchSetInfo(new PatchSet.Id(change.getId(), 1));
     ps.setSubject("Test change");
     change.setCurrentPatchSet(ps);
@@ -139,7 +137,7 @@
 
     PatchSetApproval cr = psa(userId, "Code-Review", 2);
     PatchSetApproval v = psa(userId, "Verified", 1);
-    assertEquals(new Result(
+    assertEquals(Result.create(
           list(v),
           list(copy(cr, 1)),
           list()),
@@ -155,7 +153,7 @@
 
     PatchSetApproval cr = psa(userId, "Code-Review", 5);
     PatchSetApproval v = psa(userId, "Verified", 5);
-    assertEquals(new Result(
+    assertEquals(Result.create(
           list(),
           list(copy(cr, 2), copy(v, 1)),
           list()),
@@ -166,7 +164,7 @@
   public void emptyPermissionRangeOmitsResult() throws Exception {
     PatchSetApproval cr = psa(userId, "Code-Review", 1);
     PatchSetApproval v = psa(userId, "Verified", 1);
-    assertEquals(new Result(
+    assertEquals(Result.create(
           list(),
           list(),
           list(cr, v)),
@@ -181,7 +179,7 @@
 
     PatchSetApproval cr = psa(userId, "Code-Review", 0);
     PatchSetApproval v = psa(userId, "Verified", 0);
-    assertEquals(new Result(
+    assertEquals(Result.create(
           list(cr),
           list(),
           list(v)),
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
index bb109f8..c2f3f6f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -29,7 +30,6 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.SubmoduleSubscriptionAccess;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.client.KeyUtil;
 import com.google.gwtorm.server.ListResultSet;
 import com.google.gwtorm.server.ResultSet;
@@ -118,34 +118,37 @@
   public void testEmptyCommit() throws Exception {
     expect(schemaFactory.open()).andReturn(schema);
 
-    final Repository realDb = createWorkRepository();
-    final Git git = new Git(realDb);
+    try (Repository realDb = createWorkRepository()) {
+      // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
+      @SuppressWarnings("resource")
+      final Git git = new Git(realDb);
 
-    final RevCommit mergeTip = git.commit().setMessage("test").call();
+      final RevCommit mergeTip = git.commit().setMessage("test").call();
 
-    final Branch.NameKey branchNameKey =
-        new Branch.NameKey(new Project.NameKey("test-project"), "test-branch");
+      final Branch.NameKey branchNameKey =
+          new Branch.NameKey(new Project.NameKey("test-project"), "test-branch");
 
-    expect(urlProvider.get()).andReturn("http://localhost:8080");
+      expect(urlProvider.get()).andReturn("http://localhost:8080");
 
-    expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-    final ResultSet<SubmoduleSubscription> emptySubscriptions =
-        new ListResultSet<>(new ArrayList<SubmoduleSubscription>());
-    expect(subscriptions.bySubmodule(branchNameKey)).andReturn(
-        emptySubscriptions);
+      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
+      final ResultSet<SubmoduleSubscription> emptySubscriptions =
+          new ListResultSet<>(new ArrayList<SubmoduleSubscription>());
+      expect(subscriptions.bySubmodule(branchNameKey)).andReturn(
+          emptySubscriptions);
 
-    schema.close();
+      schema.close();
 
-    doReplay();
+      doReplay();
 
-    final SubmoduleOp submoduleOp =
-        new SubmoduleOp(branchNameKey, mergeTip, new RevWalk(realDb), urlProvider,
-            schemaFactory, realDb, null, new ArrayList<Change>(), null, null,
-            null, null, null, null);
+      final SubmoduleOp submoduleOp =
+          new SubmoduleOp(branchNameKey, mergeTip, new RevWalk(realDb), urlProvider,
+              schemaFactory, realDb, null, new ArrayList<Change>(), null, null,
+              null, null, null, null);
 
-    submoduleOp.update();
+      submoduleOp.update();
 
-    doVerify();
+      doVerify();
+    }
   }
 
   /**
@@ -588,85 +591,89 @@
   public void testOneSubscriberToUpdate() throws Exception {
     expect(schemaFactory.open()).andReturn(schema);
 
-    final Repository sourceRepository = createWorkRepository();
-    final Git sourceGit = new Git(sourceRepository);
+    try (Repository sourceRepository = createWorkRepository();
+        Repository targetRepository = createWorkRepository()) {
+      // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
+      @SuppressWarnings("resource")
+      final Git sourceGit = new Git(sourceRepository);
+      // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
+      @SuppressWarnings("resource")
+      final Git targetGit = new Git(targetRepository);
 
-    addRegularFileToIndex("file.txt", "test content", sourceRepository);
+      addRegularFileToIndex("file.txt", "test content", sourceRepository);
 
-    final RevCommit sourceMergeTip =
-        sourceGit.commit().setMessage("test").call();
+      final RevCommit sourceMergeTip =
+          sourceGit.commit().setMessage("test").call();
 
-    final Branch.NameKey sourceBranchNameKey =
-        new Branch.NameKey(new Project.NameKey("source-project"),
-            "refs/heads/master");
+      final Branch.NameKey sourceBranchNameKey =
+          new Branch.NameKey(new Project.NameKey("source-project"),
+              "refs/heads/master");
 
-    final CodeReviewCommit codeReviewCommit =
-        new CodeReviewCommit(sourceMergeTip.toObjectId());
-    final Change submittedChange = new Change(
-        new Change.Key(sourceMergeTip.toObjectId().getName()), new Change.Id(1),
-        new Account.Id(1), sourceBranchNameKey, TimeUtil.nowTs());
+      final CodeReviewCommit codeReviewCommit =
+          new CodeReviewCommit(sourceMergeTip.toObjectId());
+      final Change submittedChange = new Change(
+          new Change.Key(sourceMergeTip.toObjectId().getName()), new Change.Id(1),
+          new Account.Id(1), sourceBranchNameKey, TimeUtil.nowTs());
 
-    final Map<Change.Id, CodeReviewCommit> mergedCommits = new HashMap<>();
-    mergedCommits.put(submittedChange.getId(), codeReviewCommit);
+      final Map<Change.Id, CodeReviewCommit> mergedCommits = new HashMap<>();
+      mergedCommits.put(submittedChange.getId(), codeReviewCommit);
 
-    final List<Change> submitted = new ArrayList<>();
-    submitted.add(submittedChange);
+      final List<Change> submitted = new ArrayList<>();
+      submitted.add(submittedChange);
 
-    final Repository targetRepository = createWorkRepository();
-    final Git targetGit = new Git(targetRepository);
+      addGitLinkToIndex("a", sourceMergeTip.copy(), targetRepository);
 
-    addGitLinkToIndex("a", sourceMergeTip.copy(), targetRepository);
+      targetGit.commit().setMessage("test").call();
 
-    targetGit.commit().setMessage("test").call();
+      final Branch.NameKey targetBranchNameKey =
+          new Branch.NameKey(new Project.NameKey("target-project"),
+              sourceBranchNameKey.get());
 
-    final Branch.NameKey targetBranchNameKey =
-        new Branch.NameKey(new Project.NameKey("target-project"),
-            sourceBranchNameKey.get());
+      expect(urlProvider.get()).andReturn("http://localhost:8080");
 
-    expect(urlProvider.get()).andReturn("http://localhost:8080");
+      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
+      final ResultSet<SubmoduleSubscription> subscribers =
+          new ListResultSet<>(Collections
+              .singletonList(new SubmoduleSubscription(targetBranchNameKey,
+                  sourceBranchNameKey, "source-project")));
+      expect(subscriptions.bySubmodule(sourceBranchNameKey)).andReturn(
+          subscribers);
 
-    expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-    final ResultSet<SubmoduleSubscription> subscribers =
-        new ListResultSet<>(Collections
-            .singletonList(new SubmoduleSubscription(targetBranchNameKey,
-                sourceBranchNameKey, "source-project")));
-    expect(subscriptions.bySubmodule(sourceBranchNameKey)).andReturn(
-        subscribers);
+      expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
+          .andReturn(targetRepository).anyTimes();
 
-    expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
-        .andReturn(targetRepository).anyTimes();
+      Capture<RefUpdate> ruCapture = new Capture<>();
+      gitRefUpdated.fire(eq(targetBranchNameKey.getParentKey()),
+          capture(ruCapture));
+      changeHooks.doRefUpdatedHook(eq(targetBranchNameKey),
+          anyObject(RefUpdate.class), EasyMock.<Account>isNull());
 
-    Capture<RefUpdate> ruCapture = new Capture<>();
-    gitRefUpdated.fire(eq(targetBranchNameKey.getParentKey()),
-        capture(ruCapture));
-    changeHooks.doRefUpdatedHook(eq(targetBranchNameKey),
-        anyObject(RefUpdate.class), EasyMock.<Account>isNull());
+      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
+      final ResultSet<SubmoduleSubscription> emptySubscriptions =
+          new ListResultSet<>(new ArrayList<SubmoduleSubscription>());
+      expect(subscriptions.bySubmodule(targetBranchNameKey)).andReturn(
+          emptySubscriptions);
 
-    expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-    final ResultSet<SubmoduleSubscription> emptySubscriptions =
-        new ListResultSet<>(new ArrayList<SubmoduleSubscription>());
-    expect(subscriptions.bySubmodule(targetBranchNameKey)).andReturn(
-        emptySubscriptions);
+      schema.close();
 
-    schema.close();
+      final PersonIdent myIdent =
+          new PersonIdent("test-user", "test-user@email.com");
 
-    final PersonIdent myIdent =
-        new PersonIdent("test-user", "test-user@email.com");
+      doReplay();
 
-    doReplay();
+      final SubmoduleOp submoduleOp =
+          new SubmoduleOp(sourceBranchNameKey, sourceMergeTip, new RevWalk(
+              sourceRepository), urlProvider, schemaFactory, sourceRepository,
+              new Project(sourceBranchNameKey.getParentKey()), submitted,
+              mergedCommits, myIdent, repoManager, gitRefUpdated, null,
+              changeHooks);
 
-    final SubmoduleOp submoduleOp =
-        new SubmoduleOp(sourceBranchNameKey, sourceMergeTip, new RevWalk(
-            sourceRepository), urlProvider, schemaFactory, sourceRepository,
-            new Project(sourceBranchNameKey.getParentKey()), submitted,
-            mergedCommits, myIdent, repoManager, gitRefUpdated, null,
-            changeHooks);
+      submoduleOp.update();
 
-    submoduleOp.update();
-
-    doVerify();
-    RefUpdate ru = ruCapture.getValue();
-    assertEquals(ru.getName(), targetBranchNameKey.get());
+      doVerify();
+      RefUpdate ru = ruCapture.getValue();
+      assertEquals(ru.getName(), targetBranchNameKey.get());
+    }
   }
 
   /**
@@ -693,86 +700,90 @@
   public void testAvoidingCircularReference() throws Exception {
     expect(schemaFactory.open()).andReturn(schema);
 
-    final Repository sourceRepository = createWorkRepository();
-    final Git sourceGit = new Git(sourceRepository);
+    try (Repository sourceRepository = createWorkRepository();
+        Repository targetRepository = createWorkRepository()) {
+      // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
+      @SuppressWarnings("resource")
+      final Git sourceGit = new Git(sourceRepository);
+      // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
+      @SuppressWarnings("resource")
+      final Git targetGit = new Git(targetRepository);
 
-    addRegularFileToIndex("file.txt", "test content", sourceRepository);
+      addRegularFileToIndex("file.txt", "test content", sourceRepository);
 
-    final RevCommit sourceMergeTip =
-        sourceGit.commit().setMessage("test").call();
+      final RevCommit sourceMergeTip =
+          sourceGit.commit().setMessage("test").call();
 
-    final Branch.NameKey sourceBranchNameKey =
-        new Branch.NameKey(new Project.NameKey("source-project"),
-            "refs/heads/master");
+      final Branch.NameKey sourceBranchNameKey =
+          new Branch.NameKey(new Project.NameKey("source-project"),
+              "refs/heads/master");
 
-    final CodeReviewCommit codeReviewCommit =
-        new CodeReviewCommit(sourceMergeTip.toObjectId());
-    final Change submittedChange = new Change(
-        new Change.Key(sourceMergeTip.toObjectId().getName()), new Change.Id(1),
-        new Account.Id(1), sourceBranchNameKey, TimeUtil.nowTs());
+      final CodeReviewCommit codeReviewCommit =
+          new CodeReviewCommit(sourceMergeTip.toObjectId());
+      final Change submittedChange = new Change(
+          new Change.Key(sourceMergeTip.toObjectId().getName()), new Change.Id(1),
+          new Account.Id(1), sourceBranchNameKey, TimeUtil.nowTs());
 
-    final Map<Change.Id, CodeReviewCommit> mergedCommits = new HashMap<>();
-    mergedCommits.put(submittedChange.getId(), codeReviewCommit);
+      final Map<Change.Id, CodeReviewCommit> mergedCommits = new HashMap<>();
+      mergedCommits.put(submittedChange.getId(), codeReviewCommit);
 
-    final List<Change> submitted = new ArrayList<>();
-    submitted.add(submittedChange);
+      final List<Change> submitted = new ArrayList<>();
+      submitted.add(submittedChange);
 
-    final Repository targetRepository = createWorkRepository();
-    final Git targetGit = new Git(targetRepository);
+      addGitLinkToIndex("a", sourceMergeTip.copy(), targetRepository);
 
-    addGitLinkToIndex("a", sourceMergeTip.copy(), targetRepository);
+      targetGit.commit().setMessage("test").call();
 
-    targetGit.commit().setMessage("test").call();
+      final Branch.NameKey targetBranchNameKey =
+          new Branch.NameKey(new Project.NameKey("target-project"),
+              sourceBranchNameKey.get());
 
-    final Branch.NameKey targetBranchNameKey =
-        new Branch.NameKey(new Project.NameKey("target-project"),
-            sourceBranchNameKey.get());
+      expect(urlProvider.get()).andReturn("http://localhost:8080");
 
-    expect(urlProvider.get()).andReturn("http://localhost:8080");
+      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
+      final ResultSet<SubmoduleSubscription> subscribers =
+          new ListResultSet<>(Collections
+              .singletonList(new SubmoduleSubscription(targetBranchNameKey,
+                  sourceBranchNameKey, "source-project")));
+      expect(subscriptions.bySubmodule(sourceBranchNameKey)).andReturn(
+          subscribers);
 
-    expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-    final ResultSet<SubmoduleSubscription> subscribers =
-        new ListResultSet<>(Collections
-            .singletonList(new SubmoduleSubscription(targetBranchNameKey,
-                sourceBranchNameKey, "source-project")));
-    expect(subscriptions.bySubmodule(sourceBranchNameKey)).andReturn(
-        subscribers);
+      expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
+          .andReturn(targetRepository).anyTimes();
 
-    expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
-        .andReturn(targetRepository).anyTimes();
+      Capture<RefUpdate> ruCapture = new Capture<>();
+      gitRefUpdated.fire(eq(targetBranchNameKey.getParentKey()),
+          capture(ruCapture));
+      changeHooks.doRefUpdatedHook(eq(targetBranchNameKey),
+            anyObject(RefUpdate.class), EasyMock.<Account>isNull());
 
-    Capture<RefUpdate> ruCapture = new Capture<>();
-    gitRefUpdated.fire(eq(targetBranchNameKey.getParentKey()),
-        capture(ruCapture));
-    changeHooks.doRefUpdatedHook(eq(targetBranchNameKey),
-          anyObject(RefUpdate.class), EasyMock.<Account>isNull());
+      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
+      final ResultSet<SubmoduleSubscription> incorrectSubscriptions =
+          new ListResultSet<>(Collections
+              .singletonList(new SubmoduleSubscription(sourceBranchNameKey,
+                  targetBranchNameKey, "target-project")));
+      expect(subscriptions.bySubmodule(targetBranchNameKey)).andReturn(
+          incorrectSubscriptions);
 
-    expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-    final ResultSet<SubmoduleSubscription> incorrectSubscriptions =
-        new ListResultSet<SubmoduleSubscription>(Collections
-            .singletonList(new SubmoduleSubscription(sourceBranchNameKey,
-                targetBranchNameKey, "target-project")));
-    expect(subscriptions.bySubmodule(targetBranchNameKey)).andReturn(
-        incorrectSubscriptions);
+      schema.close();
 
-    schema.close();
+      final PersonIdent myIdent =
+          new PersonIdent("test-user", "test-user@email.com");
 
-    final PersonIdent myIdent =
-        new PersonIdent("test-user", "test-user@email.com");
+      doReplay();
 
-    doReplay();
+      final SubmoduleOp submoduleOp =
+          new SubmoduleOp(sourceBranchNameKey, sourceMergeTip, new RevWalk(
+              sourceRepository), urlProvider, schemaFactory, sourceRepository,
+              new Project(sourceBranchNameKey.getParentKey()), submitted,
+              mergedCommits, myIdent, repoManager, gitRefUpdated, null, changeHooks);
 
-    final SubmoduleOp submoduleOp =
-        new SubmoduleOp(sourceBranchNameKey, sourceMergeTip, new RevWalk(
-            sourceRepository), urlProvider, schemaFactory, sourceRepository,
-            new Project(sourceBranchNameKey.getParentKey()), submitted,
-            mergedCommits, myIdent, repoManager, gitRefUpdated, null, changeHooks);
+      submoduleOp.update();
 
-    submoduleOp.update();
-
-    doVerify();
-    RefUpdate ru = ruCapture.getValue();
-    assertEquals(ru.getName(), targetBranchNameKey.get());
+      doVerify();
+      RefUpdate ru = ruCapture.getValue();
+      assertEquals(ru.getName(), targetBranchNameKey.get());
+    }
   }
 
   /**
@@ -862,67 +873,70 @@
       final List<SubmoduleSubscription> previousSubscriptions) throws Exception {
     expect(schemaFactory.open()).andReturn(schema);
 
-    final Repository realDb = createWorkRepository();
-    final Git git = new Git(realDb);
+    try (Repository realDb = createWorkRepository()) {
+      // TODO(dborowitz): Use try/finally when this doesn't double-close the repo.
+      @SuppressWarnings("resource")
+      final Git git = new Git(realDb);
 
-    addRegularFileToIndex(".gitmodules", gitModulesFileContent, realDb);
+      addRegularFileToIndex(".gitmodules", gitModulesFileContent, realDb);
 
-    final RevCommit mergeTip = git.commit().setMessage("test").call();
+      final RevCommit mergeTip = git.commit().setMessage("test").call();
 
-    expect(urlProvider.get()).andReturn("http://localhost:8080").times(2);
+      expect(urlProvider.get()).andReturn("http://localhost:8080").times(2);
 
-    expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-    expect(subscriptions.bySuperProject(mergedBranch)).andReturn(
-        new ListResultSet<>(previousSubscriptions));
-
-    SortedSet<Project.NameKey> existingProjects = new TreeSet<>();
-
-    for (SubmoduleSubscription extracted : extractedSubscriptions) {
-      existingProjects.add(extracted.getSubmodule().getParentKey());
-    }
-
-    for (int index = 0; index < extractedSubscriptions.size(); index++) {
-      expect(repoManager.list()).andReturn(existingProjects);
-    }
-
-    final Set<SubmoduleSubscription> alreadySubscribeds = new HashSet<>();
-    for (SubmoduleSubscription s : extractedSubscriptions) {
-      if (previousSubscriptions.contains(s)) {
-        alreadySubscribeds.add(s);
-      }
-    }
-
-    final Set<SubmoduleSubscription> subscriptionsToRemove =
-        new HashSet<>(previousSubscriptions);
-    final List<SubmoduleSubscription> subscriptionsToInsert =
-        new ArrayList<>(extractedSubscriptions);
-
-    subscriptionsToRemove.removeAll(subscriptionsToInsert);
-    subscriptionsToInsert.removeAll(alreadySubscribeds);
-
-    if (!subscriptionsToRemove.isEmpty()) {
       expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-      subscriptions.delete(subscriptionsToRemove);
+      expect(subscriptions.bySuperProject(mergedBranch)).andReturn(
+          new ListResultSet<>(previousSubscriptions));
+
+      SortedSet<Project.NameKey> existingProjects = new TreeSet<>();
+
+      for (SubmoduleSubscription extracted : extractedSubscriptions) {
+        existingProjects.add(extracted.getSubmodule().getParentKey());
+      }
+
+      for (int index = 0; index < extractedSubscriptions.size(); index++) {
+        expect(repoManager.list()).andReturn(existingProjects);
+      }
+
+      final Set<SubmoduleSubscription> alreadySubscribeds = new HashSet<>();
+      for (SubmoduleSubscription s : extractedSubscriptions) {
+        if (previousSubscriptions.contains(s)) {
+          alreadySubscribeds.add(s);
+        }
+      }
+
+      final Set<SubmoduleSubscription> subscriptionsToRemove =
+          new HashSet<>(previousSubscriptions);
+      final List<SubmoduleSubscription> subscriptionsToInsert =
+          new ArrayList<>(extractedSubscriptions);
+
+      subscriptionsToRemove.removeAll(subscriptionsToInsert);
+      subscriptionsToInsert.removeAll(alreadySubscribeds);
+
+      if (!subscriptionsToRemove.isEmpty()) {
+        expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
+        subscriptions.delete(subscriptionsToRemove);
+      }
+
+      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
+      subscriptions.insert(subscriptionsToInsert);
+
+      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
+      expect(subscriptions.bySubmodule(mergedBranch)).andReturn(
+          new ListResultSet<>(new ArrayList<SubmoduleSubscription>()));
+
+      schema.close();
+
+      doReplay();
+
+      final SubmoduleOp submoduleOp =
+          new SubmoduleOp(mergedBranch, mergeTip, new RevWalk(realDb),
+              urlProvider, schemaFactory, realDb, new Project(mergedBranch
+                  .getParentKey()), new ArrayList<Change>(), null, null,
+              repoManager, null, null, null);
+
+      submoduleOp.update();
     }
-
-    expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-    subscriptions.insert(subscriptionsToInsert);
-
-    expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
-    expect(subscriptions.bySubmodule(mergedBranch)).andReturn(
-        new ListResultSet<>(new ArrayList<SubmoduleSubscription>()));
-
-    schema.close();
-
-    doReplay();
-
-    final SubmoduleOp submoduleOp =
-        new SubmoduleOp(mergedBranch, mergeTip, new RevWalk(realDb),
-            urlProvider, schemaFactory, realDb, new Project(mergedBranch
-                .getParentKey()), new ArrayList<Change>(), null, null,
-            repoManager, null, null, null);
-
-    submoduleOp.update();
   }
 
   /**
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
index fc0fb32..9b3d5ed 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.index;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -22,14 +23,12 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 
-import java.io.IOException;
-
 class FakeIndex implements ChangeIndex {
-  static Schema<ChangeData> V1 = new Schema<>(1, false,
+  static Schema<ChangeData> V1 = new Schema<>(1,
     ImmutableList.<FieldDef<ChangeData, ?>> of(
       ChangeField.STATUS));
 
-  static Schema<ChangeData> V2 = new Schema<>(2, false,
+  static Schema<ChangeData> V2 = new Schema<>(2,
     ImmutableList.of(
       ChangeField.STATUS,
       ChangeField.PATH,
@@ -70,17 +69,12 @@
   }
 
   @Override
-  public void insert(ChangeData cd) {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
   public void replace(ChangeData cd) {
     throw new UnsupportedOperationException();
   }
 
   @Override
-  public void delete(ChangeData cd) {
+  public void delete(Change.Id id) {
     throw new UnsupportedOperationException();
   }
 
@@ -108,8 +102,4 @@
   public void markReady(boolean ready) {
     throw new UnsupportedOperationException();
   }
-
-  @Override
-  public void delete(int id) throws IOException {
-  }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
index 50e5764..c9a2056 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
@@ -26,9 +26,8 @@
         new FakeQueryBuilder.Definition<>(
           FakeQueryBuilder.class),
         new ChangeQueryBuilder.Arguments(null, null, null, null, null, null,
-          null, null, null, null, null, null, null, null, indexes, null, null,
-          null, null),
-        null);
+          null, null, null, null, null, null, null, null, null, null, null,
+          indexes, null, null, null, null));
   }
 
   @Operator
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 fe66ca5..042459b 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
@@ -54,9 +54,7 @@
     indexes = new IndexCollection();
     indexes.setSearchIndex(index);
     queryBuilder = new FakeQueryBuilder(indexes);
-    rewrite = new IndexRewriteImpl(
-        indexes,
-        new BasicChangeRewrites(null));
+    rewrite = new IndexRewriteImpl(indexes, new BasicChangeRewrites());
   }
 
   @Test
@@ -99,7 +97,7 @@
         parse("-status:abandoned (status:open OR status:merged)");
     assertEquals(
         query(parse("status:new OR status:submitted OR status:draft OR status:merged")),
-        rewrite.rewrite(in, 0));
+        rewrite.rewrite(in, 0, DEFAULT_MAX_QUERY_LIMIT));
   }
 
   @Test
@@ -160,24 +158,26 @@
   }
 
   @Test
-  public void testLimit() throws Exception {
-    Predicate<ChangeData> in = parse("file:a limit:3");
-    Predicate<ChangeData> out = rewrite(in);
+  public void testLimitArgumentOverridesAllLimitPredicates() throws Exception {
+    Predicate<ChangeData> in = parse("limit:1 file:a limit:3");
+    Predicate<ChangeData> out = rewrite(in, 5);
     assertSame(AndSource.class, out.getClass());
     assertEquals(ImmutableList.of(
-          query(in.getChild(0), 3),
-          in.getChild(1)),
+          query(in.getChild(1), 5),
+          parse("limit:5"),
+          parse("limit:5")),
         out.getChildren());
   }
 
   @Test
   public void testStartIncreasesLimit() throws Exception {
+    int n = 3;
     Predicate<ChangeData> f = parse("file:a");
-    Predicate<ChangeData> l = parse("limit:3");
+    Predicate<ChangeData> l = parse("limit:" + n);
     Predicate<ChangeData> in = and(f, l);
-    assertEquals(and(query(f, 3), l), rewrite.rewrite(in, 0));
-    assertEquals(and(query(f, 4), l), rewrite.rewrite(in, 1));
-    assertEquals(and(query(f, 5), l), rewrite.rewrite(in, 2));
+    assertEquals(and(query(f, 3), parse("limit:3")), rewrite.rewrite(in, 0, n));
+    assertEquals(and(query(f, 4), parse("limit:4")), rewrite.rewrite(in, 1, n));
+    assertEquals(and(query(f, 5), parse("limit:5")), rewrite.rewrite(in, 2, n));
   }
 
   @Test
@@ -222,7 +222,12 @@
 
   private Predicate<ChangeData> rewrite(Predicate<ChangeData> in)
       throws QueryParseException {
-    return rewrite.rewrite(in, 0);
+    return rewrite.rewrite(in, 0, DEFAULT_MAX_QUERY_LIMIT);
+  }
+
+  private Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int limit)
+      throws QueryParseException {
+    return rewrite.rewrite(in, 0, limit);
   }
 
   private IndexedChangeQuery query(Predicate<ChangeData> p)
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/BasicSerializationTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/BasicSerializationTest.java
index 622b31e..dbc8f02 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/BasicSerializationTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/BasicSerializationTest.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.ioutil;
 
-import org.junit.Test;
-
 import static com.google.gerrit.server.ioutil.BasicSerialization.readFixInt64;
 import static com.google.gerrit.server.ioutil.BasicSerialization.readString;
 import static com.google.gerrit.server.ioutil.BasicSerialization.readVarInt32;
@@ -25,6 +23,8 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 
+import org.junit.Test;
+
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -137,8 +137,9 @@
   private static void assertOutput(final byte[] expect,
       final ByteArrayOutputStream out) {
     final byte[] buf = out.toByteArray();
-    for (int i = 0; i < expect.length; i++)
+    for (int i = 0; i < expect.length; i++) {
       assertEquals(expect[i], buf[i]);
+    }
   }
 
   private static InputStream r(final byte[] buf) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java
index 625e4b6..c77e0f6 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java
@@ -14,14 +14,14 @@
 
 package com.google.gerrit.server.mail;
 
-import org.junit.Test;
-
-import java.io.UnsupportedEncodingException;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.fail;
 
+import org.junit.Test;
+
+import java.io.UnsupportedEncodingException;
+
 public class AddressTest {
   @Test
   public void testParse_NameEmail1() {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
index 4f20b63..f33f720 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
@@ -23,12 +23,12 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.util.TimeUtil;
 
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
new file mode 100644
index 0000000..1308723
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -0,0 +1,239 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static com.google.inject.Scopes.SINGLETON;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.CommentRange;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.FakeRealm;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.config.AllUsersNameProvider;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.AnonymousCowardNameProvider;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.DisableReverseDnsLookup;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitModule;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.testutil.FakeAccountCache;
+import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.gerrit.testutil.TestChanges;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.StandardKeyEncoder;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.util.Providers;
+
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeUtils;
+import org.joda.time.DateTimeUtils.MillisProvider;
+import org.junit.After;
+import org.junit.Before;
+
+import java.sql.Timestamp;
+import java.util.TimeZone;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class AbstractChangeNotesTest {
+  private static final TimeZone TZ =
+      TimeZone.getTimeZone("America/Los_Angeles");
+
+  private static final NotesMigration MIGRATION = NotesMigration.allEnabled();
+
+  protected Account.Id otherUserId;
+  protected FakeAccountCache accountCache;
+  protected IdentifiedUser changeOwner;
+  protected IdentifiedUser otherUser;
+  protected InMemoryRepository repo;
+  protected InMemoryRepositoryManager repoManager;
+  protected PersonIdent serverIdent;
+  protected Project.NameKey project;
+
+  @Inject protected IdentifiedUser.GenericFactory userFactory;
+
+  private Injector injector;
+  private String systemTimeZone;
+  private volatile long clockStepMs;
+
+  @Inject private AllUsersNameProvider allUsers;
+
+  @Before
+  public void setUp() throws Exception {
+    setTimeForTesting();
+    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
+
+    serverIdent = new PersonIdent(
+        "Gerrit Server", "noreply@gerrit.com", TimeUtil.nowTs(), TZ);
+    project = new Project.NameKey("test-project");
+    repoManager = new InMemoryRepositoryManager();
+    repo = repoManager.createRepository(project);
+    accountCache = new FakeAccountCache();
+    Account co = new Account(new Account.Id(1), TimeUtil.nowTs());
+    co.setFullName("Change Owner");
+    co.setPreferredEmail("change@owner.com");
+    accountCache.put(co);
+    Account ou = new Account(new Account.Id(2), TimeUtil.nowTs());
+    ou.setFullName("Other Account");
+    ou.setPreferredEmail("other@account.com");
+    accountCache.put(ou);
+
+    injector = Guice.createInjector(new FactoryModule() {
+      @Override
+      public void configure() {
+        install(new GitModule());
+        bind(NotesMigration.class).toInstance(MIGRATION);
+        bind(GitRepositoryManager.class).toInstance(repoManager);
+        bind(ProjectCache.class).toProvider(Providers.<ProjectCache> of(null));
+        bind(CapabilityControl.Factory.class)
+            .toProvider(Providers.<CapabilityControl.Factory> of(null));
+        bind(Config.class).annotatedWith(GerritServerConfig.class)
+            .toInstance(new Config());
+        bind(String.class).annotatedWith(AnonymousCowardName.class)
+            .toProvider(AnonymousCowardNameProvider.class);
+        bind(String.class).annotatedWith(CanonicalWebUrl.class)
+            .toInstance("http://localhost:8080/");
+        bind(Boolean.class).annotatedWith(DisableReverseDnsLookup.class)
+            .toInstance(Boolean.FALSE);
+        bind(Realm.class).to(FakeRealm.class);
+        bind(GroupBackend.class).to(SystemGroupBackend.class).in(SINGLETON);
+        bind(AccountCache.class).toInstance(accountCache);
+        bind(PersonIdent.class).annotatedWith(GerritPersonIdent.class)
+            .toInstance(serverIdent);
+        bind(GitReferenceUpdated.class)
+            .toInstance(GitReferenceUpdated.DISABLED);
+      }
+    });
+
+    injector.injectMembers(this);
+    repoManager.createRepository(allUsers.get());
+    changeOwner = userFactory.create(co.getId());
+    otherUser = userFactory.create(ou.getId());
+    otherUserId = otherUser.getAccountId();
+  }
+
+  private void setTimeForTesting() {
+    systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
+    clockStepMs = MILLISECONDS.convert(1, SECONDS);
+    final AtomicLong clockMs = new AtomicLong(
+        new DateTime(2009, 9, 30, 17, 0, 0).getMillis());
+
+    DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
+      @Override
+      public long getMillis() {
+        return clockMs.getAndAdd(clockStepMs);
+      }
+    });
+  }
+
+  @After
+  public void resetTime() {
+    DateTimeUtils.setCurrentMillisSystem();
+    System.setProperty("user.timezone", systemTimeZone);
+  }
+
+  protected Change newChange() {
+    return TestChanges.newChange(project, changeOwner.getAccountId());
+  }
+
+  protected ChangeUpdate newUpdate(Change c, IdentifiedUser user)
+      throws OrmException {
+    return TestChanges.newUpdate(
+        injector, repoManager, MIGRATION, c, allUsers, user);
+  }
+
+  protected ChangeNotes newNotes(Change c) throws OrmException {
+    return new ChangeNotes(repoManager, MIGRATION, allUsers, c).load();
+  }
+
+  protected static SubmitRecord submitRecord(String status,
+      String errorMessage, SubmitRecord.Label... labels) {
+    SubmitRecord rec = new SubmitRecord();
+    rec.status = SubmitRecord.Status.valueOf(status);
+    rec.errorMessage = errorMessage;
+    if (labels.length > 0) {
+      rec.labels = ImmutableList.copyOf(labels);
+    }
+    return rec;
+  }
+
+  protected static SubmitRecord.Label submitLabel(String name, String status,
+      Account.Id appliedBy) {
+    SubmitRecord.Label label = new SubmitRecord.Label();
+    label.label = name;
+    label.status = SubmitRecord.Label.Status.valueOf(status);
+    label.appliedBy = appliedBy;
+    return label;
+  }
+
+  protected PatchLineComment newPublishedPatchLineComment(PatchSet.Id psId,
+      String filename, String UUID, CommentRange range, int line,
+      IdentifiedUser commenter, String parentUUID, Timestamp t,
+      String message, short side, String commitSHA1) {
+    return newPatchLineComment(psId, filename, UUID, range, line, commenter,
+        parentUUID, t, message, side, commitSHA1,
+        PatchLineComment.Status.PUBLISHED);
+  }
+
+  protected PatchLineComment newPatchLineComment(PatchSet.Id psId,
+      String filename, String UUID, CommentRange range, int line,
+      IdentifiedUser commenter, String parentUUID, Timestamp t,
+      String message, short side, String commitSHA1,
+      PatchLineComment.Status status) {
+    PatchLineComment comment = new PatchLineComment(
+        new PatchLineComment.Key(
+            new Patch.Key(psId, filename), UUID),
+        line, commenter.getAccountId(), parentUUID, t);
+    comment.setSide(side);
+    comment.setMessage(message);
+    comment.setRange(range);
+    comment.setRevId(new RevId(commitSHA1));
+    comment.setStatus(status);
+    return comment;
+  }
+
+  protected static Timestamp truncate(Timestamp ts) {
+    return new Timestamp((ts.getTime() / 1000) * 1000);
+  }
+
+  protected static Timestamp after(Change c, long millis) {
+    return new Timestamp(c.getCreatedOn().getTime() + millis);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
new file mode 100644
index 0000000..c41c4ec
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -0,0 +1,215 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static org.junit.Assert.fail;
+
+import com.google.gerrit.common.TimeUtil;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ChangeNotesParserTest extends AbstractChangeNotesTest {
+  private TestRepository<InMemoryRepository> testRepo;
+  private RevWalk walk;
+
+  @Before
+  public void setUpTestRepo() throws Exception {
+    testRepo = new TestRepository<>(repo);
+    walk = new RevWalk(repo);
+  }
+
+  @After
+  public void tearDownTestRepo() throws Exception {
+    walk.close();
+  }
+
+  @Test
+  public void parseAuthor() throws Exception {
+    assertParseSucceeds("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n");
+    assertParseFails(writeCommit("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n",
+        new PersonIdent("Change Owner", "owner@example.com",
+          serverIdent.getWhen(), serverIdent.getTimeZone())));
+    assertParseFails(writeCommit("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n",
+        new PersonIdent("Change Owner", "x@gerrit",
+          serverIdent.getWhen(), serverIdent.getTimeZone())));
+  }
+
+  @Test
+  public void parseStatus() throws Exception {
+    assertParseSucceeds("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n"
+        + "Status: NEW\n");
+    assertParseSucceeds("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n"
+        + "Status: new\n");
+    assertParseFails("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n"
+        + "Status: OOPS\n");
+    assertParseFails("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n"
+        + "Status: NEW\n"
+        + "Status: NEW\n");
+  }
+
+  @Test
+  public void parsePatchSetId() throws Exception {
+    assertParseSucceeds("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n");
+    assertParseFails("Update change\n"
+        + "\n");
+    assertParseFails("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n"
+        + "Patch-Set: 1\n");
+    assertParseSucceeds("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n");
+    assertParseFails("Update change\n"
+        + "\n"
+        + "Patch-Set: x\n");
+  }
+
+  @Test
+  public void parseApproval() throws Exception {
+    assertParseSucceeds("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n"
+        + "Label: Label1=+1\n"
+        + "Label: Label2=1\n"
+        + "Label: Label3=0\n"
+        + "Label: Label4=-1\n");
+    assertParseFails("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n"
+        + "Label: Label1=X\n");
+    assertParseFails("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n"
+        + "Label: Label1 = 1\n");
+    assertParseFails("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n"
+        + "Label: X+Y\n");
+  }
+
+  @Test
+  public void parseSubmitRecords() throws Exception {
+    assertParseSucceeds("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n"
+        + "Submitted-with: NOT_READY\n"
+        + "Submitted-with: OK: Verified: Change Owner <1@gerrit>\n"
+        + "Submitted-with: NEED: Code-Review\n"
+        + "Submitted-with: NOT_READY\n"
+        + "Submitted-with: OK: Verified: Change Owner <1@gerrit>\n"
+        + "Submitted-with: NEED: Alternative-Code-Review\n");
+    assertParseFails("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n"
+        + "Submitted-with: OOPS\n");
+    assertParseFails("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n"
+        + "Submitted-with: NEED: X+Y\n");
+    assertParseFails("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n"
+        + "Submitted-with: OK: X+Y: Change Owner <1@gerrit>\n");
+    assertParseFails("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n"
+        + "Submitted-with: OK: Code-Review: 1@gerrit\n");
+  }
+
+  @Test
+  public void parseReviewer() throws Exception {
+    assertParseSucceeds("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n"
+        + "Reviewer: Change Owner <1@gerrit>\n"
+        + "CC: Other Account <2@gerrit>\n");
+    assertParseFails("Update change\n"
+        + "\n"
+        + "Patch-Set: 1\n"
+        + "Reviewer: 1@gerrit\n");
+  }
+
+  private RevCommit writeCommit(String body) throws Exception {
+    return writeCommit(body, ChangeNoteUtil.newIdent(
+        changeOwner.getAccount(), TimeUtil.nowTs(), serverIdent,
+        "Anonymous Coward"));
+  }
+
+  private RevCommit writeCommit(String body, PersonIdent author)
+      throws Exception {
+    try (ObjectInserter ins = testRepo.getRepository().newObjectInserter()) {
+      CommitBuilder cb = new CommitBuilder();
+      cb.setAuthor(author);
+      cb.setCommitter(new PersonIdent(serverIdent, author.getWhen()));
+      cb.setTreeId(testRepo.tree());
+      cb.setMessage(body);
+      ObjectId id = ins.insert(cb);
+      ins.flush();
+      RevCommit commit = walk.parseCommit(id);
+      walk.parseBody(commit);
+      return commit;
+    }
+  }
+
+  private void assertParseSucceeds(String body) throws Exception {
+    try (ChangeNotesParser parser = newParser(writeCommit(body))) {
+      parser.parseAll();
+    }
+  }
+
+  private void assertParseFails(String body) throws Exception {
+    assertParseFails(writeCommit(body));
+  }
+
+  private void assertParseFails(RevCommit commit) throws Exception {
+    try (ChangeNotesParser parser = newParser(commit)) {
+      parser.parseAll();
+      fail("Expected parse to fail:\n" + commit.getFullMessage());
+    } catch (ConfigInvalidException e) {
+      // Expected.
+    }
+  }
+
+  private ChangeNotesParser newParser(ObjectId tip) throws Exception {
+    return new ChangeNotesParser(newChange(), tip, walk, repoManager);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 29d2449..aea966a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -17,330 +17,51 @@
 import static com.google.gerrit.server.notedb.ReviewerState.CC;
 import static com.google.gerrit.server.notedb.ReviewerState.REVIEWER;
 import static com.google.gerrit.testutil.TestChanges.incrementPatchSet;
-import static com.google.inject.Scopes.SINGLETON;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.CommentRange;
-import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.CapabilityControl;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.account.Realm;
-import com.google.gerrit.server.config.AnonymousCowardName;
-import com.google.gerrit.server.config.AnonymousCowardNameProvider;
-import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.config.FactoryModule;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitModule;
-import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.notedb.CommentsInNotesUtil;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.util.TimeUtil;
-import com.google.gerrit.testutil.TestChanges;
-import com.google.gerrit.testutil.FakeAccountCache;
-import com.google.gerrit.testutil.FakeRealm;
-import com.google.gerrit.testutil.InMemoryRepositoryManager;
-import com.google.gwtorm.client.KeyUtil;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.StandardKeyEncoder;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-import com.google.inject.util.Providers;
 
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.notes.Note;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeUtils;
-import org.joda.time.DateTimeUtils.MillisProvider;
-import org.junit.After;
-import org.junit.Before;
+import org.eclipse.jgit.transport.ReceiveCommand;
 import org.junit.Test;
 
+import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
-import java.util.Date;
+import java.util.LinkedHashSet;
 import java.util.List;
-import java.util.TimeZone;
-import java.util.concurrent.atomic.AtomicLong;
 
-public class ChangeNotesTest {
-  private static final TimeZone TZ =
-      TimeZone.getTimeZone("America/Los_Angeles");
-
-  private PersonIdent serverIdent;
-  private Project.NameKey project;
-  private InMemoryRepositoryManager repoManager;
-  private InMemoryRepository repo;
-  private FakeAccountCache accountCache;
-  private IdentifiedUser changeOwner;
-  private IdentifiedUser otherUser;
-  private Injector injector;
-  private String systemTimeZone;
-  private volatile long clockStepMs;
-
-  @Before
-  public void setUp() throws Exception {
-    setTimeForTesting();
-    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
-
-    serverIdent = new PersonIdent(
-        "Gerrit Server", "noreply@gerrit.com", TimeUtil.nowTs(), TZ);
-    project = new Project.NameKey("test-project");
-    repoManager = new InMemoryRepositoryManager();
-    repo = repoManager.createRepository(project);
-    accountCache = new FakeAccountCache();
-    Account co = new Account(new Account.Id(1), TimeUtil.nowTs());
-    co.setFullName("Change Owner");
-    co.setPreferredEmail("change@owner.com");
-    accountCache.put(co);
-    Account ou = new Account(new Account.Id(2), TimeUtil.nowTs());
-    ou.setFullName("Other Account");
-    ou.setPreferredEmail("other@account.com");
-    accountCache.put(ou);
-
-    injector = Guice.createInjector(new FactoryModule() {
-      @Override
-      public void configure() {
-        install(new GitModule());
-        bind(NotesMigration.class).toInstance(NotesMigration.allEnabled());
-        bind(GitRepositoryManager.class).toInstance(repoManager);
-        bind(ProjectCache.class).toProvider(Providers.<ProjectCache> of(null));
-        bind(CapabilityControl.Factory.class)
-            .toProvider(Providers.<CapabilityControl.Factory> of(null));
-        bind(Config.class).annotatedWith(GerritServerConfig.class)
-            .toInstance(new Config());
-        bind(String.class).annotatedWith(AnonymousCowardName.class)
-            .toProvider(AnonymousCowardNameProvider.class);
-        bind(String.class).annotatedWith(CanonicalWebUrl.class)
-            .toInstance("http://localhost:8080/");
-        bind(Realm.class).to(FakeRealm.class);
-        bind(GroupBackend.class).to(SystemGroupBackend.class).in(SINGLETON);
-        bind(AccountCache.class).toInstance(accountCache);
-        bind(PersonIdent.class).annotatedWith(GerritPersonIdent.class)
-            .toInstance(serverIdent);
-        bind(GitReferenceUpdated.class)
-            .toInstance(GitReferenceUpdated.DISABLED);
-      }
-    });
-
-    IdentifiedUser.GenericFactory userFactory =
-        injector.getInstance(IdentifiedUser.GenericFactory.class);
-    changeOwner = userFactory.create(co.getId());
-    otherUser = userFactory.create(ou.getId());
-  }
-
-  private void setTimeForTesting() {
-    systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
-    clockStepMs = MILLISECONDS.convert(1, SECONDS);
-    final AtomicLong clockMs = new AtomicLong(
-        new DateTime(2009, 9, 30, 17, 0, 0).getMillis());
-
-    DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
-      @Override
-      public long getMillis() {
-        return clockMs.getAndAdd(clockStepMs);
-      }
-    });
-  }
-
-  @After
-  public void resetTime() {
-    DateTimeUtils.setCurrentMillisSystem();
-    System.setProperty("user.timezone", systemTimeZone);
-  }
-
-  @Test
-  public void approvalsCommitFormatSimple() throws Exception {
-    Change c = newChange();
-    ChangeUpdate update = newUpdate(c, changeOwner);
-    update.putApproval("Verified", (short) 1);
-    update.putApproval("Code-Review", (short) -1);
-    update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
-    update.putReviewer(otherUser.getAccount().getId(), CC);
-    update.commit();
-    assertEquals("refs/changes/01/1/meta", update.getRefName());
-
-    RevWalk walk = new RevWalk(repo);
-    try {
-      RevCommit commit = walk.parseCommit(update.getRevision());
-      walk.parseBody(commit);
-      assertEquals("Update patch set 1\n"
-          + "\n"
-          + "Patch-set: 1\n"
-          + "Reviewer: Change Owner <1@gerrit>\n"
-          + "CC: Other Account <2@gerrit>\n"
-          + "Label: Code-Review=-1\n"
-          + "Label: Verified=+1\n",
-          commit.getFullMessage());
-
-      PersonIdent author = commit.getAuthorIdent();
-      assertEquals("Change Owner", author.getName());
-      assertEquals("1@gerrit", author.getEmailAddress());
-      assertEquals(new Date(c.getCreatedOn().getTime() + 1000),
-          author.getWhen());
-      assertEquals(TimeZone.getTimeZone("GMT-7:00"), author.getTimeZone());
-
-      PersonIdent committer = commit.getCommitterIdent();
-      assertEquals("Gerrit Server", committer.getName());
-      assertEquals("noreply@gerrit.com", committer.getEmailAddress());
-      assertEquals(author.getWhen(), committer.getWhen());
-      assertEquals(author.getTimeZone(), committer.getTimeZone());
-    } finally {
-      walk.release();
-    }
-  }
-
-  @Test
-  public void changeMessageCommitFormatSimple() throws Exception {
-    Change c = newChange();
-    ChangeUpdate update = newUpdate(c, changeOwner);
-    update.setChangeMessage("Just a little code change.\n"
-        + "How about a new line");
-    update.commit();
-    assertEquals("refs/changes/01/1/meta", update.getRefName());
-
-    RevWalk walk = new RevWalk(repo);
-    try {
-      RevCommit commit = walk.parseCommit(update.getRevision());
-      walk.parseBody(commit);
-      assertEquals("Update patch set 1\n"
-          + "\n"
-          + "Just a little code change.\n"
-          + "How about a new line\n"
-          + "\n"
-          + "Patch-set: 1\n",
-          commit.getFullMessage());
-    } finally {
-      walk.release();
-    }
-  }
-
-  @Test
-  public void approvalTombstoneCommitFormat() throws Exception {
-    Change c = newChange();
-    ChangeUpdate update = newUpdate(c, changeOwner);
-    update.removeApproval("Code-Review");
-    update.commit();
-
-    RevWalk walk = new RevWalk(repo);
-    try {
-      RevCommit commit = walk.parseCommit(update.getRevision());
-      walk.parseBody(commit);
-      assertEquals("Update patch set 1\n"
-          + "\n"
-          + "Patch-set: 1\n"
-          + "Label: -Code-Review\n",
-          commit.getFullMessage());
-    } finally {
-      walk.release();
-    }
-  }
-
-  @Test
-  public void submitCommitFormat() throws Exception {
-    Change c = newChange();
-    ChangeUpdate update = newUpdate(c, changeOwner);
-    update.setSubject("Submit patch set 1");
-
-    update.submit(ImmutableList.of(
-        submitRecord("NOT_READY", null,
-          submitLabel("Verified", "OK", changeOwner.getAccountId()),
-          submitLabel("Code-Review", "NEED", null)),
-        submitRecord("NOT_READY", null,
-          submitLabel("Verified", "OK", changeOwner.getAccountId()),
-          submitLabel("Alternative-Code-Review", "NEED", null))));
-    update.commit();
-
-    RevWalk walk = new RevWalk(repo);
-    try {
-      RevCommit commit = walk.parseCommit(update.getRevision());
-      walk.parseBody(commit);
-      assertEquals("Submit patch set 1\n"
-          + "\n"
-          + "Patch-set: 1\n"
-          + "Status: submitted\n"
-          + "Submitted-with: NOT_READY\n"
-          + "Submitted-with: OK: Verified: Change Owner <1@gerrit>\n"
-          + "Submitted-with: NEED: Code-Review\n"
-          + "Submitted-with: NOT_READY\n"
-          + "Submitted-with: OK: Verified: Change Owner <1@gerrit>\n"
-          + "Submitted-with: NEED: Alternative-Code-Review\n",
-          commit.getFullMessage());
-
-      PersonIdent author = commit.getAuthorIdent();
-      assertEquals("Change Owner", author.getName());
-      assertEquals("1@gerrit", author.getEmailAddress());
-      assertEquals(new Date(c.getCreatedOn().getTime() + 1000),
-          author.getWhen());
-      assertEquals(TimeZone.getTimeZone("GMT-7:00"), author.getTimeZone());
-
-      PersonIdent committer = commit.getCommitterIdent();
-      assertEquals("Gerrit Server", committer.getName());
-      assertEquals("noreply@gerrit.com", committer.getEmailAddress());
-      assertEquals(author.getWhen(), committer.getWhen());
-      assertEquals(author.getTimeZone(), committer.getTimeZone());
-    } finally {
-      walk.release();
-    }
-  }
-
-  @Test
-  public void submitWithErrorMessage() throws Exception {
-    Change c = newChange();
-    ChangeUpdate update = newUpdate(c, changeOwner);
-    update.setSubject("Submit patch set 1");
-
-    update.submit(ImmutableList.of(
-        submitRecord("RULE_ERROR", "Problem with patch set:\n1")));
-    update.commit();
-
-    RevWalk walk = new RevWalk(repo);
-    try {
-      RevCommit commit = walk.parseCommit(update.getRevision());
-      walk.parseBody(commit);
-      assertEquals("Submit patch set 1\n"
-          + "\n"
-          + "Patch-set: 1\n"
-          + "Status: submitted\n"
-          + "Submitted-with: RULE_ERROR Problem with patch set: 1\n",
-          commit.getFullMessage());
-    } finally {
-      walk.release();
-    }
-  }
-
+public class ChangeNotesTest extends AbstractChangeNotesTest {
   @Test
   public void approvalsOnePatchSet() throws Exception {
     Change c = newChange();
@@ -612,6 +333,51 @@
   }
 
   @Test
+  public void emptyChangeUpdate() throws Exception {
+    ChangeUpdate update = newUpdate(newChange(), changeOwner);
+    update.commit();
+    assertNull(update.getRevision());
+  }
+
+  @Test
+  public void hashtagCommit() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    LinkedHashSet<String> hashtags = new LinkedHashSet<>();
+    hashtags.add("tag1");
+    hashtags.add("tag2");
+    update.setHashtags(hashtags);
+    update.commit();
+    try (RevWalk walk = new RevWalk(repo)) {
+      RevCommit commit = walk.parseCommit(update.getRevision());
+      walk.parseBody(commit);
+      assertTrue(commit.getFullMessage().endsWith("Hashtags: tag1,tag2\n"));
+    }
+  }
+
+  @Test
+  public void hashtagChangeNotes() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    LinkedHashSet<String> hashtags = new LinkedHashSet<>();
+    hashtags.add("tag1");
+    hashtags.add("tag2");
+    update.setHashtags(hashtags);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    assertEquals(hashtags, notes.getHashtags());
+  }
+
+  @Test
+  public void emptyExceptSubject() throws Exception {
+    ChangeUpdate update = newUpdate(newChange(), changeOwner);
+    update.setSubject("Create change");
+    update.commit();
+    assertNotNull(update.getRevision());
+  }
+
+  @Test
   public void multipleUpdatesInBatch() throws Exception {
     Change c = newChange();
     ChangeUpdate update1 = newUpdate(c, changeOwner);
@@ -644,6 +410,109 @@
   }
 
   @Test
+  public void multipleUpdatesIncludingComments() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update1 = newUpdate(c, otherUser);
+    String uuid1 = "uuid1";
+    String message1 = "comment 1";
+    CommentRange range1 = new CommentRange(1, 1, 2, 1);
+    Timestamp time1 = TimeUtil.nowTs();
+    PatchSet.Id psId = c.currentPatchSetId();
+    BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
+    BatchMetaDataUpdate batch = update1.openUpdateInBatch(bru);
+    PatchLineComment comment1 = newPublishedPatchLineComment(psId, "file1",
+        uuid1, range1, range1.getEndLine(), otherUser, null, time1, message1,
+        (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+    update1.setPatchSetId(psId);
+    update1.upsertComment(comment1);
+    update1.writeCommit(batch);
+    ChangeUpdate update2 = newUpdate(c, otherUser);
+    update2.putApproval("Code-Review", (short) 2);
+    update2.writeCommit(batch);
+
+    try (RevWalk rw = new RevWalk(repo)) {
+      batch.commit();
+      bru.execute(rw, NullProgressMonitor.INSTANCE);
+
+      ChangeNotes notes = newNotes(c);
+      ObjectId tip = notes.getRevision();
+      RevCommit commitWithApprovals = rw.parseCommit(tip);
+      assertNotNull(commitWithApprovals);
+      RevCommit commitWithComments = commitWithApprovals.getParent(0);
+      assertNotNull(commitWithComments);
+
+      ChangeNotesParser notesWithComments =
+          new ChangeNotesParser(c, commitWithComments.copy(), rw, repoManager);
+      notesWithComments.parseAll();
+      ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals1 =
+          notesWithComments.buildApprovals();
+      assertEquals(0, approvals1.size());
+      assertEquals(1, notesWithComments.commentsForBase.size());
+      notesWithComments.close();
+
+      ChangeNotesParser notesWithApprovals =
+          new ChangeNotesParser(c, commitWithApprovals.copy(), rw, repoManager);
+      notesWithApprovals.parseAll();
+      ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals2 =
+          notesWithApprovals.buildApprovals();
+      assertEquals(1, approvals2.size());
+      assertEquals(1, notesWithApprovals.commentsForBase.size());
+      notesWithApprovals.close();
+    } finally {
+      batch.close();
+    }
+  }
+
+  @Test
+  public void multipleUpdatesAcrossRefs() throws Exception {
+    Change c1 = newChange();
+    ChangeUpdate update1 = newUpdate(c1, changeOwner);
+    update1.putApproval("Verified", (short) 1);
+
+    Change c2 = newChange();
+    ChangeUpdate update2 = newUpdate(c2, otherUser);
+    update2.putApproval("Code-Review", (short) 2);
+
+    BatchMetaDataUpdate batch1 = null;
+    BatchMetaDataUpdate batch2 = null;
+
+    BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
+    try {
+      batch1 = update1.openUpdateInBatch(bru);
+      batch1.write(update1, new CommitBuilder());
+      batch1.commit();
+      assertNull(repo.getRef(update1.getRefName()));
+
+      batch2 = update2.openUpdateInBatch(bru);
+      batch2.write(update2, new CommitBuilder());
+      batch2.commit();
+      assertNull(repo.getRef(update2.getRefName()));
+    } finally {
+      if (batch1 != null) {
+        batch1.close();
+      }
+      if (batch2 != null) {
+        batch2.close();
+      }
+    }
+
+    List<ReceiveCommand> cmds = bru.getCommands();
+    assertEquals(2, cmds.size());
+    assertEquals(update1.getRefName(), cmds.get(0).getRefName());
+    assertEquals(update2.getRefName(), cmds.get(1).getRefName());
+
+    try (RevWalk rw = new RevWalk(repo)) {
+      bru.execute(rw, NullProgressMonitor.INSTANCE);
+    }
+
+    assertEquals(ReceiveCommand.Result.OK, cmds.get(0).getResult());
+    assertEquals(ReceiveCommand.Result.OK, cmds.get(1).getResult());
+
+    assertNotNull(repo.getRef(update1.getRefName()));
+    assertNotNull(repo.getRef(update2.getRefName()));
+  }
+
+  @Test
   public void changeMessageOnePatchSet() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
@@ -666,6 +535,64 @@
   }
 
   @Test
+  public void noChangeMessage() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
+        notes.getChangeMessages();
+    assertEquals(0, changeMessages.keySet().size());
+  }
+
+  @Test
+  public void changeMessageWithTrailingDoubleNewline() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.setChangeMessage("Testing trailing double newline\n"
+        + "\n");
+    update.commit();
+    PatchSet.Id ps1 = c.currentPatchSetId();
+
+    ChangeNotes notes = newNotes(c);
+    ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
+        notes.getChangeMessages();
+    assertEquals(1, changeMessages.keySet().size());
+
+    ChangeMessage cm1 = Iterables.getOnlyElement(changeMessages.get(ps1));
+    assertEquals("Testing trailing double newline\n" + "\n", cm1.getMessage());
+    assertEquals(changeOwner.getAccount().getId(), cm1.getAuthor());
+  }
+
+  @Test
+  public void changeMessageWithMultipleParagraphs() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.setChangeMessage("Testing paragraph 1\n"
+        + "\n"
+        + "Testing paragraph 2\n"
+        + "\n"
+        + "Testing paragraph 3");
+    update.commit();
+    PatchSet.Id ps1 = c.currentPatchSetId();
+
+    ChangeNotes notes = newNotes(c);
+    ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
+        notes.getChangeMessages();
+    assertEquals(1, changeMessages.keySet().size());
+
+    ChangeMessage cm1 = Iterables.getOnlyElement(changeMessages.get(ps1));
+    assertEquals("Testing paragraph 1\n"
+        + "\n"
+        + "Testing paragraph 2\n"
+        + "\n"
+        + "Testing paragraph 3", cm1.getMessage());
+    assertEquals(changeOwner.getAccount().getId(), cm1.getAuthor());
+  }
+
+  @Test
   public void changeMessagesMultiplePatchSets() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
@@ -701,112 +628,6 @@
   }
 
   @Test
-  public void noChangeMessage() throws Exception {
-    Change c = newChange();
-    ChangeUpdate update = newUpdate(c, changeOwner);
-    update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
-    update.commit();
-
-    RevWalk walk = new RevWalk(repo);
-    try {
-      RevCommit commit = walk.parseCommit(update.getRevision());
-      walk.parseBody(commit);
-      assertEquals("Update patch set 1\n"
-          + "\n"
-          + "Patch-set: 1\n"
-          + "Reviewer: Change Owner <1@gerrit>\n",
-          commit.getFullMessage());
-    } finally {
-      walk.release();
-    }
-
-    ChangeNotes notes = newNotes(c);
-    ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
-        notes.getChangeMessages();
-    assertEquals(0, changeMessages.keySet().size());
-  }
-
-  @Test
-  public void changeMessageWithTrailingDoubleNewline() throws Exception {
-    Change c = newChange();
-    ChangeUpdate update = newUpdate(c, changeOwner);
-    update.setChangeMessage("Testing trailing double newline\n"
-        + "\n");
-    update.commit();
-    PatchSet.Id ps1 = c.currentPatchSetId();
-
-    RevWalk walk = new RevWalk(repo);
-    try {
-      RevCommit commit = walk.parseCommit(update.getRevision());
-      walk.parseBody(commit);
-      assertEquals("Update patch set 1\n"
-          + "\n"
-          + "Testing trailing double newline\n"
-          + "\n"
-          + "\n"
-          + "\n"
-          + "Patch-set: 1\n",
-          commit.getFullMessage());
-    } finally {
-      walk.release();
-    }
-
-    ChangeNotes notes = newNotes(c);
-    ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
-        notes.getChangeMessages();
-    assertEquals(1, changeMessages.keySet().size());
-
-    ChangeMessage cm1 = Iterables.getOnlyElement(changeMessages.get(ps1));
-    assertEquals("Testing trailing double newline\n" + "\n", cm1.getMessage());
-    assertEquals(changeOwner.getAccount().getId(), cm1.getAuthor());
-
-  }
-
-  @Test
-  public void changeMessageWithMultipleParagraphs() throws Exception {
-    Change c = newChange();
-    ChangeUpdate update = newUpdate(c, changeOwner);
-    update.setChangeMessage("Testing paragraph 1\n"
-        + "\n"
-        + "Testing paragraph 2\n"
-        + "\n"
-        + "Testing paragraph 3");
-    update.commit();
-    PatchSet.Id ps1 = c.currentPatchSetId();
-
-    RevWalk walk = new RevWalk(repo);
-    try {
-      RevCommit commit = walk.parseCommit(update.getRevision());
-      walk.parseBody(commit);
-      assertEquals("Update patch set 1\n"
-          + "\n"
-          + "Testing paragraph 1\n"
-          + "\n"
-          + "Testing paragraph 2\n"
-          + "\n"
-          + "Testing paragraph 3\n"
-          + "\n"
-          + "Patch-set: 1\n",
-          commit.getFullMessage());
-    } finally {
-      walk.release();
-    }
-
-    ChangeNotes notes = newNotes(c);
-    ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
-        notes.getChangeMessages();
-    assertEquals(1, changeMessages.keySet().size());
-
-    ChangeMessage cm1 = Iterables.getOnlyElement(changeMessages.get(ps1));
-    assertEquals("Testing paragraph 1\n"
-        + "\n"
-        + "Testing paragraph 2\n"
-        + "\n"
-        + "Testing paragraph 3", cm1.getMessage());
-    assertEquals(changeOwner.getAccount().getId(), cm1.getAuthor());
-  }
-
-  @Test
   public void changeMessageMultipleInOnePatchSet() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
@@ -844,7 +665,9 @@
   public void patchLineCommentNotesFormatSide1() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, otherUser);
-    String uuid = "uuid";
+    String uuid1 = "uuid1";
+    String uuid2 = "uuid2";
+    String uuid3 = "uuid3";
     String message1 = "comment 1";
     String message2 = "comment 2";
     String message3 = "comment 3";
@@ -855,76 +678,78 @@
     PatchSet.Id psId = c.currentPatchSetId();
 
     PatchLineComment comment1 = newPublishedPatchLineComment(psId, "file1",
-        uuid, range1, range1.getEndLine(), otherUser, null, time1, message1,
+        uuid1, range1, range1.getEndLine(), otherUser, null, time1, message1,
         (short) 1, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
-    update.putComment(comment1);
+    update.upsertComment(comment1);
     update.commit();
 
     update = newUpdate(c, otherUser);
     CommentRange range2 = new CommentRange(2, 1, 3, 1);
     PatchLineComment comment2 = newPublishedPatchLineComment(psId, "file1",
-        uuid, range2, range2.getEndLine(), otherUser, null, time2, message2,
+        uuid2, range2, range2.getEndLine(), otherUser, null, time2, message2,
         (short) 1, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
-    update.putComment(comment2);
+    update.upsertComment(comment2);
     update.commit();
 
     update = newUpdate(c, otherUser);
     CommentRange range3 = new CommentRange(3, 1, 4, 1);
     PatchLineComment comment3 = newPublishedPatchLineComment(psId, "file2",
-        uuid, range3, range3.getEndLine(), otherUser, null, time3, message3,
+        uuid3, range3, range3.getEndLine(), otherUser, null, time3, message3,
         (short) 1, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
-    update.putComment(comment3);
+    update.upsertComment(comment3);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
 
-    RevWalk walk = new RevWalk(repo);
-    ArrayList<Note> notesInTree =
-        Lists.newArrayList(notes.getNoteMap().iterator());
-    Note note = Iterables.getOnlyElement(notesInTree);
+    try (RevWalk walk = new RevWalk(repo)) {
+      ArrayList<Note> notesInTree =
+          Lists.newArrayList(notes.getNoteMap().iterator());
+      Note note = Iterables.getOnlyElement(notesInTree);
 
-    byte[] bytes =
-        walk.getObjectReader().open(
-            note.getData(), Constants.OBJ_BLOB).getBytes();
-    String noteString = new String(bytes, UTF_8);
-    assertEquals("Patch-set: 1\n"
-        + "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
-        + "File: file1\n"
-        + "\n"
-        + "1:1-2:1\n"
-        + CommentsInNotesUtil.formatTime(serverIdent, time1) + "\n"
-        + "Author: Other Account <2@gerrit>\n"
-        + "UUID: uuid\n"
-        + "Bytes: 9\n"
-        + "comment 1\n"
-        + "\n"
-        + "2:1-3:1\n"
-        + CommentsInNotesUtil.formatTime(serverIdent, time2) + "\n"
-        + "Author: Other Account <2@gerrit>\n"
-        + "UUID: uuid\n"
-        + "Bytes: 9\n"
-        + "comment 2\n"
-        + "\n"
-        + "File: file2\n"
-        + "\n"
-        + "3:1-4:1\n"
-        + CommentsInNotesUtil.formatTime(serverIdent, time3) + "\n"
-        + "Author: Other Account <2@gerrit>\n"
-        + "UUID: uuid\n"
-        + "Bytes: 9\n"
-        + "comment 3\n"
-        + "\n",
-        noteString);
+      byte[] bytes =
+          walk.getObjectReader().open(
+              note.getData(), Constants.OBJ_BLOB).getBytes();
+      String noteString = new String(bytes, UTF_8);
+      assertEquals("Patch-set: 1\n"
+          + "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+          + "File: file1\n"
+          + "\n"
+          + "1:1-2:1\n"
+          + CommentsInNotesUtil.formatTime(serverIdent, time1) + "\n"
+          + "Author: Other Account <2@gerrit>\n"
+          + "UUID: uuid1\n"
+          + "Bytes: 9\n"
+          + "comment 1\n"
+          + "\n"
+          + "2:1-3:1\n"
+          + CommentsInNotesUtil.formatTime(serverIdent, time2) + "\n"
+          + "Author: Other Account <2@gerrit>\n"
+          + "UUID: uuid2\n"
+          + "Bytes: 9\n"
+          + "comment 2\n"
+          + "\n"
+          + "File: file2\n"
+          + "\n"
+          + "3:1-4:1\n"
+          + CommentsInNotesUtil.formatTime(serverIdent, time3) + "\n"
+          + "Author: Other Account <2@gerrit>\n"
+          + "UUID: uuid3\n"
+          + "Bytes: 9\n"
+          + "comment 3\n"
+          + "\n",
+          noteString);
+    }
   }
 
   @Test
   public void patchLineCommentNotesFormatSide0() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, otherUser);
-    String uuid = "uuid";
+    String uuid1 = "uuid1";
+    String uuid2 = "uuid2";
     String message1 = "comment 1";
     String message2 = "comment 2";
     CommentRange range1 = new CommentRange(1, 1, 2, 1);
@@ -933,60 +758,61 @@
     PatchSet.Id psId = c.currentPatchSetId();
 
     PatchLineComment comment1 = newPublishedPatchLineComment(psId, "file1",
-        uuid, range1, range1.getEndLine(), otherUser, null, time1, message1,
+        uuid1, range1, range1.getEndLine(), otherUser, null, time1, message1,
         (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
-    update.putComment(comment1);
+    update.upsertComment(comment1);
     update.commit();
 
     update = newUpdate(c, otherUser);
     CommentRange range2 = new CommentRange(2, 1, 3, 1);
     PatchLineComment comment2 = newPublishedPatchLineComment(psId, "file1",
-        uuid, range2, range2.getEndLine(), otherUser, null, time2, message2,
+        uuid2, range2, range2.getEndLine(), otherUser, null, time2, message2,
         (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
-    update.putComment(comment2);
+    update.upsertComment(comment2);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
 
-    RevWalk walk = new RevWalk(repo);
-    ArrayList<Note> notesInTree =
-        Lists.newArrayList(notes.getNoteMap().iterator());
-    Note note = Iterables.getOnlyElement(notesInTree);
+    try (RevWalk walk = new RevWalk(repo)) {
+      ArrayList<Note> notesInTree =
+          Lists.newArrayList(notes.getNoteMap().iterator());
+      Note note = Iterables.getOnlyElement(notesInTree);
 
-    byte[] bytes =
-        walk.getObjectReader().open(
-            note.getData(), Constants.OBJ_BLOB).getBytes();
-    String noteString = new String(bytes, UTF_8);
-    assertEquals("Base-for-patch-set: 1\n"
-        + "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
-        + "File: file1\n"
-        + "\n"
-        + "1:1-2:1\n"
-        + CommentsInNotesUtil.formatTime(serverIdent, time1) + "\n"
-        + "Author: Other Account <2@gerrit>\n"
-        + "UUID: uuid\n"
-        + "Bytes: 9\n"
-        + "comment 1\n"
-        + "\n"
-        + "2:1-3:1\n"
-        + CommentsInNotesUtil.formatTime(serverIdent, time2) + "\n"
-        + "Author: Other Account <2@gerrit>\n"
-        + "UUID: uuid\n"
-        + "Bytes: 9\n"
-        + "comment 2\n"
-        + "\n",
-        noteString);
+      byte[] bytes =
+          walk.getObjectReader().open(
+              note.getData(), Constants.OBJ_BLOB).getBytes();
+      String noteString = new String(bytes, UTF_8);
+      assertEquals("Base-for-patch-set: 1\n"
+          + "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+          + "File: file1\n"
+          + "\n"
+          + "1:1-2:1\n"
+          + CommentsInNotesUtil.formatTime(serverIdent, time1) + "\n"
+          + "Author: Other Account <2@gerrit>\n"
+          + "UUID: uuid1\n"
+          + "Bytes: 9\n"
+          + "comment 1\n"
+          + "\n"
+          + "2:1-3:1\n"
+          + CommentsInNotesUtil.formatTime(serverIdent, time2) + "\n"
+          + "Author: Other Account <2@gerrit>\n"
+          + "UUID: uuid2\n"
+          + "Bytes: 9\n"
+          + "comment 2\n"
+          + "\n",
+          noteString);
+    }
   }
 
-
   @Test
   public void patchLineCommentMultipleOnePatchsetOneFileBothSides()
       throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, otherUser);
-    String uuid = "uuid";
+    String uuid1 = "uuid1";
+    String uuid2 = "uuid2";
     String messageForBase = "comment for base";
     String messageForPS = "comment for ps";
     CommentRange range = new CommentRange(1, 1, 2, 1);
@@ -994,20 +820,20 @@
     PatchSet.Id psId = c.currentPatchSetId();
 
     PatchLineComment commentForBase =
-        newPublishedPatchLineComment(psId, "filename", uuid,
+        newPublishedPatchLineComment(psId, "filename", uuid1,
         range, range.getEndLine(), otherUser, null, now, messageForBase,
         (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
-    update.putComment(commentForBase);
+    update.upsertComment(commentForBase);
     update.commit();
 
     update = newUpdate(c, otherUser);
     PatchLineComment commentForPS =
-        newPublishedPatchLineComment(psId, "filename", uuid,
+        newPublishedPatchLineComment(psId, "filename", uuid2,
         range, range.getEndLine(), otherUser, null, now, messageForPS,
         (short) 1, "abcd4567abcd4567abcd4567abcd4567abcd4567");
     update.setPatchSetId(psId);
-    update.putComment(commentForPS);
+    update.upsertComment(commentForPS);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
@@ -1027,7 +853,8 @@
   @Test
   public void patchLineCommentMultipleOnePatchsetOneFile() throws Exception {
     Change c = newChange();
-    String uuid = "uuid";
+    String uuid1 = "uuid1";
+    String uuid2 = "uuid2";
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id psId = c.currentPatchSetId();
     String filename = "filename";
@@ -1037,18 +864,18 @@
     Timestamp timeForComment1 = TimeUtil.nowTs();
     Timestamp timeForComment2 = TimeUtil.nowTs();
     PatchLineComment comment1 = newPublishedPatchLineComment(psId, filename,
-        uuid, range, range.getEndLine(), otherUser, null, timeForComment1,
+        uuid1, range, range.getEndLine(), otherUser, null, timeForComment1,
         "comment 1", side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
-    update.putComment(comment1);
+    update.upsertComment(comment1);
     update.commit();
 
     update = newUpdate(c, otherUser);
     PatchLineComment comment2 = newPublishedPatchLineComment(psId, filename,
-        uuid, range, range.getEndLine(), otherUser, null, timeForComment2,
+        uuid2, range, range.getEndLine(), otherUser, null, timeForComment2,
         "comment 2", side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
-    update.putComment(comment2);
+    update.upsertComment(comment2);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
@@ -1086,7 +913,7 @@
         uuid, range, range.getEndLine(), otherUser, null, now, "comment 1",
         side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
-    update.putComment(comment1);
+    update.upsertComment(comment1);
     update.commit();
 
     update = newUpdate(c, otherUser);
@@ -1094,7 +921,7 @@
         uuid, range, range.getEndLine(), otherUser, null, now, "comment 2",
         side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
-    update.putComment(comment2);
+    update.upsertComment(comment2);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
@@ -1130,7 +957,7 @@
         uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps1",
         side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(ps1);
-    update.putComment(comment1);
+    update.upsertComment(comment1);
     update.commit();
 
     incrementPatchSet(c);
@@ -1142,7 +969,7 @@
         uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps2",
         side, "abcd4567abcd4567abcd4567abcd4567abcd4567");
     update.setPatchSetId(ps2);
-    update.putComment(comment2);
+    update.upsertComment(comment2);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
@@ -1165,68 +992,213 @@
     assertEquals(comment2, commentFromPs2);
   }
 
-  private Change newChange() {
-    return TestChanges.newChange(project, changeOwner);
+  @Test
+  public void patchLineCommentSingleDraftToPublished() throws Exception {
+    Change c = newChange();
+    String uuid = "uuid";
+    CommentRange range = new CommentRange(1, 1, 2, 1);
+    PatchSet.Id ps1 = c.currentPatchSetId();
+    String filename = "filename1";
+    short side = (short) 1;
+
+    ChangeUpdate update = newUpdate(c, otherUser);
+    Timestamp now = TimeUtil.nowTs();
+    PatchLineComment comment1 = newPatchLineComment(ps1, filename, uuid,
+        range, range.getEndLine(), otherUser, null, now, "comment on ps1", side,
+        "abcd4567abcd4567abcd4567abcd4567abcd4567", Status.DRAFT);
+    update.setPatchSetId(ps1);
+    update.insertComment(comment1);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    assertEquals(1, notes.getDraftPsComments(otherUserId).values().size());
+    assertEquals(0, notes.getDraftBaseComments(otherUserId).values().size());
+
+    comment1.setStatus(Status.PUBLISHED);
+    update = newUpdate(c, otherUser);
+    update.setPatchSetId(ps1);
+    update.updateComment(comment1);
+    update.commit();
+
+    notes = newNotes(c);
+
+    assertTrue(notes.getDraftPsComments(otherUserId).values().isEmpty());
+    assertTrue(notes.getDraftBaseComments(otherUserId).values().isEmpty());
+
+    assertTrue(notes.getBaseComments().values().isEmpty());
+    PatchLineComment commentFromNotes =
+        Iterables.getOnlyElement(notes.getPatchSetComments().values());
+    assertEquals(comment1, commentFromNotes);
   }
 
-  private PatchLineComment newPublishedPatchLineComment(PatchSet.Id psId,
-      String filename, String UUID, CommentRange range, int line,
-      IdentifiedUser commenter, String parentUUID, Timestamp t,
-      String message, short side, String commitSHA1) {
-    return newPatchLineComment(psId, filename, UUID, range, line, commenter,
-        parentUUID, t, message, side, commitSHA1, Status.PUBLISHED);
+  @Test
+  public void patchLineCommentMultipleDraftsSameSidePublishOne()
+      throws OrmException, IOException {
+    Change c = newChange();
+    String uuid1 = "uuid1";
+    String uuid2 = "uuid2";
+    CommentRange range1 = new CommentRange(1, 1, 2, 2);
+    CommentRange range2 = new CommentRange(2, 2, 3, 3);
+    String filename = "filename1";
+    short side = (short) 1;
+    Timestamp now = TimeUtil.nowTs();
+    String commitSHA1 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
+    PatchSet.Id psId = c.currentPatchSetId();
+
+    // Write two drafts on the same side of one patch set.
+    ChangeUpdate update = newUpdate(c, otherUser);
+    update.setPatchSetId(psId);
+    PatchLineComment comment1 = newPatchLineComment(psId, filename, uuid1,
+        range1, range1.getEndLine(), otherUser, null, now, "comment on ps1",
+        side, commitSHA1, Status.DRAFT);
+    PatchLineComment comment2 = newPatchLineComment(psId, filename, uuid2,
+        range2, range2.getEndLine(), otherUser, null, now, "other on ps1",
+        side, commitSHA1, Status.DRAFT);
+    update.insertComment(comment1);
+    update.insertComment(comment2);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    assertTrue(notes.getDraftBaseComments(otherUserId).values().isEmpty());
+    assertEquals(2, notes.getDraftPsComments(otherUserId).values().size());
+
+    assertTrue(notes.getDraftPsComments(otherUserId).containsValue(comment1));
+    assertTrue(notes.getDraftPsComments(otherUserId).containsValue(comment2));
+
+    // Publish first draft.
+    update = newUpdate(c, otherUser);
+    update.setPatchSetId(psId);
+    comment1.setStatus(Status.PUBLISHED);
+    update.updateComment(comment1);
+    update.commit();
+
+    notes = newNotes(c);
+    assertEquals(comment1,
+        Iterables.getOnlyElement(notes.getPatchSetComments().get(psId)));
+    assertEquals(comment2,
+        Iterables.getOnlyElement(
+            notes.getDraftPsComments(otherUserId).values()));
+
+    assertTrue(notes.getBaseComments().values().isEmpty());
+    assertTrue(notes.getDraftBaseComments(otherUserId).values().isEmpty());
   }
 
-  private PatchLineComment newPatchLineComment(PatchSet.Id psId,
-      String filename, String UUID, CommentRange range, int line,
-      IdentifiedUser commenter, String parentUUID, Timestamp t,
-      String message, short side, String commitSHA1, Status status) {
-    PatchLineComment comment = new PatchLineComment(
-        new PatchLineComment.Key(
-            new Patch.Key(psId, filename), UUID),
-        line, commenter.getAccountId(), parentUUID, t);
-    comment.setSide(side);
-    comment.setMessage(message);
-    comment.setRange(range);
-    comment.setRevId(new RevId(commitSHA1));
-    comment.setStatus(status);
-    return comment;
+  @Test
+  public void patchLineCommentsMultipleDraftsBothSidesPublishAll()
+      throws OrmException, IOException {
+    Change c = newChange();
+    String uuid1 = "uuid1";
+    String uuid2 = "uuid2";
+    CommentRange range1 = new CommentRange(1, 1, 2, 2);
+    CommentRange range2 = new CommentRange(2, 2, 3, 3);
+    String filename = "filename1";
+    Timestamp now = TimeUtil.nowTs();
+    String commitSHA1 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
+    String baseSHA1 = "abcd1234abcd1234abcd1234abcd1234abcd1234";
+    PatchSet.Id psId = c.currentPatchSetId();
+
+    // Write two drafts, one on each side of the patchset.
+    ChangeUpdate update = newUpdate(c, otherUser);
+    update.setPatchSetId(psId);
+    PatchLineComment baseComment = newPatchLineComment(psId, filename, uuid1,
+        range1, range1.getEndLine(), otherUser, null, now, "comment on base",
+        (short) 0, baseSHA1, Status.DRAFT);
+    PatchLineComment psComment = newPatchLineComment(psId, filename, uuid2,
+        range2, range2.getEndLine(), otherUser, null, now, "comment on ps",
+        (short) 1, commitSHA1, Status.DRAFT);
+
+    update.insertComment(baseComment);
+    update.insertComment(psComment);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    PatchLineComment baseDraftCommentFromNotes =
+        Iterables.getOnlyElement(
+            notes.getDraftBaseComments(otherUserId).values());
+    PatchLineComment psDraftCommentFromNotes =
+        Iterables.getOnlyElement(
+            notes.getDraftPsComments(otherUserId).values());
+
+    assertEquals(baseComment, baseDraftCommentFromNotes);
+    assertEquals(psComment, psDraftCommentFromNotes);
+
+    // Publish both comments.
+    update = newUpdate(c, otherUser);
+    update.setPatchSetId(psId);
+
+    baseComment.setStatus(Status.PUBLISHED);
+    psComment.setStatus(Status.PUBLISHED);
+    update.updateComment(baseComment);
+    update.updateComment(psComment);
+    update.commit();
+
+    notes = newNotes(c);
+
+    PatchLineComment baseCommentFromNotes =
+        Iterables.getOnlyElement(notes.getBaseComments().values());
+    PatchLineComment psCommentFromNotes =
+        Iterables.getOnlyElement(notes.getPatchSetComments().values());
+
+    assertEquals(baseComment, baseCommentFromNotes);
+    assertEquals(psComment, psCommentFromNotes);
+
+    assertTrue(notes.getDraftBaseComments(otherUserId).values().isEmpty());
+    assertTrue(notes.getDraftPsComments(otherUserId).values().isEmpty());
   }
 
-  private ChangeUpdate newUpdate(Change c, IdentifiedUser user)
-      throws Exception {
-    return TestChanges.newUpdate(injector, repoManager, c, user);
+  @Test
+  public void fileComment() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, otherUser);
+    String uuid = "uuid";
+    String messageForBase = "comment for base";
+    Timestamp now = TimeUtil.nowTs();
+    PatchSet.Id psId = c.currentPatchSetId();
+
+    PatchLineComment commentForBase =
+        newPublishedPatchLineComment(psId, "filename", uuid,
+        null, 0, otherUser, null, now, messageForBase,
+        (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+    update.setPatchSetId(psId);
+    update.upsertComment(commentForBase);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    Multimap<PatchSet.Id, PatchLineComment> commentsForBase =
+        notes.getBaseComments();
+    Multimap<PatchSet.Id, PatchLineComment> commentsForPs =
+        notes.getPatchSetComments();
+
+    assertTrue(commentsForPs.isEmpty());
+    assertEquals(commentForBase,
+        Iterables.getOnlyElement(commentsForBase.get(psId)));
   }
 
-  private ChangeNotes newNotes(Change c) throws OrmException {
-    return new ChangeNotes(repoManager, c).load();
-  }
+  @Test
+  public void patchLineCommentNoRange() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, otherUser);
+    String uuid = "uuid";
+    String messageForBase = "comment for base";
+    Timestamp now = TimeUtil.nowTs();
+    PatchSet.Id psId = c.currentPatchSetId();
 
-  private static Timestamp truncate(Timestamp ts) {
-    return new Timestamp((ts.getTime() / 1000) * 1000);
-  }
+    PatchLineComment commentForBase =
+        newPublishedPatchLineComment(psId, "filename", uuid,
+        null, 1, otherUser, null, now, messageForBase,
+        (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+    update.setPatchSetId(psId);
+    update.upsertComment(commentForBase);
+    update.commit();
 
-  private static Timestamp after(Change c, long millis) {
-    return new Timestamp(c.getCreatedOn().getTime() + millis);
-  }
+    ChangeNotes notes = newNotes(c);
+    Multimap<PatchSet.Id, PatchLineComment> commentsForBase =
+        notes.getBaseComments();
+    Multimap<PatchSet.Id, PatchLineComment> commentsForPs =
+        notes.getPatchSetComments();
 
-  private static SubmitRecord submitRecord(String status,
-      String errorMessage, SubmitRecord.Label... labels) {
-    SubmitRecord rec = new SubmitRecord();
-    rec.status = SubmitRecord.Status.valueOf(status);
-    rec.errorMessage = errorMessage;
-    if (labels.length > 0) {
-      rec.labels = ImmutableList.copyOf(labels);
-    }
-    return rec;
-  }
-
-  private static SubmitRecord.Label submitLabel(String name, String status,
-      Account.Id appliedBy) {
-    SubmitRecord.Label label = new SubmitRecord.Label();
-    label.label = name;
-    label.status = SubmitRecord.Label.Status.valueOf(status);
-    label.appliedBy = appliedBy;
-    return label;
+    assertTrue(commentsForPs.isEmpty());
+    assertEquals(commentForBase,
+        Iterables.getOnlyElement(commentsForBase.get(psId)));
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
new file mode 100644
index 0000000..328509a
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -0,0 +1,257 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static com.google.gerrit.server.notedb.ReviewerState.CC;
+import static com.google.gerrit.server.notedb.ReviewerState.REVIEWER;
+import static org.junit.Assert.assertEquals;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.testutil.TestChanges;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.Test;
+
+import java.util.Date;
+import java.util.TimeZone;
+
+public class CommitMessageOutputTest extends AbstractChangeNotesTest {
+  @Test
+  public void approvalsCommitFormatSimple() throws Exception {
+    Change c = TestChanges.newChange(project, changeOwner.getAccountId(), 1);
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.putApproval("Verified", (short) 1);
+    update.putApproval("Code-Review", (short) -1);
+    update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
+    update.putReviewer(otherUser.getAccount().getId(), CC);
+    update.commit();
+    assertEquals("refs/changes/01/1/meta", update.getRefName());
+
+    RevCommit commit = parseCommit(update.getRevision());
+    assertBodyEquals("Update patch set 1\n"
+        + "\n"
+        + "Patch-set: 1\n"
+        + "Reviewer: Change Owner <1@gerrit>\n"
+        + "CC: Other Account <2@gerrit>\n"
+        + "Label: Code-Review=-1\n"
+        + "Label: Verified=+1\n",
+        commit);
+
+    PersonIdent author = commit.getAuthorIdent();
+    assertEquals("Change Owner", author.getName());
+    assertEquals("1@gerrit", author.getEmailAddress());
+    assertEquals(new Date(c.getCreatedOn().getTime() + 1000),
+        author.getWhen());
+    assertEquals(TimeZone.getTimeZone("GMT-7:00"), author.getTimeZone());
+
+    PersonIdent committer = commit.getCommitterIdent();
+    assertEquals("Gerrit Server", committer.getName());
+    assertEquals("noreply@gerrit.com", committer.getEmailAddress());
+    assertEquals(author.getWhen(), committer.getWhen());
+    assertEquals(author.getTimeZone(), committer.getTimeZone());
+  }
+
+  @Test
+  public void changeMessageCommitFormatSimple() throws Exception {
+    Change c = TestChanges.newChange(project, changeOwner.getAccountId(), 1);
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.setChangeMessage("Just a little code change.\n"
+        + "How about a new line");
+    update.commit();
+    assertEquals("refs/changes/01/1/meta", update.getRefName());
+
+    assertBodyEquals("Update patch set 1\n"
+        + "\n"
+        + "Just a little code change.\n"
+        + "How about a new line\n"
+        + "\n"
+        + "Patch-set: 1\n",
+        update.getRevision());
+  }
+
+  @Test
+  public void approvalTombstoneCommitFormat() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.removeApproval("Code-Review");
+    update.commit();
+
+    assertBodyEquals("Update patch set 1\n"
+        + "\n"
+        + "Patch-set: 1\n"
+        + "Label: -Code-Review\n",
+        update.getRevision());
+  }
+
+  @Test
+  public void submitCommitFormat() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.setSubject("Submit patch set 1");
+
+    update.submit(ImmutableList.of(
+        submitRecord("NOT_READY", null,
+          submitLabel("Verified", "OK", changeOwner.getAccountId()),
+          submitLabel("Code-Review", "NEED", null)),
+        submitRecord("NOT_READY", null,
+          submitLabel("Verified", "OK", changeOwner.getAccountId()),
+          submitLabel("Alternative-Code-Review", "NEED", null))));
+    update.commit();
+
+    RevCommit commit = parseCommit(update.getRevision());
+    assertBodyEquals("Submit patch set 1\n"
+        + "\n"
+        + "Patch-set: 1\n"
+        + "Status: submitted\n"
+        + "Submitted-with: NOT_READY\n"
+        + "Submitted-with: OK: Verified: Change Owner <1@gerrit>\n"
+        + "Submitted-with: NEED: Code-Review\n"
+        + "Submitted-with: NOT_READY\n"
+        + "Submitted-with: OK: Verified: Change Owner <1@gerrit>\n"
+        + "Submitted-with: NEED: Alternative-Code-Review\n",
+        commit);
+
+    PersonIdent author = commit.getAuthorIdent();
+    assertEquals("Change Owner", author.getName());
+    assertEquals("1@gerrit", author.getEmailAddress());
+    assertEquals(new Date(c.getCreatedOn().getTime() + 1000),
+        author.getWhen());
+    assertEquals(TimeZone.getTimeZone("GMT-7:00"), author.getTimeZone());
+
+    PersonIdent committer = commit.getCommitterIdent();
+    assertEquals("Gerrit Server", committer.getName());
+    assertEquals("noreply@gerrit.com", committer.getEmailAddress());
+    assertEquals(author.getWhen(), committer.getWhen());
+    assertEquals(author.getTimeZone(), committer.getTimeZone());
+  }
+
+  @Test
+  public void anonymousUser() throws Exception {
+    Account anon = new Account(new Account.Id(3), TimeUtil.nowTs());
+    accountCache.put(anon);
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, userFactory.create(anon.getId()));
+    update.setChangeMessage("Comment on the change.");
+    update.commit();
+
+    RevCommit commit = parseCommit(update.getRevision());
+    assertBodyEquals("Update patch set 1\n"
+        + "\n"
+        + "Comment on the change.\n"
+        + "\n"
+        + "Patch-set: 1\n",
+        commit);
+
+    PersonIdent author = commit.getAuthorIdent();
+    assertEquals("Anonymous Coward (3)", author.getName());
+    assertEquals("3@gerrit", author.getEmailAddress());
+  }
+
+  @Test
+  public void submitWithErrorMessage() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.setSubject("Submit patch set 1");
+
+    update.submit(ImmutableList.of(
+        submitRecord("RULE_ERROR", "Problem with patch set:\n1")));
+    update.commit();
+
+    assertBodyEquals("Submit patch set 1\n"
+        + "\n"
+        + "Patch-set: 1\n"
+        + "Status: submitted\n"
+        + "Submitted-with: RULE_ERROR Problem with patch set: 1\n",
+        update.getRevision());
+  }
+
+  @Test
+  public void noChangeMessage() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
+    update.commit();
+
+    assertBodyEquals("Update patch set 1\n"
+        + "\n"
+        + "Patch-set: 1\n"
+        + "Reviewer: Change Owner <1@gerrit>\n",
+        update.getRevision());
+  }
+
+  @Test
+  public void changeMessageWithTrailingDoubleNewline() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.setChangeMessage("Testing trailing double newline\n"
+        + "\n");
+    update.commit();
+
+    assertBodyEquals("Update patch set 1\n"
+        + "\n"
+        + "Testing trailing double newline\n"
+        + "\n"
+        + "\n"
+        + "\n"
+        + "Patch-set: 1\n",
+        update.getRevision());
+  }
+
+  @Test
+  public void changeMessageWithMultipleParagraphs() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.setChangeMessage("Testing paragraph 1\n"
+        + "\n"
+        + "Testing paragraph 2\n"
+        + "\n"
+        + "Testing paragraph 3");
+    update.commit();
+
+    assertBodyEquals("Update patch set 1\n"
+        + "\n"
+        + "Testing paragraph 1\n"
+        + "\n"
+        + "Testing paragraph 2\n"
+        + "\n"
+        + "Testing paragraph 3\n"
+        + "\n"
+        + "Patch-set: 1\n",
+        update.getRevision());
+  }
+
+  private RevCommit parseCommit(ObjectId id) throws Exception {
+    if (id instanceof RevCommit) {
+      return (RevCommit) id;
+    }
+    try (RevWalk walk = new RevWalk(repo)) {
+      RevCommit commit = walk.parseCommit(id);
+      walk.parseBody(commit);
+      return commit;
+    }
+  }
+
+  private void assertBodyEquals(String expected, ObjectId commitId)
+      throws Exception {
+    RevCommit commit = parseCommit(commitId);
+    assertEquals(expected, commit.getFullMessage());
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/patch/PatchListEntryTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/patch/PatchListEntryTest.java
index af60ea8..bff557c 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/patch/PatchListEntryTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/patch/PatchListEntryTest.java
@@ -14,14 +14,15 @@
 
 package com.google.gerrit.server.patch;
 
-import com.google.gerrit.reviewdb.client.Patch;
-import org.junit.Test;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
+import com.google.gerrit.reviewdb.client.Patch;
+
+import org.junit.Test;
+
 public class PatchListEntryTest {
   @Test
   public void testEmpty1() {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/ProjectControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/ProjectControlTest.java
new file mode 100644
index 0000000..b5b321e
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/ProjectControlTest.java
@@ -0,0 +1,176 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import static com.google.gerrit.common.data.Permission.READ;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.Util.allow;
+import static com.google.gerrit.server.project.Util.deny;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.schema.SchemaCreator;
+import com.google.gerrit.testutil.InMemoryDatabase;
+import com.google.gerrit.testutil.InMemoryModule;
+import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Unit tests for {@link ProjectControl}. */
+public class ProjectControlTest {
+  @Inject private AccountManager accountManager;
+  @Inject private IdentifiedUser.RequestFactory userFactory;
+  @Inject private InMemoryDatabase schemaFactory;
+  @Inject private InMemoryRepositoryManager repoManager;
+  @Inject private ProjectControl.GenericFactory projectControlFactory;
+  @Inject private SchemaCreator schemaCreator;
+
+  private LifecycleManager lifecycle;
+  private ReviewDb db;
+  private TestRepository<InMemoryRepository> repo;
+  private ProjectConfig project;
+  private IdentifiedUser user;
+
+  @Before
+  public void setUp() throws Exception {
+    Injector injector = Guice.createInjector(new InMemoryModule());
+    injector.injectMembers(this);
+    lifecycle = new LifecycleManager();
+    lifecycle.add(injector);
+    lifecycle.start();
+
+    db = schemaFactory.open();
+    schemaCreator.create(db);
+    Account.Id userId = accountManager.authenticate(AuthRequest.forUser("user"))
+        .getAccountId();
+    user = userFactory.create(userId);
+
+    Project.NameKey name = new Project.NameKey("project");
+    InMemoryRepository inMemoryRepo = repoManager.createRepository(name);
+    project = new ProjectConfig(name);
+    project.load(inMemoryRepo);
+    repo = new TestRepository<>(inMemoryRepo);
+  }
+
+  @After
+  public void tearDown() {
+    if (repo != null) {
+      repo.getRepository().close();
+    }
+    if (lifecycle != null) {
+      lifecycle.stop();
+    }
+    if (db != null) {
+      db.close();
+    }
+    InMemoryDatabase.drop(schemaFactory);
+  }
+
+  @Test
+  public void canReadCommitWhenAllRefsVisible() throws Exception {
+    allow(project, READ, REGISTERED_USERS, "refs/*");
+    ObjectId id = repo.branch("master").commit().create();
+    ProjectControl pc = newProjectControl();
+    RevWalk rw = repo.getRevWalk();
+    assertTrue(pc.canReadCommit(db, rw, rw.parseCommit(id)));
+  }
+
+  @Test
+  public void canReadCommitIfRefVisible() throws Exception {
+    allow(project, READ, REGISTERED_USERS, "refs/heads/branch1");
+    deny(project, READ, REGISTERED_USERS, "refs/heads/branch2");
+
+    ObjectId id1 = repo.branch("branch1").commit().create();
+    ObjectId id2 = repo.branch("branch2").commit().create();
+
+    ProjectControl pc = newProjectControl();
+    RevWalk rw = repo.getRevWalk();
+    assertTrue(pc.canReadCommit(db, rw, rw.parseCommit(id1)));
+    assertFalse(pc.canReadCommit(db, rw, rw.parseCommit(id2)));
+  }
+
+  @Test
+  public void canReadCommitIfReachableFromVisibleRef() throws Exception {
+    allow(project, READ, REGISTERED_USERS, "refs/heads/branch1");
+    deny(project, READ, REGISTERED_USERS, "refs/heads/branch2");
+
+    RevCommit parent1 = repo.commit().create();
+    repo.branch("branch1").commit().parent(parent1).create();
+
+    RevCommit parent2 = repo.commit().create();
+    repo.branch("branch2").commit().parent(parent2).create();
+
+    ProjectControl pc = newProjectControl();
+    RevWalk rw = repo.getRevWalk();
+    assertTrue(pc.canReadCommit(db, rw, rw.parseCommit(parent1)));
+    assertFalse(pc.canReadCommit(db, rw, rw.parseCommit(parent2)));
+  }
+
+  @Test
+  public void cannotReadAfterRollbackWithRestrictedRead() throws Exception {
+    allow(project, READ, REGISTERED_USERS, "refs/heads/branch1");
+
+    RevCommit parent1 = repo.commit().create();
+    ObjectId id1 = repo.branch("branch1").commit().parent(parent1).create();
+
+    ProjectControl pc = newProjectControl();
+    RevWalk rw = repo.getRevWalk();
+    assertTrue(pc.canReadCommit(db, rw, rw.parseCommit(parent1)));
+    assertTrue(pc.canReadCommit(db, rw, rw.parseCommit(id1)));
+
+    repo.branch("branch1").update(parent1);
+    assertTrue(pc.canReadCommit(db, rw, rw.parseCommit(parent1)));
+    assertFalse(pc.canReadCommit(db, rw, rw.parseCommit(id1)));
+  }
+
+  @Test
+  public void canReadAfterRollbackWithAllRefsVisible() throws Exception {
+    allow(project, READ, REGISTERED_USERS, "refs/*");
+
+    RevCommit parent1 = repo.commit().create();
+    ObjectId id1 = repo.branch("branch1").commit().parent(parent1).create();
+
+    ProjectControl pc = newProjectControl();
+    RevWalk rw = repo.getRevWalk();
+    assertTrue(pc.canReadCommit(db, rw, rw.parseCommit(parent1)));
+    assertTrue(pc.canReadCommit(db, rw, rw.parseCommit(id1)));
+
+    repo.branch("branch1").update(parent1);
+    assertTrue(pc.canReadCommit(db, rw, rw.parseCommit(parent1)));
+    assertFalse(pc.canReadCommit(db, rw, rw.parseCommit(id1)));
+  }
+
+  private ProjectControl newProjectControl() throws Exception {
+    return projectControlFactory.controlFor(project.getName(), user);
+  }
+}
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 5478a6c..4c123fd 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
@@ -448,7 +448,7 @@
   }
 
   @Test
-  public void testUnblockVisibilityByREGISTEREDUsers() {
+  public void testUnblockVisibilityByRegisteredUsers() {
     block(local, READ, ANONYMOUS_USERS, "refs/heads/*");
     allow(local, READ, REGISTERED_USERS, "refs/heads/*");
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
index ac1e0d7..e39700c 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
@@ -29,27 +29,31 @@
 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.reviewdb.server.ReviewDb;
 import com.google.gerrit.rules.PrologEnvironment;
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.FakeRealm;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
+import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.change.ChangeKindCacheImpl;
+import com.google.gerrit.server.change.MergeabilityCache;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.CanonicalWebUrlProvider;
+import com.google.gerrit.server.config.DisableReverseDnsLookup;
 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.MergeUtil;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.patch.PatchListCache;
@@ -58,9 +62,11 @@
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
+import com.google.inject.Provider;
 import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
 
@@ -178,7 +184,7 @@
   private final CapabilityControl.Factory capabilityControlFactory;
   private final ChangeControl.AssistedFactory changeControlFactory;
   private final PermissionCollection.Factory sectionSorter;
-  private final GitRepositoryManager repoManager;
+  private final InMemoryRepositoryManager repoManager;
 
   private final AllProjectsName allProjectsName =
       new AllProjectsName("All-Projects");
@@ -241,36 +247,44 @@
       }
 
       @Override
-      public ProjectState checkedGet(NameKey projectName) throws IOException {
+      public ProjectState checkedGet(Project.NameKey projectName)
+          throws IOException {
         return all.get(projectName);
       }
 
       @Override
-      public void evict(NameKey p) {
+      public void evict(Project.NameKey p) {
       }
     };
 
     Injector injector = Guice.createInjector(new FactoryModule() {
+      @SuppressWarnings({"rawtypes", "unchecked"})
       @Override
       protected void configure() {
+        Provider nullProvider = Providers.of(null);
         bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(
             new Config());
-        bind(ReviewDb.class).toProvider(Providers.<ReviewDb> of(null));
+        bind(ReviewDb.class).toProvider(nullProvider);
         bind(GitRepositoryManager.class).toInstance(repoManager);
-        bind(PatchListCache.class)
-            .toProvider(Providers.<PatchListCache> of(null));
+        bind(PatchListCache.class).toProvider(nullProvider);
+        bind(Realm.class).to(FakeRealm.class);
 
         factory(CapabilityControl.Factory.class);
         factory(ChangeControl.AssistedFactory.class);
         factory(ChangeData.Factory.class);
+        factory(MergeUtil.Factory.class);
         bind(ProjectCache.class).toInstance(projectCache);
         bind(AccountCache.class).toInstance(new FakeAccountCache());
         bind(GroupBackend.class).to(SystemGroupBackend.class);
         bind(String.class).annotatedWith(CanonicalWebUrl.class)
             .toProvider(CanonicalWebUrlProvider.class);
+        bind(Boolean.class).annotatedWith(DisableReverseDnsLookup.class)
+            .toInstance(Boolean.FALSE);
         bind(String.class).annotatedWith(AnonymousCowardName.class)
             .toProvider(AnonymousCowardNameProvider.class);
         bind(ChangeKindCache.class).to(ChangeKindCacheImpl.NoCache.class);
+        bind(MergeabilityCache.class)
+          .to(MergeabilityCache.NotImplemented.class);
       }
     });
 
@@ -283,21 +297,26 @@
       injector.getInstance(ChangeControl.AssistedFactory.class);
   }
 
-  public void add(ProjectConfig pc) {
+  public InMemoryRepository add(ProjectConfig pc) {
     PrologEnvironment.Factory envFactory = null;
     ProjectControl.AssistedFactory projectControlFactory = null;
     RulesCache rulesCache = null;
     SitePaths sitePaths = null;
     List<CommentLinkInfo> commentLinks = null;
 
+    InMemoryRepository repo;
     try {
-      repoManager.createRepository(pc.getProject().getNameKey());
-    } catch (IOException e) {
+      repo = repoManager.createRepository(pc.getName());
+      if (pc.getProject() == null) {
+        pc.load(repo);
+      }
+    } catch (IOException | ConfigInvalidException e) {
       throw new RuntimeException(e);
     }
-    all.put(pc.getProject().getNameKey(), new ProjectState(sitePaths,
+    all.put(pc.getName(), new ProjectState(sitePaths,
         projectCache, allProjectsName, projectControlFactory, envFactory,
         repoManager, rulesCache, commentLinks, pc));
+    return repo;
   }
 
   public ProjectControl user(ProjectConfig local, AccountGroup.UUID... memberOf) {
@@ -310,8 +329,8 @@
 
     return new ProjectControl(Collections.<AccountGroup.UUID> emptySet(),
         Collections.<AccountGroup.UUID> emptySet(), projectCache,
-        sectionSorter, repoManager, changeControlFactory, canonicalWebUrl,
-        new MockUser(name, memberOf), newProjectState(local));
+        sectionSorter, repoManager, changeControlFactory, null, null,
+        canonicalWebUrl, new MockUser(name, memberOf), newProjectState(local));
   }
 
   private ProjectState newProjectState(ProjectConfig local) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/QueryParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/QueryParserTest.java
index 0eca069..e349273 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/QueryParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/QueryParserTest.java
@@ -13,11 +13,11 @@
 // limitations under the License.
 
 package com.google.gerrit.server.query;
+import static org.junit.Assert.assertEquals;
+
 import org.antlr.runtime.tree.Tree;
 import org.junit.Test;
 
-import static org.junit.Assert.assertEquals;
-
 public class QueryParserTest {
   @Test
   public void testProjectBare() throws QueryParseException {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index fb50744..34588fa 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -14,21 +14,29 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.TruthJUnit.assume;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.HOURS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.hash.Hashing;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.Account;
@@ -42,16 +50,17 @@
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.change.ChangeInserter;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
+import com.google.gerrit.server.change.ChangeTriplet;
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.PostReview;
 import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.project.CreateProject;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gerrit.server.util.TimeUtil;
+import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gerrit.testutil.InMemoryDatabase;
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
 import com.google.inject.Inject;
@@ -61,6 +70,7 @@
 
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeUtils;
@@ -69,21 +79,31 @@
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.util.List;
 import java.util.concurrent.atomic.AtomicLong;
 
 @Ignore
+@RunWith(ConfigSuite.class)
 public abstract class AbstractQueryChangesTest {
   private static final TopLevelResource TLR = TopLevelResource.INSTANCE;
 
+  @ConfigSuite.Config
+  public static Config noteDbEnabled() {
+    return NotesMigration.allEnabledConfig();
+  }
+
+  @ConfigSuite.Parameter public Config config;
   @Inject protected AccountManager accountManager;
   @Inject protected ChangeInserter.Factory changeFactory;
   @Inject protected ChangesCollection changes;
   @Inject protected CreateProject.Factory projectFactory;
+  @Inject protected GerritApi gApi;
   @Inject protected IdentifiedUser.RequestFactory userFactory;
   @Inject protected InMemoryDatabase schemaFactory;
   @Inject protected InMemoryRepositoryManager repoManager;
+  @Inject protected NotesMigration notesMigration;
   @Inject protected PostReview postReview;
   @Inject protected ProjectControl.GenericFactory projectControlFactory;
   @Inject protected Provider<QueryChanges> queryProvider;
@@ -116,18 +136,22 @@
     userAccount.setPreferredEmail("user@example.com");
     db.accounts().update(ImmutableList.of(userAccount));
     user = userFactory.create(userId);
+    requestContext.setContext(newRequestContext(userAccount.getId()));
+  }
 
-    requestContext.setContext(new RequestContext() {
+  private RequestContext newRequestContext(Account.Id requestUserId) {
+    final CurrentUser requestUser = userFactory.create(requestUserId);
+    return new RequestContext() {
       @Override
       public CurrentUser getCurrentUser() {
-        return user;
+        return requestUser;
       }
 
       @Override
       public Provider<ReviewDb> getReviewDbProvider() {
         return Providers.of(db);
       }
-    });
+    };
   }
 
   @After
@@ -169,7 +193,7 @@
     Change change1 = newChange(repo, null, null, null, null).insert();
     Change change2 = newChange(repo, null, null, null, null).insert();
 
-    assertTrue(query("12345").isEmpty());
+    assertThat(query("12345")).isEmpty();
     assertResultEquals(change1, queryOne(change1.getId().get()));
     assertResultEquals(change2, queryOne(change2.getId().get()));
   }
@@ -180,7 +204,7 @@
     Change change = newChange(repo, null, null, null, null).insert();
     String key = change.getKey().get();
 
-    assertTrue(query("I0000000000000000000000000000000000000000").isEmpty());
+    assertThat(query("I0000000000000000000000000000000000000000")).isEmpty();
     for (int i = 0; i <= 36; i++) {
       String q = key.substring(0, 41 - i);
       assertResultEquals("result for " + q, change, queryOne(q));
@@ -188,6 +212,33 @@
   }
 
   @Test
+  public void byTriplet() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change change = newChange(repo, null, null, null, "branch").insert();
+    String k = change.getKey().get();
+
+    assertResultEquals(change, queryOne("repo~branch~" + k));
+    assertResultEquals(change, queryOne("change:repo~branch~" + k));
+    assertResultEquals(change, queryOne("repo~refs/heads/branch~" + k));
+    assertResultEquals(change, queryOne("change:repo~refs/heads/branch~" + k));
+    assertResultEquals(change, queryOne("repo~branch~" + k.substring(0, 10)));
+    assertResultEquals(change,
+        queryOne("change:repo~branch~" + k.substring(0, 10)));
+
+    assertThat(query("foo~bar")).isEmpty();
+    assertBadQuery("change:foo~bar");
+    assertThat(query("otherrepo~branch~" + k)).isEmpty();
+    assertThat(query("change:otherrepo~branch~" + k)).isEmpty();
+    assertThat(query("repo~otherbranch~" + k)).isEmpty();
+    assertThat(query("change:repo~otherbranch~" + k)).isEmpty();
+    assertThat(query("repo~branch~I0000000000000000000000000000000000000000"))
+        .isEmpty();
+    assertThat(query(
+          "change:repo~branch~I0000000000000000000000000000000000000000"))
+        .isEmpty();
+  }
+
+  @Test
   public void byStatus() throws Exception {
     TestRepository<InMemoryRepository> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
@@ -200,6 +251,7 @@
     ins2.insert();
 
     assertResultEquals(change1, queryOne("status:new"));
+    assertResultEquals(change1, queryOne("status:NEW"));
     assertResultEquals(change1, queryOne("is:new"));
     assertResultEquals(change2, queryOne("status:merged"));
     assertResultEquals(change2, queryOne("is:merged"));
@@ -223,11 +275,22 @@
 
     List<ChangeInfo> results;
     results = query("status:open");
-    assertEquals(2, results.size());
+    assertThat(results).hasSize(2);
     assertResultEquals(change2, results.get(0));
     assertResultEquals(change1, results.get(1));
+
+    assertThat(query("status:OPEN")).hasSize(2);
+    assertThat(query("status:o")).hasSize(2);
+    assertThat(query("status:op")).hasSize(2);
+    assertThat(query("status:ope")).hasSize(2);
+    assertThat(query("status:pending")).hasSize(2);
+    assertThat(query("status:PENDING")).hasSize(2);
+    assertThat(query("status:p")).hasSize(2);
+    assertThat(query("status:pe")).hasSize(2);
+    assertThat(query("status:pen")).hasSize(2);
+
     results = query("is:open");
-    assertEquals(2, results.size());
+    assertThat(results).hasSize(2);
     assertResultEquals(change2, results.get(0));
     assertResultEquals(change1, results.get(1));
   }
@@ -250,23 +313,54 @@
 
     List<ChangeInfo> results;
     results = query("status:closed");
-    assertEquals(2, results.size());
+    assertThat(results).hasSize(2);
     assertResultEquals(change2, results.get(0));
     assertResultEquals(change1, results.get(1));
+
+    assertThat(query("status:CLOSED")).hasSize(2);
+    assertThat(query("status:c")).hasSize(2);
+    assertThat(query("status:cl")).hasSize(2);
+    assertThat(query("status:clo")).hasSize(2);
+    assertThat(query("status:clos")).hasSize(2);
+    assertThat(query("status:close")).hasSize(2);
+    assertThat(query("status:closed")).hasSize(2);
+
     results = query("is:closed");
-    assertEquals(2, results.size());
+    assertThat(results).hasSize(2);
     assertResultEquals(change2, results.get(0));
     assertResultEquals(change1, results.get(1));
   }
 
   @Test
+  public void byStatusPrefix() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    ChangeInserter ins1 = newChange(repo, null, null, null, null);
+    Change change1 = ins1.getChange();
+    change1.setStatus(Change.Status.NEW);
+    ins1.insert();
+    ChangeInserter ins2 = newChange(repo, null, null, null, null);
+    Change change2 = ins2.getChange();
+    change2.setStatus(Change.Status.MERGED);
+    ins2.insert();
+
+    assertResultEquals(change1, queryOne("status:n"));
+    assertResultEquals(change1, queryOne("status:ne"));
+    assertResultEquals(change1, queryOne("status:new"));
+    assertResultEquals(change1, queryOne("status:N"));
+    assertResultEquals(change1, queryOne("status:nE"));
+    assertResultEquals(change1, queryOne("status:neW"));
+    assertBadQuery("status:nx");
+    assertBadQuery("status:newx");
+  }
+
+  @Test
   public void byCommit() throws Exception {
     TestRepository<InMemoryRepository> repo = createProject("repo");
     ChangeInserter ins = newChange(repo, null, null, null, null);
     ins.insert();
     String sha = ins.getPatchSet().getRevision().get();
 
-    assertTrue(query("0000000000000000000000000000000000000000").isEmpty());
+    assertThat(query("0000000000000000000000000000000000000000")).isEmpty();
     for (int i = 0; i <= 36; i++) {
       String q = sha.substring(0, 40 - i);
       assertResultEquals("result for " + q, ins.getChange(), queryOne(q));
@@ -295,7 +389,7 @@
 
     assertResultEquals(change1, queryOne("ownerin:Administrators"));
     List<ChangeInfo> results = query("ownerin:\"Registered Users\"");
-    assertEquals(results.toString(), 2, results.size());
+    assertThat(results).hasSize(2);
     assertResultEquals(change2, results.get(0));
     assertResultEquals(change1, results.get(1));
   }
@@ -307,8 +401,8 @@
     Change change1 = newChange(repo1, null, null, null, null).insert();
     Change change2 = newChange(repo2, null, null, null, null).insert();
 
-    assertTrue(query("project:foo").isEmpty());
-    assertTrue(query("project:repo").isEmpty());
+    assertThat(query("project:foo")).isEmpty();
+    assertThat(query("project:repo")).isEmpty();
     assertResultEquals(change1, queryOne("project:repo1"));
     assertResultEquals(change2, queryOne("project:repo2"));
   }
@@ -320,13 +414,13 @@
     Change change1 = newChange(repo1, null, null, null, null).insert();
     Change change2 = newChange(repo2, null, null, null, null).insert();
 
-    assertTrue(query("projects:foo").isEmpty());
+    assertThat(query("projects:foo")).isEmpty();
     assertResultEquals(change1, queryOne("projects:repo1"));
     assertResultEquals(change2, queryOne("projects:repo2"));
 
     List<ChangeInfo> results;
     results = query("projects:repo");
-    assertEquals(results.toString(), 2, results.size());
+    assertThat(results).hasSize(2);
     assertResultEquals(change2, results.get(0));
     assertResultEquals(change1, results.get(1));
   }
@@ -337,15 +431,15 @@
     Change change1 = newChange(repo, null, null, null, "master").insert();
     Change change2 = newChange(repo, null, null, null, "branch").insert();
 
-    assertTrue(query("branch:foo").isEmpty());
+    assertThat(query("branch:foo")).isEmpty();
     assertResultEquals(change1, queryOne("branch:master"));
     assertResultEquals(change1, queryOne("branch:refs/heads/master"));
-    assertTrue(query("ref:master").isEmpty());
+    assertThat(query("ref:master")).isEmpty();
     assertResultEquals(change1, queryOne("ref:refs/heads/master"));
     assertResultEquals(change1, queryOne("branch:refs/heads/master"));
     assertResultEquals(change2, queryOne("branch:branch"));
     assertResultEquals(change2, queryOne("branch:refs/heads/branch"));
-    assertTrue(query("ref:branch").isEmpty());
+    assertThat(query("ref:branch")).isEmpty();
     assertResultEquals(change2, queryOne("ref:refs/heads/branch"));
   }
 
@@ -364,7 +458,7 @@
 
     Change change3 = newChange(repo, null, null, null, null).insert();
 
-    assertTrue(query("topic:foo").isEmpty());
+    assertThat(query("topic:foo")).isEmpty();
     assertResultEquals(change1, queryOne("topic:feature1"));
     assertResultEquals(change2, queryOne("topic:feature2"));
     assertResultEquals(change3, queryOne("topic:\"\""));
@@ -378,7 +472,7 @@
     RevCommit commit2 = repo.parseBody(repo.commit().message("two").create());
     Change change2 = newChange(repo, commit2, null, null, null).insert();
 
-    assertTrue(query("message:foo").isEmpty());
+    assertThat(query("message:foo")).isEmpty();
     assertResultEquals(change1, queryOne("message:one"));
     assertResultEquals(change2, queryOne("message:two"));
   }
@@ -393,7 +487,7 @@
         repo.parseBody(repo.commit().message("12346 67891").create());
     Change change2 = newChange(repo, commit2, null, null, null).insert();
 
-    assertTrue(query("message:1234").isEmpty());
+    assertThat(query("message:1234")).isEmpty();
     assertResultEquals(change1, queryOne("message:12345"));
     assertResultEquals(change2, queryOne("message:12346"));
   }
@@ -411,31 +505,31 @@
     postReview.apply(new RevisionResource(
         changes.parse(change.getId()), ins.getPatchSet()), input);
 
-    assertTrue(query("label:Code-Review=-2").isEmpty());
-    assertTrue(query("label:Code-Review-2").isEmpty());
-    assertTrue(query("label:Code-Review=-1").isEmpty());
-    assertTrue(query("label:Code-Review-1").isEmpty());
-    assertTrue(query("label:Code-Review=0").isEmpty());
+    assertThat(query("label:Code-Review=-2")).isEmpty();
+    assertThat(query("label:Code-Review-2")).isEmpty();
+    assertThat(query("label:Code-Review=-1")).isEmpty();
+    assertThat(query("label:Code-Review-1")).isEmpty();
+    assertThat(query("label:Code-Review=0")).isEmpty();
     assertResultEquals(change, queryOne("label:Code-Review=+1"));
     assertResultEquals(change, queryOne("label:Code-Review=1"));
     assertResultEquals(change, queryOne("label:Code-Review+1"));
-    assertTrue(query("label:Code-Review=+2").isEmpty());
-    assertTrue(query("label:Code-Review=2").isEmpty());
-    assertTrue(query("label:Code-Review+2").isEmpty());
+    assertThat(query("label:Code-Review=+2")).isEmpty();
+    assertThat(query("label:Code-Review=2")).isEmpty();
+    assertThat(query("label:Code-Review+2")).isEmpty();
 
     assertResultEquals(change, queryOne("label:Code-Review>=0"));
     assertResultEquals(change, queryOne("label:Code-Review>0"));
     assertResultEquals(change, queryOne("label:Code-Review>=1"));
-    assertTrue(query("label:Code-Review>1").isEmpty());
-    assertTrue(query("label:Code-Review>=2").isEmpty());
+    assertThat(query("label:Code-Review>1")).isEmpty();
+    assertThat(query("label:Code-Review>=2")).isEmpty();
 
     assertResultEquals(change, queryOne("label: Code-Review<=2"));
     assertResultEquals(change, queryOne("label: Code-Review<2"));
     assertResultEquals(change, queryOne("label: Code-Review<=1"));
-    assertTrue(query("label:Code-Review<1").isEmpty());
-    assertTrue(query("label:Code-Review<=0").isEmpty());
+    assertThat(query("label:Code-Review<1")).isEmpty();
+    assertThat(query("label:Code-Review<=0")).isEmpty();
 
-    assertTrue(query("label:Code-Review=+1,anotheruser").isEmpty());
+    assertThat(query("label:Code-Review=+1,anotheruser")).isEmpty();
     assertResultEquals(change, queryOne("label:Code-Review=+1,user"));
     assertResultEquals(change, queryOne("label:Code-Review=+1,user=user"));
     assertResultEquals(change, queryOne("label:Code-Review=+1,Administrators"));
@@ -453,9 +547,22 @@
 
     List<ChangeInfo> results;
     for (int i = 1; i <= n + 2; i++) {
+      int expectedSize;
+      Boolean expectedMoreChanges;
+      if (i < n) {
+        expectedSize = i;
+        expectedMoreChanges = true;
+      } else {
+        expectedSize = n;
+        expectedMoreChanges = null;
+      }
       results = query("status:new limit:" + i);
-      assertEquals(Math.min(i, n), results.size());
+      String msg = "i=" + i;
+      assert_().withFailureMessage(msg).that(results).hasSize(expectedSize);
       assertResultEquals(last, results.get(0));
+      assert_().withFailureMessage(msg)
+          .that(results.get(results.size() - 1)._moreChanges)
+          .isEqualTo(expectedMoreChanges);
     }
   }
 
@@ -470,25 +577,25 @@
     QueryChanges q;
     List<ChangeInfo> results;
     results = query("status:new");
-    assertEquals(2, results.size());
+    assertThat(results).hasSize(2);
     assertResultEquals(changes.get(1), results.get(0));
     assertResultEquals(changes.get(0), results.get(1));
 
     q = newQuery("status:new");
     q.setStart(1);
     results = query(q);
-    assertEquals(1, results.size());
+    assertThat(results).hasSize(1);
     assertResultEquals(changes.get(0), results.get(0));
 
     q = newQuery("status:new");
     q.setStart(2);
     results = query(q);
-    assertEquals(0, results.size());
+    assertThat(results).isEmpty();
 
     q = newQuery("status:new");
     q.setStart(3);
     results = query(q);
-    assertEquals(0, results.size());
+    assertThat(results).isEmpty();
   }
 
   @Test
@@ -502,27 +609,27 @@
     QueryChanges q;
     List<ChangeInfo> results;
     results = query("status:new limit:2");
-    assertEquals(2, results.size());
+    assertThat(results).hasSize(2);
     assertResultEquals(changes.get(2), results.get(0));
     assertResultEquals(changes.get(1), results.get(1));
 
     q = newQuery("status:new limit:2");
     q.setStart(1);
     results = query(q);
-    assertEquals(2, results.size());
+    assertThat(results).hasSize(2);
     assertResultEquals(changes.get(1), results.get(0));
     assertResultEquals(changes.get(0), results.get(1));
 
     q = newQuery("status:new limit:2");
     q.setStart(2);
     results = query(q);
-    assertEquals(1, results.size());
+    assertThat(results).hasSize(1);
     assertResultEquals(changes.get(0), results.get(0));
 
     q = newQuery("status:new limit:2");
     q.setStart(3);
     results = query(q);
-    assertEquals(0, results.size());
+    assertThat(results).isEmpty();
   }
 
   @Test
@@ -548,7 +655,7 @@
     }
 
     List<ChangeInfo> results = query("status:new");
-    assertEquals(5, results.size());
+    assertThat(results).hasSize(5);
     assertResultEquals(changes.get(3), results.get(0));
     assertResultEquals(changes.get(4), results.get(1));
     assertResultEquals(changes.get(1), results.get(2));
@@ -564,11 +671,11 @@
     Change change1 = ins1.insert();
     Change change2 = newChange(repo, null, null, null, null).insert();
 
-    assertTrue(lastUpdatedMs(change1) < lastUpdatedMs(change2));
+    assertThat(lastUpdatedMs(change1) < lastUpdatedMs(change2)).isTrue();
 
     List<ChangeInfo> results;
     results = query("status:new");
-    assertEquals(2, results.size());
+    assertThat(results).hasSize(2);
     assertResultEquals(change2, results.get(0));
     assertResultEquals(change1, results.get(1));
 
@@ -578,12 +685,12 @@
         changes.parse(change1.getId()), ins1.getPatchSet()), input);
     change1 = db.changes().get(change1.getId());
 
-    assertTrue(lastUpdatedMs(change1) > lastUpdatedMs(change2));
-    assertTrue(lastUpdatedMs(change1) - lastUpdatedMs(change2)
-        > MILLISECONDS.convert(1, MINUTES));
+    assertThat(lastUpdatedMs(change1) > lastUpdatedMs(change2)).isTrue();
+    assertThat(lastUpdatedMs(change1) - lastUpdatedMs(change2)
+        > MILLISECONDS.convert(1, MINUTES)).isTrue();
 
     results = query("status:new");
-    assertEquals(2, results.size());
+    assertThat(results).hasSize(2);
     // change1 moved to the top.
     assertResultEquals(change1, results.get(0));
     assertResultEquals(change2, results.get(1));
@@ -596,11 +703,11 @@
     Change change1 = ins1.insert();
     Change change2 = newChange(repo, null, null, null, null).insert();
 
-    assertTrue(lastUpdatedMs(change1) < lastUpdatedMs(change2));
+    assertThat(lastUpdatedMs(change1) < lastUpdatedMs(change2)).isTrue();
 
     List<ChangeInfo> results;
     results = query("status:new");
-    assertEquals(2, results.size());
+    assertThat(results).hasSize(2);
     assertResultEquals(change2, results.get(0));
     assertResultEquals(change1, results.get(1));
 
@@ -610,12 +717,12 @@
         changes.parse(change1.getId()), ins1.getPatchSet()), input);
     change1 = db.changes().get(change1.getId());
 
-    assertTrue(lastUpdatedMs(change1) > lastUpdatedMs(change2));
-    assertTrue(lastUpdatedMs(change1) - lastUpdatedMs(change2)
-        < MILLISECONDS.convert(1, MINUTES));
+    assertThat(lastUpdatedMs(change1) > lastUpdatedMs(change2)).isTrue();
+    assertThat(lastUpdatedMs(change1) - lastUpdatedMs(change2)
+        < MILLISECONDS.convert(1, MINUTES)).isTrue();
 
     results = query("status:new");
-    assertEquals(2, results.size());
+    assertThat(results).hasSize(2);
     // change1 moved to the top.
     assertResultEquals(change1, results.get(0));
     assertResultEquals(change2, results.get(1));
@@ -631,7 +738,7 @@
       newChange(repo, null, null, user2, null).insert();
     }
 
-    //assertResultEquals(change, queryOne("status:new ownerin:Administrators"));
+    assertResultEquals(change, queryOne("status:new ownerin:Administrators"));
     assertResultEquals(change,
         queryOne("status:new ownerin:Administrators limit:2"));
   }
@@ -645,8 +752,8 @@
       newChange(repo, null, null, user2, null).insert();
     }
 
-    assertTrue(query("status:new ownerin:Administrators").isEmpty());
-    assertTrue(query("status:new ownerin:Administrators limit:2").isEmpty());
+    assertThat(query("status:new ownerin:Administrators")).isEmpty();
+    assertThat(query("status:new ownerin:Administrators limit:2")).isEmpty();
   }
 
   @Test
@@ -658,7 +765,7 @@
         .create());
     Change change = newChange(repo, commit, null, null, null).insert();
 
-    assertTrue(query("file:file").isEmpty());
+    assertThat(query("file:file")).isEmpty();
     assertResultEquals(change, queryOne("file:dir"));
     assertResultEquals(change, queryOne("file:file1"));
     assertResultEquals(change, queryOne("file:file2"));
@@ -675,8 +782,8 @@
         .create());
     Change change = newChange(repo, commit, null, null, null).insert();
 
-    assertTrue(query("file:.*file.*").isEmpty());
-    assertTrue(query("file:^file.*").isEmpty()); // Whole path only.
+    assertThat(query("file:.*file.*")).isEmpty();
+    assertThat(query("file:^file.*")).isEmpty(); // Whole path only.
     assertResultEquals(change, queryOne("file:^dir.file.*"));
   }
 
@@ -689,10 +796,10 @@
         .create());
     Change change = newChange(repo, commit, null, null, null).insert();
 
-    assertTrue(query("path:file").isEmpty());
-    assertTrue(query("path:dir").isEmpty());
-    assertTrue(query("path:file1").isEmpty());
-    assertTrue(query("path:file2").isEmpty());
+    assertThat(query("path:file")).isEmpty();
+    assertThat(query("path:dir")).isEmpty();
+    assertThat(query("path:file1")).isEmpty();
+    assertThat(query("path:file2")).isEmpty();
     assertResultEquals(change, queryOne("path:dir/file1"));
     assertResultEquals(change, queryOne("path:dir/file2"));
   }
@@ -706,7 +813,7 @@
         .create());
     Change change = newChange(repo, commit, null, null, null).insert();
 
-    assertTrue(query("path:.*file.*").isEmpty());
+    assertThat(query("path:.*file.*")).isEmpty();
     assertResultEquals(change, queryOne("path:^dir.file.*"));
   }
 
@@ -726,7 +833,7 @@
     postReview.apply(new RevisionResource(
         changes.parse(change.getId()), ins.getPatchSet()), input);
 
-    assertTrue(query("comment:foo").isEmpty());
+    assertThat(query("comment:foo")).isEmpty();
     assertResultEquals(change, queryOne("comment:toplevel"));
     assertResultEquals(change, queryOne("comment:inline"));
   }
@@ -740,25 +847,25 @@
     Change change2 = newChange(repo, null, null, null, null).insert();
     clockStepMs = 0; // Queried by AgePredicate constructor.
     long now = TimeUtil.nowMs();
-    assertEquals(thirtyHours, lastUpdatedMs(change2) - lastUpdatedMs(change1));
-    assertEquals(thirtyHours, now - lastUpdatedMs(change2));
-    assertEquals(now, TimeUtil.nowMs());
+    assertThat(lastUpdatedMs(change2) - lastUpdatedMs(change1)).isEqualTo(thirtyHours);
+    assertThat(now - lastUpdatedMs(change2)).isEqualTo(thirtyHours);
+    assertThat(TimeUtil.nowMs()).isEqualTo(now);
 
-    assertTrue(query("-age:1d").isEmpty());
-    assertTrue(query("-age:" + (30*60-1) + "m").isEmpty());
+    assertThat(query("-age:1d")).isEmpty();
+    assertThat(query("-age:" + (30 * 60 - 1) + "m")).isEmpty();
     assertResultEquals(change2, queryOne("-age:2d"));
 
     List<ChangeInfo> results;
     results = query("-age:3d");
-    assertEquals(2, results.size());
+    assertThat(results).hasSize(2);
     assertResultEquals(change2, results.get(0));
     assertResultEquals(change1, results.get(1));
 
-    assertTrue(query("age:3d").isEmpty());
+    assertThat(query("age:3d")).isEmpty();
     assertResultEquals(change1, queryOne("age:2d"));
 
     results = query("age:1d");
-    assertEquals(2, results.size());
+    assertThat(results).hasSize(2);
     assertResultEquals(change2, results.get(0));
     assertResultEquals(change1, results.get(1));
   }
@@ -771,11 +878,11 @@
     Change change2 = newChange(repo, null, null, null, null).insert();
     clockStepMs = 0;
 
-    assertTrue(query("before:2009-09-29").isEmpty());
-    assertTrue(query("before:2009-09-30").isEmpty());
-    assertTrue(query("before:\"2009-09-30 16:59:00 -0400\"").isEmpty());
-    assertTrue(query("before:\"2009-09-30 20:59:00 -0000\"").isEmpty());
-    assertTrue(query("before:\"2009-09-30 20:59:00\"").isEmpty());
+    assertThat(query("before:2009-09-29")).isEmpty();
+    assertThat(query("before:2009-09-30")).isEmpty();
+    assertThat(query("before:\"2009-09-30 16:59:00 -0400\"")).isEmpty();
+    assertThat(query("before:\"2009-09-30 20:59:00 -0000\"")).isEmpty();
+    assertThat(query("before:\"2009-09-30 20:59:00\"")).isEmpty();
     assertResultEquals(change1,
         queryOne("before:\"2009-09-30 17:02:00 -0400\""));
     assertResultEquals(change1,
@@ -786,7 +893,7 @@
 
     List<ChangeInfo> results;
     results = query("before:2009-10-03");
-    assertEquals(2, results.size());
+    assertThat(results).hasSize(2);
     assertResultEquals(change2, results.get(0));
     assertResultEquals(change1, results.get(1));
   }
@@ -799,7 +906,7 @@
     Change change2 = newChange(repo, null, null, null, null).insert();
     clockStepMs = 0;
 
-    assertTrue(query("after:2009-10-03").isEmpty());
+    assertThat(query("after:2009-10-03")).isEmpty();
     assertResultEquals(change2,
         queryOne("after:\"2009-10-01 20:59:59 -0400\""));
     assertResultEquals(change2,
@@ -808,7 +915,7 @@
 
     List<ChangeInfo> results;
     results = query("after:2009-09-30");
-    assertEquals(2, results.size());
+    assertThat(results).hasSize(2);
     assertResultEquals(change2, results.get(0));
     assertResultEquals(change1, results.get(1));
   }
@@ -827,14 +934,14 @@
     Change change1 = newChange(repo, commit1, null, null, null).insert();
     Change change2 = newChange(repo, commit2, null, null, null).insert();
 
-    assertTrue(query("added:>4").isEmpty());
+    assertThat(query("added:>4")).isEmpty();
     assertResultEquals(change1, queryOne("added:3"));
     assertResultEquals(change1, queryOne("added:>2"));
     assertResultEquals(change1, queryOne("added:>=3"));
     assertResultEquals(change2, queryOne("added:<1"));
     assertResultEquals(change2, queryOne("added:<=0"));
 
-    assertTrue(query("deleted:>3").isEmpty());
+    assertThat(query("deleted:>3")).isEmpty();
     assertResultEquals(change2, queryOne("deleted:2"));
     assertResultEquals(change2, queryOne("deleted:>1"));
     assertResultEquals(change2, queryOne("deleted:>=2"));
@@ -842,7 +949,7 @@
     assertResultEquals(change1, queryOne("deleted:<=0"));
 
     for (String str : Lists.newArrayList("delta", "size")) {
-      assertTrue(query(str + ":<2").isEmpty());
+      assertThat(query(str + ":<2")).isEmpty();
       assertResultEquals(change1, queryOne(str + ":3"));
       assertResultEquals(change1, queryOne(str + ":>2"));
       assertResultEquals(change1, queryOne(str + ":>=3"));
@@ -851,6 +958,50 @@
     }
   }
 
+  private List<Change> setUpHashtagChanges() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change change1 = newChange(repo, null, null, null, null).insert();
+    Change change2 = newChange(repo, null, null, null, null).insert();
+
+    HashtagsInput in = new HashtagsInput();
+    in.add = ImmutableSet.of("foo");
+    gApi.changes().id(change1.getId().get()).setHashtags(in);
+
+    in.add = ImmutableSet.of("foo", "bar", "a tag");
+    gApi.changes().id(change2.getId().get()).setHashtags(in);
+
+    return ImmutableList.of(change1, change2);
+  }
+
+  @Test
+  public void byHashtagWithNotedb() throws Exception {
+    assume().that(notesMigration.enabled()).isTrue();
+    List<Change> changes = setUpHashtagChanges();
+    List<ChangeInfo> results = query("hashtag:foo");
+    assertThat(results).hasSize(2);
+    assertResultEquals(changes.get(1), results.get(0));
+    assertResultEquals(changes.get(0), results.get(1));
+    assertResultEquals(changes.get(1), queryOne("hashtag:bar"));
+    assertResultEquals(changes.get(1), queryOne("hashtag:\"a tag\""));
+    assertResultEquals(changes.get(1), queryOne("hashtag:\"a tag \""));
+    assertResultEquals(changes.get(1), queryOne("hashtag:\" a tag \""));
+    assertResultEquals(changes.get(1), queryOne("hashtag:\"#a tag\""));
+    assertResultEquals(changes.get(1), queryOne("hashtag:\"# #a tag\""));
+  }
+
+  @Test
+  public void byHashtagWithoutNotedb() throws Exception {
+    assume().that(notesMigration.enabled()).isFalse();
+    setUpHashtagChanges();
+    assertThat(query("hashtag:foo")).isEmpty();
+    assertThat(query("hashtag:bar")).isEmpty();
+    assertThat(query("hashtag:\" bar \"")).isEmpty();
+    assertThat(query("hashtag:\"a tag\"")).isEmpty();
+    assertThat(query("hashtag:\" a tag \"")).isEmpty();
+    assertThat(query("hashtag:#foo")).isEmpty();
+    assertThat(query("hashtag:\"# #foo\"")).isEmpty();
+  }
+
   @Test
   public void byDefault() throws Exception {
     TestRepository<InMemoryRepository> repo = createProject("repo");
@@ -884,6 +1035,7 @@
 
     assertResultEquals(change1,
         queryOne(Integer.toString(change1.getId().get())));
+    assertResultEquals(change1, queryOne(ChangeTriplet.format(change1)));
     assertResultEquals(change2, queryOne("foosubject"));
     assertResultEquals(change3, queryOne("Foo.java"));
     assertResultEquals(change4, queryOne("Code-Review+1"));
@@ -892,8 +1044,51 @@
     assertResultEquals(change6, queryOne("branch6"));
     assertResultEquals(change6, queryOne("refs/heads/branch6"));
 
-    assertEquals(6, query("user@example.com").size());
-    assertEquals(6, query("repo").size());
+    assertThat(query("user@example.com")).hasSize(6);
+    assertThat(query("repo")).hasSize(6);
+  }
+
+  @Test
+  public void implicitVisibleTo() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change change1 = newChange(repo, null, null, userId.get(), null).insert();
+    ChangeInserter ins2 = newChange(repo, null, null, userId.get(), null);
+    Change change2 = ins2.getChange();
+    change2.setStatus(Change.Status.DRAFT);
+    ins2.insert();
+
+    String q = "project:repo";
+    List<ChangeInfo> results = query(q);
+    assertThat(results).hasSize(2);
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+
+    // Second user cannot see first user's drafts.
+    requestContext.setContext(newRequestContext(accountManager
+        .authenticate(AuthRequest.forUser("anotheruser")).getAccountId()));
+    assertResultEquals(change1, queryOne(q));
+  }
+
+  @Test
+  public void explicitVisibleTo() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change change1 = newChange(repo, null, null, userId.get(), null).insert();
+    ChangeInserter ins2 = newChange(repo, null, null, userId.get(), null);
+    Change change2 = ins2.getChange();
+    change2.setStatus(Change.Status.DRAFT);
+    ins2.insert();
+
+    String q = "project:repo";
+    List<ChangeInfo> results = query(q);
+    assertThat(results).hasSize(2);
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+
+    // Second user cannot see first user's drafts.
+    Account.Id user2 = accountManager
+        .authenticate(AuthRequest.forUser("anotheruser"))
+        .getAccountId();
+    assertResultEquals(change1, queryOne(q + " visibleto:" + user2.get()));
   }
 
   protected ChangeInserter newChange(
@@ -904,7 +1099,7 @@
       commit = repo.parseBody(repo.commit().message("message").create());
     }
     Account.Id ownerId = owner != null ? new Account.Id(owner) : userId;
-    branch = Objects.firstNonNull(branch, "refs/heads/master");
+    branch = MoreObjects.firstNonNull(branch, "refs/heads/master");
     if (!branch.startsWith("refs/heads/")) {
       branch = "refs/heads/" + branch;
     }
@@ -926,26 +1121,35 @@
     Change change = new Change(new Change.Key(key), id, ownerId,
         new Branch.NameKey(project, branch), TimeUtil.nowTs());
     return changeFactory.create(
-        projectControlFactory.controlFor(project,
-          userFactory.create(ownerId)).controlFor(change).getRefControl(),
+        projectControlFactory.controlFor(project, userFactory.create(ownerId)),
         change,
         commit);
   }
 
   protected void assertResultEquals(Change expected, ChangeInfo actual) {
-    assertEquals(expected.getId().get(), actual._number);
+    assertThat(actual._number).isEqualTo(expected.getId().get());
   }
 
   protected void assertResultEquals(String message, Change expected,
       ChangeInfo actual) {
-    assertEquals(message, expected.getId().get(), actual._number);
+    assert_().withFailureMessage(message).that(actual._number)
+        .isEqualTo(expected.getId().get());
+  }
+
+  protected void assertBadQuery(Object query) throws Exception {
+    try {
+      query(query);
+      fail("expected BadRequestException for query: " + query);
+    } catch (BadRequestException e) {
+      // Expected.
+    }
   }
 
   protected TestRepository<InMemoryRepository> createProject(String name)
       throws Exception {
     CreateProject create = projectFactory.create(name);
     create.apply(TLR, new ProjectInput());
-    return new TestRepository<InMemoryRepository>(
+    return new TestRepository<>(
         repoManager.openRepository(new Project.NameKey(name)));
   }
 
@@ -958,16 +1162,17 @@
   @SuppressWarnings({"rawtypes", "unchecked"})
   protected List<ChangeInfo> query(QueryChanges q) throws Exception {
     Object result = q.apply(TLR);
-    assertTrue(
-        String.format("expected List<ChangeInfo>, found %s for [%s]",
-          result, q.getQuery(0)),
-        result instanceof List);
+    assert_()
+        .withFailureMessage(
+            String.format("expected List<ChangeInfo>, found %s for [%s]",
+                result, q.getQuery(0))).that(result).isInstanceOf(List.class);
     List results = (List) result;
     if (!results.isEmpty()) {
-      assertTrue(
-          String.format("expected ChangeInfo, found %s for [%s]",
-            result, q.getQuery(0)),
-          results.get(0) instanceof ChangeInfo);
+      assert_()
+          .withFailureMessage(
+              String.format("expected ChangeInfo, found %s for [%s]", result,
+                  q.getQuery(0))).that(results.get(0))
+          .isInstanceOf(ChangeInfo.class);
     }
     return (List<ChangeInfo>) result;
   }
@@ -978,10 +1183,11 @@
 
   protected ChangeInfo queryOne(Object query) throws Exception {
     List<ChangeInfo> results = query(query);
-    assertTrue(
-        String.format("expected singleton List<ChangeInfo>, found %s for [%s]",
-          results, query),
-        results.size() == 1);
+    assert_()
+        .withFailureMessage(
+            String.format(
+                "expected singleton List<ChangeInfo>, found %s for [%s]",
+                results, query)).that(results).hasSize(1);
     return results.get(0);
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
index 370dd9d..742c230 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
@@ -14,12 +14,43 @@
 
 package com.google.gerrit.server.query.change;
 
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.testutil.InMemoryModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
 public class LuceneQueryChangesTest extends AbstractQueryChangesTest {
+  @Override
   protected Injector createInjector() {
-    return Guice.createInjector(new InMemoryModule());
+    Config luceneConfig = new Config(config);
+    InMemoryModule.setDefaults(luceneConfig);
+    return Guice.createInjector(new InMemoryModule(luceneConfig));
+  }
+
+  @Test
+  public void fullTextWithSpecialChars() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    RevCommit commit1 =
+        repo.parseBody(repo.commit().message("foo_bar_foo").create());
+    Change change1 = newChange(repo, commit1, null, null, null).insert();
+    RevCommit commit2 =
+        repo.parseBody(repo.commit().message("one.two.three").create());
+    Change change2 = newChange(repo, commit2, null, null, null).insert();
+
+    assertTrue(query("message:foo_ba").isEmpty());
+    assertResultEquals(change1, queryOne("message:bar"));
+    assertResultEquals(change1, queryOne("message:foo_bar"));
+    assertResultEquals(change1, queryOne("message:foo bar"));
+    assertResultEquals(change2, queryOne("message:two"));
+    assertResultEquals(change2, queryOne("message:one.two"));
+    assertResultEquals(change2, queryOne("message:one two"));
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV7Test.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV7Test.java
deleted file mode 100644
index 1dcd83b..0000000
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV7Test.java
+++ /dev/null
@@ -1,185 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.MINUTES;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import com.google.common.collect.Lists;
-import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.change.ChangeInserter;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
-import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.testutil.InMemoryModule;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
-import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.Config;
-import org.junit.Ignore;
-import org.junit.Test;
-
-import java.util.List;
-
-public class LuceneQueryChangesV7Test extends AbstractQueryChangesTest {
-  protected Injector createInjector() {
-    Config cfg = InMemoryModule.newDefaultConfig();
-    cfg.setInt("index", "lucene", "testVersion", 7);
-    return Guice.createInjector(new InMemoryModule(cfg));
-  }
-
-  // Tests for features not supported in V7.
-  @Ignore
-  @Override
-  @Test
-  public void byProjectPrefix() {}
-
-  @Ignore
-  @Override
-  @Test
-  public void byDefault() {}
-
-  @Ignore
-  @Override
-  @Test
-  public void bySize() {}
-  // End tests for features not supported in V7.
-
-  @Test
-  public void pagination() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
-    List<Change> changes = Lists.newArrayList();
-    for (int i = 0; i < 5; i++) {
-      changes.add(newChange(repo, null, null, null, null).insert());
-    }
-
-    // Page forward and back through 3 pages of results.
-    QueryChanges q;
-    List<ChangeInfo> results;
-    results = query("status:new limit:2");
-    assertEquals(2, results.size());
-    assertResultEquals(changes.get(4), results.get(0));
-    assertResultEquals(changes.get(3), results.get(1));
-
-    q = newQuery("status:new limit:2");
-    q.setSortKeyBefore(results.get(1)._sortkey);
-    results = query(q);
-    assertEquals(2, results.size());
-    assertResultEquals(changes.get(2), results.get(0));
-    assertResultEquals(changes.get(1), results.get(1));
-
-    q = newQuery("status:new limit:2");
-    q.setSortKeyBefore(results.get(1)._sortkey);
-    results = query(q);
-    assertEquals(1, results.size());
-    assertResultEquals(changes.get(0), results.get(0));
-
-    q = newQuery("status:new limit:2");
-    q.setSortKeyAfter(results.get(0)._sortkey);
-    results = query(q);
-    assertEquals(2, results.size());
-    assertResultEquals(changes.get(2), results.get(0));
-    assertResultEquals(changes.get(1), results.get(1));
-
-    q = newQuery("status:new limit:2");
-    q.setSortKeyAfter(results.get(0)._sortkey);
-    results = query(q);
-    assertEquals(2, results.size());
-    assertResultEquals(changes.get(4), results.get(0));
-    assertResultEquals(changes.get(3), results.get(1));
-  }
-
-  @Override
-  @Test
-  public void updatedOrderWithSubMinuteResolution() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
-    ChangeInserter ins1 = newChange(repo, null, null, null, null);
-    Change change1 = ins1.insert();
-    Change change2 = newChange(repo, null, null, null, null).insert();
-
-    assertTrue(lastUpdatedMs(change1) < lastUpdatedMs(change2));
-
-    List<ChangeInfo> results;
-    results = query("status:new");
-    assertEquals(2, results.size());
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
-
-    ReviewInput input = new ReviewInput();
-    input.message = "toplevel";
-    postReview.apply(new RevisionResource(
-        changes.parse(change1.getId()), ins1.getPatchSet()), input);
-    change1 = db.changes().get(change1.getId());
-
-    assertTrue(lastUpdatedMs(change1) > lastUpdatedMs(change2));
-    assertTrue(lastUpdatedMs(change1) - lastUpdatedMs(change2)
-        < MILLISECONDS.convert(1, MINUTES));
-
-    results = query("status:new");
-    assertEquals(2, results.size());
-    // Same order as before change1 was modified.
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
-  }
-
-  @Test
-  public void sortKeyBreaksTiesOnChangeId() throws Exception {
-    clockStepMs = 0;
-    TestRepository<InMemoryRepository> repo = createProject("repo");
-    ChangeInserter ins1 = newChange(repo, null, null, null, null);
-    Change change1 = ins1.insert();
-    Change change2 = newChange(repo, null, null, null, null).insert();
-
-    ReviewInput input = new ReviewInput();
-    input.message = "toplevel";
-    postReview.apply(new RevisionResource(
-        changes.parse(change1.getId()), ins1.getPatchSet()), input);
-    change1 = db.changes().get(change1.getId());
-
-    assertEquals(change1.getLastUpdatedOn(), change2.getLastUpdatedOn());
-
-    List<ChangeInfo> results = query("status:new");
-    assertEquals(2, results.size());
-    // Updated at the same time, 2 > 1.
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
-  }
-
-  @Override
-  @Test
-  public void byTopic() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
-    ChangeInserter ins1 = newChange(repo, null, null, null, null);
-    Change change1 = ins1.getChange();
-    change1.setTopic("feature1");
-    ins1.insert();
-
-    ChangeInserter ins2 = newChange(repo, null, null, null, null);
-    Change change2 = ins2.getChange();
-    change2.setTopic("feature2");
-    ins2.insert();
-
-    newChange(repo, null, null, null, null).insert();
-
-    assertTrue(query("topic:\"\"").isEmpty());
-    assertTrue(query("topic:foo").isEmpty());
-    assertResultEquals(change1, queryOne("topic:feature1"));
-    assertResultEquals(change2, queryOne("topic:feature2"));
-  }
-}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexPathPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
index 8f1fa86..55d0f38 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexPathPredicateTest.java
@@ -79,7 +79,7 @@
   }
 
   private static RegexPathPredicate predicate(String pattern) {
-    return new RegexPathPredicate(ChangeQueryBuilder.FIELD_PATH, pattern);
+    return new RegexPathPredicate(pattern);
   }
 
   private static ChangeData change(String... files) throws OrmException {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
index 996aafa..0c8157d 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
@@ -70,21 +70,11 @@
   public void testGetCauses_CreateSchema() throws OrmException, SQLException,
       IOException {
     // Initially the schema should be empty.
-    //
-    {
-      final JdbcSchema d = (JdbcSchema) db.open();
-      try {
-        final String[] types = {"TABLE", "VIEW"};
-        final ResultSet rs =
-            d.getConnection().getMetaData().getTables(null, null, null, types);
-        try {
-          assertFalse(rs.next());
-        } finally {
-          rs.close();
-        }
-      } finally {
-        d.close();
-      }
+    String[] types = {"TABLE", "VIEW"};
+    try (JdbcSchema d = (JdbcSchema) db.open();
+        ResultSet rs = d.getConnection().getMetaData()
+          .getTables(null, null, null, types)) {
+      assertFalse(rs.next());
     }
 
     // Create the schema using the current schema version.
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
index d8b6048..8686fe6 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.schema;
 
+import static org.junit.Assert.assertEquals;
+
 import com.google.gerrit.reviewdb.client.SystemConfig;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -47,8 +49,6 @@
 import java.util.List;
 import java.util.UUID;
 
-import static org.junit.Assert.assertEquals;
-
 public class SchemaUpdaterTest {
   private InMemoryDatabase db;
 
@@ -74,7 +74,6 @@
       protected void configure() {
         bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {}).toInstance(db);
         bind(SitePaths.class).toInstance(paths);
-        install(new SchemaVersion.Module());
 
         Config cfg = new Config();
         cfg.setString("user", null, "name", "Gerrit Code Review");
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java
index 17fe068..9c8e86a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java
@@ -423,21 +423,17 @@
   }
 
   private DirCacheEntry file(final String name) throws IOException {
-    final ObjectInserter oi = repository.newObjectInserter();
-    try {
+    try (ObjectInserter oi = repository.newObjectInserter()) {
       final DirCacheEntry e = new DirCacheEntry(name);
       e.setFileMode(FileMode.REGULAR_FILE);
       e.setObjectId(oi.insert(Constants.OBJ_BLOB, Constants.encode(name)));
       oi.flush();
       return e;
-    } finally {
-      oi.release();
     }
   }
 
   private void setHEAD() throws Exception {
-    final ObjectInserter oi = repository.newObjectInserter();
-    try {
+    try (ObjectInserter oi = repository.newObjectInserter()) {
       final CommitBuilder commit = new CommitBuilder();
       commit.setTreeId(oi.insert(Constants.OBJ_TREE, new byte[] {}));
       commit.setAuthor(author);
@@ -456,8 +452,6 @@
         default:
           fail(Constants.HEAD + " did not change: " + ref.getResult());
       }
-    } finally {
-      oi.release();
     }
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/IdGeneratorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/IdGeneratorTest.java
index a2228d8..3be4f8a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/util/IdGeneratorTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/IdGeneratorTest.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.util;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
 import org.junit.Test;
 
 import java.util.HashSet;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
 public class IdGeneratorTest {
   @Test
   public void test1234() {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/LabelVoteTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/LabelVoteTest.java
index 0ed0ba8..4fdbdb2 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/util/LabelVoteTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/LabelVoteTest.java
@@ -23,23 +23,23 @@
   public void parse() {
     LabelVote l;
     l = LabelVote.parse("Code-Review-2");
-    assertEquals("Code-Review", l.getLabel());
-    assertEquals((short) -2, l.getValue());
+    assertEquals("Code-Review", l.label());
+    assertEquals((short) -2, l.value());
     l = LabelVote.parse("Code-Review-1");
-    assertEquals("Code-Review", l.getLabel());
-    assertEquals((short) -1, l.getValue());
+    assertEquals("Code-Review", l.label());
+    assertEquals((short) -1, l.value());
     l = LabelVote.parse("-Code-Review");
-    assertEquals("Code-Review", l.getLabel());
-    assertEquals((short) 0, l.getValue());
+    assertEquals("Code-Review", l.label());
+    assertEquals((short) 0, l.value());
     l = LabelVote.parse("Code-Review");
-    assertEquals("Code-Review", l.getLabel());
-    assertEquals((short) 1, l.getValue());
+    assertEquals("Code-Review", l.label());
+    assertEquals((short) 1, l.value());
     l = LabelVote.parse("Code-Review+1");
-    assertEquals("Code-Review", l.getLabel());
-    assertEquals((short) 1, l.getValue());
+    assertEquals("Code-Review", l.label());
+    assertEquals((short) 1, l.value());
     l = LabelVote.parse("Code-Review+2");
-    assertEquals("Code-Review", l.getLabel());
-    assertEquals((short) 2, l.getValue());
+    assertEquals("Code-Review", l.label());
+    assertEquals((short) 2, l.value());
   }
 
   @Test
@@ -55,26 +55,26 @@
   public void parseWithEquals() {
     LabelVote l;
     l = LabelVote.parseWithEquals("Code-Review=-2");
-    assertEquals("Code-Review", l.getLabel());
-    assertEquals((short) -2, l.getValue());
+    assertEquals("Code-Review", l.label());
+    assertEquals((short) -2, l.value());
     l = LabelVote.parseWithEquals("Code-Review=-1");
-    assertEquals("Code-Review", l.getLabel());
-    assertEquals((short) -1, l.getValue());
+    assertEquals("Code-Review", l.label());
+    assertEquals((short) -1, l.value());
     l = LabelVote.parseWithEquals("Code-Review=0");
-    assertEquals("Code-Review", l.getLabel());
-    assertEquals((short) 0, l.getValue());
+    assertEquals("Code-Review", l.label());
+    assertEquals((short) 0, l.value());
     l = LabelVote.parseWithEquals("Code-Review=1");
-    assertEquals("Code-Review", l.getLabel());
-    assertEquals((short) 1, l.getValue());
+    assertEquals("Code-Review", l.label());
+    assertEquals((short) 1, l.value());
     l = LabelVote.parseWithEquals("Code-Review=+1");
-    assertEquals("Code-Review", l.getLabel());
-    assertEquals((short) 1, l.getValue());
+    assertEquals("Code-Review", l.label());
+    assertEquals((short) 1, l.value());
     l = LabelVote.parseWithEquals("Code-Review=2");
-    assertEquals("Code-Review", l.getLabel());
-    assertEquals((short) 2, l.getValue());
+    assertEquals("Code-Review", l.label());
+    assertEquals((short) 2, l.value());
     l = LabelVote.parseWithEquals("Code-Review=+2");
-    assertEquals("Code-Review", l.getLabel());
-    assertEquals((short) 2, l.getValue());
+    assertEquals("Code-Review", l.label());
+    assertEquals((short) 2, l.value());
   }
 
   @Test
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/ParboiledTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/ParboiledTest.java
new file mode 100644
index 0000000..f406de1
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/ParboiledTest.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.parboiled.BaseParser;
+import org.parboiled.Parboiled;
+import org.parboiled.Rule;
+import org.parboiled.annotations.BuildParseTree;
+import org.parboiled.parserunners.ReportingParseRunner;
+import org.parboiled.support.ParseTreeUtils;
+import org.parboiled.support.ParsingResult;
+
+public class ParboiledTest {
+
+  private static final String EXPECTED =
+  "[Expression] '42'\n" +
+  "  [Term] '42'\n" +
+  "    [Factor] '42'\n" +
+  "      [Number] '42'\n" +
+  "        [0..9] '4'\n" +
+  "        [0..9] '2'\n" +
+  "    [ZeroOrMore]\n" +
+  "  [ZeroOrMore]\n";
+
+  private CalculatorParser parser;
+
+  @Before
+  public void setUp() {
+    parser = Parboiled.createParser(CalculatorParser.class);
+  }
+
+  @Test
+  public void test() {
+    ParsingResult<String> result =
+        new ReportingParseRunner<String>(parser.Expression()).run("42");
+    assertThat(result.hasErrors()).isFalse();
+    // next test is optional; we could stop here.
+    assertThat(ParseTreeUtils.printNodeTree(result)).isEqualTo(EXPECTED);
+  }
+
+  @BuildParseTree
+  static class CalculatorParser extends BaseParser<Object> {
+    Rule Expression() {
+      return Sequence(Term(), ZeroOrMore(AnyOf("+-"), Term()));
+    }
+
+    Rule Term() {
+      return Sequence(Factor(), ZeroOrMore(AnyOf("*/"), Factor()));
+    }
+
+    Rule Factor() {
+      return FirstOf(Number(), Sequence('(', Expression(), ')'));
+    }
+
+    Rule Number() {
+      return OneOrMore(CharRange('0', '9'));
+    }
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/RegexListSearcherTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/RegexListSearcherTest.java
new file mode 100644
index 0000000..8f73005
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/RegexListSearcherTest.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Ordering;
+
+import org.junit.Test;
+
+import java.util.List;
+
+public class RegexListSearcherTest {
+  private static final List<String> EMPTY = ImmutableList.of();
+
+  @Test
+  public void emptyList() {
+    assertSearchReturns(EMPTY, "pat", EMPTY);
+  }
+
+  @Test
+  public void hasMatch() {
+    List<String> list = ImmutableList.of("bar", "foo", "quux");
+    assertTrue(RegexListSearcher.ofStrings("foo").hasMatch(list));
+    assertFalse(RegexListSearcher.ofStrings("xyz").hasMatch(list));
+  }
+
+  @Test
+  public void anchors() {
+    List<String> list = ImmutableList.of("foo");
+    assertSearchReturns(list, "^f.*", list);
+    assertSearchReturns(list, "^f.*o$", list);
+    assertSearchReturns(list, "f.*o$", list);
+    assertSearchReturns(list, "f.*o$", list);
+    assertSearchReturns(EMPTY, "^.*\\$", list);
+  }
+
+  @Test
+  public void noCommonPrefix() {
+    List<String> list = ImmutableList.of("bar", "foo", "quux");
+    assertSearchReturns(ImmutableList.of("foo"), "f.*", list);
+    assertSearchReturns(ImmutableList.of("foo"), ".*o.*", list);
+    assertSearchReturns(ImmutableList.of("bar", "foo", "quux"), ".*[aou].*",
+        list);
+  }
+
+  @Test
+  public void commonPrefix() {
+    List<String> list = ImmutableList.of(
+        "bar",
+        "baz",
+        "foo1",
+        "foo2",
+        "foo3",
+        "quux");
+    assertSearchReturns(ImmutableList.of("bar", "baz"), "b.*", list);
+    assertSearchReturns(ImmutableList.of("foo1", "foo2"), "foo[12]", list);
+    assertSearchReturns(ImmutableList.of("foo1", "foo2", "foo3"), "foo.*",
+        list);
+    assertSearchReturns(ImmutableList.of("quux"), "q.*", list);
+  }
+
+  private void assertSearchReturns(List<?> expected, String re,
+    List<String> inputs) {
+    assertTrue(Ordering.natural().isOrdered(inputs));
+    assertEquals(expected,
+        ImmutableList.copyOf(RegexListSearcher.ofStrings(re).search(inputs)));
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/ConfigSuite.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/ConfigSuite.java
index 1da3c30..707dd12 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/ConfigSuite.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/ConfigSuite.java
@@ -19,7 +19,7 @@
 import static java.lang.annotation.ElementType.METHOD;
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.Lists;
 
 import org.junit.runner.Runner;
@@ -43,26 +43,26 @@
  * tests is created with the {@link Parameter} field set to the config.
  *
  * <pre>
- * @RunWith(ConfigSuite.class)
+ * {@literal @}RunWith(ConfigSuite.class)
  * public abstract class MyAbstractTest {
- *   @ConfigSuite.Parameter
+ *   {@literal @}ConfigSuite.Parameter
  *   protected Config cfg;
  *
- *   @ConfigSuite.Config
+ *   {@literal @}ConfigSuite.Config
  *   public static Config firstConfig() {
  *     Config cfg = new Config();
  *     cfg.setString("gerrit", null, "testValue", "a");
  *   }
  * }
  *
- * public class MyTest {
- *   @ConfigSuite.Config
+ * public class MyTest extends MyAbstractTest {
+ *   {@literal @}ConfigSuite.Config
  *   public static Config secondConfig() {
  *     Config cfg = new Config();
  *     cfg.setString("gerrit", null, "testValue", "b");
  *   }
  *
- *   @Test
+ *   {@literal @}Test
  *   public void myTest() {
  *     // Test using cfg.
  *   }
@@ -75,12 +75,20 @@
  *   <li><strong>firstConfig</strong>: {@code MyTest.myTest[firstConfig]}</li>
  *   <li><strong>secondConfig</strong>: {@code MyTest.myTest[secondConfig]}</li>
  * </ul>
+ *
+ * Additionally, config values used by <strong>default</strong> can be set
+ * in a method annotated with {@code @ConfigSuite.Default}.
  */
 public class ConfigSuite extends Suite {
   private static final String DEFAULT = "default";
 
   @Target({METHOD})
   @Retention(RUNTIME)
+  public static @interface Default {
+  }
+
+  @Target({METHOD})
+  @Retention(RUNTIME)
   public static @interface Config {
   }
 
@@ -111,7 +119,7 @@
 
     @Override
     protected String getName() {
-      return Objects.firstNonNull(name, DEFAULT);
+      return MoreObjects.firstNonNull(name, DEFAULT);
     }
 
     @Override
@@ -122,11 +130,12 @@
   }
 
   private static List<Runner> runnersFor(Class<?> clazz) {
+    Method defaultConfig = getDefaultConfig(clazz);
     List<Method> configs = getConfigs(clazz);
     Field field = getParameterField(clazz);
     List<Runner> result = Lists.newArrayListWithCapacity(configs.size() + 1);
     try {
-      result.add(new ConfigRunner(clazz, field, null, null));
+      result.add(new ConfigRunner(clazz, field, null, defaultConfig));
       for (Method m : configs) {
         result.add(new ConfigRunner(clazz, field, m.getName(), m));
       }
@@ -136,6 +145,20 @@
     }
   }
 
+  private static Method getDefaultConfig(Class<?> clazz) {
+    Method result = null;
+    for (Method m : clazz.getMethods()) {
+      Default ann = m.getAnnotation(Default.class);
+      if (ann != null) {
+        checkArgument(result == null,
+            "Multiple methods annotated with @ConfigSuite.Method: %s, %s",
+            result, m);
+        result = m;
+      }
+    }
+    return result;
+  }
+
   private static List<Method> getConfigs(Class<?> clazz) {
     List<Method> result = Lists.newArrayListWithExpectedSize(3);
     for (Method m : clazz.getMethods()) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountByEmailCache.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountByEmailCache.java
new file mode 100644
index 0000000..c3bfe1e
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountByEmailCache.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.testutil;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.SetMultimap;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.AccountByEmailCache;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/** Fake implementation of {@link AccountByEmailCache} for testing. */
+public class FakeAccountByEmailCache implements AccountByEmailCache {
+  private final SetMultimap<String, Account.Id> byEmail;
+  private final Set<Account.Id> anyEmail;
+
+  public FakeAccountByEmailCache() {
+    byEmail = HashMultimap.create();
+    anyEmail = new HashSet<>();
+  }
+
+  @Override
+  public synchronized Set<Account.Id> get(String email) {
+    return Collections.unmodifiableSet(
+        Sets.union(byEmail.get(email), anyEmail));
+  }
+
+  @Override
+  public synchronized void evict(String email) {
+    // Do nothing.
+  }
+
+  public synchronized void put(String email, Account.Id id) {
+    byEmail.put(email, id);
+  }
+
+  public synchronized void putAny(Account.Id id) {
+    anyEmail.add(id);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java
index db1ed05..011d69d 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java
@@ -16,12 +16,12 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.util.TimeUtil;
 
 import java.util.Map;
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/FilesystemLoggingMockingTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/FilesystemLoggingMockingTestCase.java
index 0ccdae7..7f8fc32 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/FilesystemLoggingMockingTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/FilesystemLoggingMockingTestCase.java
@@ -170,6 +170,7 @@
     }
   }
 
+  @Override
   public void tearDown() throws Exception {
     cleanupCreatedFiles();
     super.tearDown();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
index 6353904..49fcc96 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
@@ -67,7 +67,6 @@
     }
   }
 
-  private final SchemaVersion schemaVersion;
   private final SchemaCreator schemaCreator;
 
   private Connection openHandle;
@@ -75,9 +74,7 @@
   private boolean created;
 
   @Inject
-  InMemoryDatabase(SchemaVersion schemaVersion,
-      SchemaCreator schemaCreator) throws OrmException {
-    this.schemaVersion = schemaVersion;
+  InMemoryDatabase(SchemaCreator schemaCreator) throws OrmException {
     this.schemaCreator = schemaCreator;
 
     try {
@@ -161,6 +158,6 @@
 
   public void assertSchemaVersion() throws OrmException {
     final CurrentSchemaVersion act = getSchemaVersion();
-    assertEquals(schemaVersion.getVersionNbr(), act.versionNbr);
+    assertEquals(SchemaVersion.getBinaryVersion(), act.versionNbr);
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
index d96e861..fc74c20 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
-import com.google.gerrit.server.change.MergeabilityChecksExecutorModule;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.config.AllUsersName;
@@ -51,10 +50,10 @@
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
 import com.google.gerrit.server.mail.SmtpEmailSender;
 import com.google.gerrit.server.patch.DiffExecutor;
-import com.google.gerrit.server.schema.Current;
 import com.google.gerrit.server.schema.DataSourceType;
 import com.google.gerrit.server.schema.SchemaCreator;
-import com.google.gerrit.server.schema.SchemaVersion;
+import com.google.gerrit.server.securestore.DefaultSecureStore;
+import com.google.gerrit.server.securestore.SecureStore;
 import com.google.gerrit.server.ssh.NoSshKeyCache;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
@@ -82,6 +81,11 @@
 public class InMemoryModule extends FactoryModule {
   public static Config newDefaultConfig() {
     Config cfg = new Config();
+    setDefaults(cfg);
+    return cfg;
+  }
+
+  public static void setDefaults(Config cfg) {
     cfg.setEnum("auth", null, "type", AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT);
     cfg.setString("gerrit", null, "basePath", "git");
     cfg.setString("gerrit", null, "allProjects", "Test-Projects");
@@ -93,7 +97,6 @@
     cfg.setBoolean("index", "lucene", "testInmemory", true);
     cfg.setInt("index", "lucene", "testVersion",
         ChangeSchemas.getLatest().getVersion());
-    return cfg;
   }
 
   private final Config cfg;
@@ -125,8 +128,6 @@
 
     bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
 
-    install(new SchemaVersion.Module());
-
     bind(File.class).annotatedWith(SitePath.class).toInstance(new File("."));
     bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(cfg);
     bind(SocketAddress.class).annotatedWith(RemotePeer.class).toInstance(
@@ -152,6 +153,8 @@
     bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {})
         .to(InMemoryDatabase.class);
 
+    bind(SecureStore.class).to(DefaultSecureStore.class);
+
     bind(ChangeHooks.class).to(DisabledChangeHooks.class);
     install(NoSshKeyCache.module());
     install(new CanonicalWebUrlModule() {
@@ -169,13 +172,12 @@
       @Singleton
       @DiffExecutor
       public ExecutorService createDiffExecutor() {
-        return MoreExecutors.sameThreadExecutor();
+        return MoreExecutors.newDirectExecutorService();
       }
     });
     install(new DefaultCacheFactory.Module());
     install(new SmtpEmailSender.Module());
     install(new SignedTokenEmailTokenVerifier.Module());
-    install(new MergeabilityChecksExecutorModule());
 
     IndexType indexType = null;
     try {
@@ -206,9 +208,9 @@
 
   @Provides
   @Singleton
-  InMemoryDatabase getInMemoryDatabase(@Current SchemaVersion schemaVersion,
-      SchemaCreator schemaCreator) throws OrmException {
-    return new InMemoryDatabase(schemaVersion, schemaCreator);
+  InMemoryDatabase getInMemoryDatabase(SchemaCreator schemaCreator)
+      throws OrmException {
+    return new InMemoryDatabase(schemaCreator);
   }
 
   private Module luceneIndexModule() {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
index 328df75..0635464 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
@@ -80,6 +80,12 @@
   }
 
   @Override
+  public InMemoryRepository openMetadataRepository(Project.NameKey name)
+      throws RepositoryNotFoundException {
+    return openRepository(name);
+  }
+
+  @Override
   public SortedSet<Project.NameKey> list() {
     SortedSet<Project.NameKey> names = Sets.newTreeSet();
     for (DfsRepository repo : repos.values()) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TempFileUtil.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TempFileUtil.java
similarity index 94%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TempFileUtil.java
rename to gerrit-server/src/test/java/com/google/gerrit/testutil/TempFileUtil.java
index 0b78f57..72c2b5a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TempFileUtil.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TempFileUtil.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.acceptance;
+package com.google.gerrit.testutil;
 
 import java.io.File;
 import java.io.IOException;
@@ -22,7 +22,7 @@
 public class TempFileUtil {
   private static List<File> allDirsCreated = new ArrayList<>();
 
-  public synchronized static File createTempDirectory() throws IOException {
+  public static synchronized File createTempDirectory() throws IOException {
     File tmp = File.createTempFile("gerrit_test_", "").getCanonicalFile();
     if (!tmp.delete() || !tmp.mkdir()) {
       throw new IOException("Cannot create " + tmp.getPath());
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
index 1a0ccaf..675634e 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
@@ -17,62 +17,96 @@
 import static org.easymock.EasyMock.expect;
 
 import com.google.common.collect.Ordering;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.ChangeDraftUpdate;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Injector;
 
 import org.easymock.EasyMock;
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * Utility functions to create and manipulate Change, ChangeUpdate, and
  * ChangeControl objects for testing.
  */
 public class TestChanges {
-  public static Change newChange(Project.NameKey project, IdentifiedUser user) {
-    Change.Id changeId = new Change.Id(1);
+  private static final AtomicInteger nextChangeId = new AtomicInteger(1);
+
+  public static Change newChange(Project.NameKey project, Account.Id userId) {
+    return newChange(project, userId, nextChangeId.getAndIncrement());
+  }
+
+  public static Change newChange(Project.NameKey project, Account.Id userId,
+      int id) {
+    Change.Id changeId = new Change.Id(id);
     Change c = new Change(
         new Change.Key("Iabcd1234abcd1234abcd1234abcd1234abcd1234"),
         changeId,
-        user.getAccount().getId(),
+        userId,
         new Branch.NameKey(project, "master"),
         TimeUtil.nowTs());
     incrementPatchSet(c);
     return c;
   }
 
+  public static PatchSet newPatchSet(PatchSet.Id id, ObjectId revision,
+      Account.Id userId) {
+    return newPatchSet(id, revision.name(), userId);
+  }
+
+  public static PatchSet newPatchSet(PatchSet.Id id, String revision,
+      Account.Id userId) {
+    PatchSet ps = new PatchSet(id);
+    ps.setRevision(new RevId(revision));
+    ps.setUploader(userId);
+    ps.setCreatedOn(TimeUtil.nowTs());
+    return ps;
+  }
+
   public static ChangeUpdate newUpdate(Injector injector,
-      GitRepositoryManager repoManager, Change c, final IdentifiedUser user)
+      GitRepositoryManager repoManager, NotesMigration migration, Change c,
+      final AllUsersNameProvider allUsers, final IdentifiedUser user)
       throws OrmException {
     return injector.createChildInjector(new FactoryModule() {
       @Override
       public void configure() {
         factory(ChangeUpdate.Factory.class);
+        factory(ChangeDraftUpdate.Factory.class);
         bind(IdentifiedUser.class).toInstance(user);
+        bind(AllUsersName.class).toProvider(allUsers);
       }
     }).getInstance(ChangeUpdate.Factory.class).create(
-        stubChangeControl(repoManager, c, user), TimeUtil.nowTs(),
-        Ordering.<String> natural());
+        stubChangeControl(repoManager, migration, c, allUsers, user),
+        TimeUtil.nowTs(), Ordering.<String> natural());
   }
 
   public static ChangeControl stubChangeControl(
-      GitRepositoryManager repoManager, Change c, IdentifiedUser user)
-      throws OrmException {
+      GitRepositoryManager repoManager, NotesMigration migration,
+      Change c, AllUsersNameProvider allUsers,
+      IdentifiedUser user) throws OrmException {
     ChangeControl ctl = EasyMock.createNiceMock(ChangeControl.class);
     expect(ctl.getChange()).andStubReturn(c);
     expect(ctl.getCurrentUser()).andStubReturn(user);
-    ChangeNotes notes = new ChangeNotes(repoManager, c);
-    notes = notes.load();
+    ChangeNotes notes = new ChangeNotes(repoManager, migration, allUsers, c)
+        .load();
     expect(ctl.getNotes()).andStubReturn(notes);
     EasyMock.replay(ctl);
     return ctl;
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 830db27..78f5265 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,8 +41,6 @@
 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.gerrit.server.query.change.SortKeyPredicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
@@ -50,7 +48,6 @@
 import org.apache.lucene.analysis.standard.StandardAnalyzer;
 import org.apache.lucene.analysis.util.CharArraySet;
 import org.apache.lucene.search.Query;
-import org.apache.lucene.util.Version;
 import org.apache.solr.client.solrj.SolrQuery;
 import org.apache.solr.client.solrj.SolrQuery.SortClause;
 import org.apache.solr.client.solrj.SolrServer;
@@ -108,13 +105,8 @@
       throw new IllegalStateException("index.url must be supplied");
     }
 
-    // Version is only used to determine the list of stop words used by the
-    // analyzer, so use the latest version rather than trying to match the Solr
-    // server version.
-    @SuppressWarnings("deprecation")
-    Version v = Version.LUCENE_CURRENT;
     queryBuilder = new QueryBuilder(
-        schema, new StandardAnalyzer(v, CharArraySet.EMPTY_SET));
+        new StandardAnalyzer(CharArraySet.EMPTY_SET));
 
     base = Strings.nullToEmpty(base);
     openIndex = new CloudSolrServer(url);
@@ -147,25 +139,6 @@
   }
 
   @Override
-  public void insert(ChangeData cd) throws IOException {
-    String id = cd.getId().toString();
-    SolrInputDocument doc = toDocument(cd);
-    try {
-      if (cd.change().getStatus().isOpen()) {
-        closedIndex.deleteById(id);
-        openIndex.add(doc);
-      } else {
-        openIndex.deleteById(id);
-        closedIndex.add(doc);
-      }
-    } catch (OrmException | SolrServerException e) {
-      throw new IOException(e);
-    }
-    commit(openIndex);
-    commit(closedIndex);
-  }
-
-  @Override
   public void replace(ChangeData cd) throws IOException {
     String id = cd.getId().toString();
     SolrInputDocument doc = toDocument(cd);
@@ -185,22 +158,8 @@
   }
 
   @Override
-  public void delete(ChangeData cd) throws IOException {
-    String id = cd.getId().toString();
-    try {
-      if (cd.change().getStatus().isOpen()) {
-        delete(id, openIndex);
-      } else {
-        delete(id, closedIndex);
-      }
-    } catch (OrmException e) {
-      throw new IOException(e);
-    }
-  }
-
-  @Override
-  public void delete(int id) throws IOException {
-    String idString = Integer.toString(id);
+  public void delete(Change.Id id) throws IOException {
+    String idString = Integer.toString(id.get());
     delete(idString, openIndex);
     delete(idString, closedIndex);
   }
@@ -238,23 +197,15 @@
       indexes.add(closedIndex);
     }
     return new QuerySource(indexes, queryBuilder.toQuery(p), start, limit,
-        getSorts(schema, p));
+        getSorts());
   }
 
-  @SuppressWarnings("deprecation")
-  private static List<SortClause> getSorts(Schema<ChangeData> schema,
-      Predicate<ChangeData> p) {
-    if (SortKeyPredicate.hasSortKeyField(schema)) {
-      boolean reverse = ChangeQueryBuilder.hasNonTrivialSortKeyAfter(schema, p);
-      return ImmutableList.of(new SortClause(ChangeField.SORTKEY.getName(),
-          !reverse ? SolrQuery.ORDER.desc : SolrQuery.ORDER.asc));
-    } else {
-      return ImmutableList.of(
-          new SortClause(
-            ChangeField.UPDATED.getName(), SolrQuery.ORDER.desc),
-          new SortClause(
-            ChangeField.LEGACY_ID.getName(), SolrQuery.ORDER.desc));
-    }
+  private static List<SortClause> getSorts() {
+    return ImmutableList.of(
+        new SortClause(
+          ChangeField.UPDATED.getName(), SolrQuery.ORDER.desc),
+        new SortClause(
+          ChangeField.LEGACY_ID.getName(), SolrQuery.ORDER.desc));
   }
 
   private void commit(SolrServer server) throws IOException {
@@ -266,12 +217,12 @@
   }
 
   private class QuerySource implements ChangeDataSource {
-    private final List<SolrServer> indexes;
+    private final List<SolrServer> servers;
     private final SolrQuery query;
 
     public QuerySource(List<SolrServer> indexes, Query q, int start, int limit,
         List<SortClause> sorts) {
-      this.indexes = indexes;
+      this.servers = indexes;
 
       query = new SolrQuery(q.toString());
       query.setParam("shards.tolerant", true);
@@ -303,7 +254,7 @@
       try {
         // TODO Sort documents during merge to select only top N.
         SolrDocumentList docs = new SolrDocumentList();
-        for (SolrServer index : indexes) {
+        for (SolrServer index : servers) {
           docs.addAll(index.query(query).getResults());
         }
 
@@ -337,49 +288,35 @@
     }
   }
 
-  private SolrInputDocument toDocument(ChangeData cd) throws IOException {
-    try {
-      SolrInputDocument result = new SolrInputDocument();
-      for (Values<ChangeData> values : schema.buildFields(cd, fillArgs)) {
-        add(result, values);
-      }
-      return result;
-    } catch (OrmException e) {
-      throw new IOException(e);
+  private SolrInputDocument toDocument(ChangeData cd) {
+    SolrInputDocument result = new SolrInputDocument();
+    for (Values<ChangeData> values : schema.buildFields(cd, fillArgs)) {
+      add(result, values);
     }
+    return result;
   }
 
-  private void add(SolrInputDocument doc, Values<ChangeData> values)
-      throws OrmException {
+  private void add(SolrInputDocument doc, Values<ChangeData> values) {
     String name = values.getField().getName();
     FieldType<?> type = values.getField().getType();
 
     if (type == FieldType.INTEGER) {
       for (Object value : values.getValues()) {
-        doc.addField(name, (Integer) value);
+        doc.addField(name, value);
       }
     } else if (type == FieldType.LONG) {
       for (Object value : values.getValues()) {
-        doc.addField(name, (Long) value);
+        doc.addField(name, value);
       }
     } else if (type == FieldType.TIMESTAMP) {
-      @SuppressWarnings("deprecation")
-      boolean legacy = values.getField() == ChangeField.LEGACY_UPDATED;
-      if (legacy) {
-        for (Object value : values.getValues()) {
-          int t = queryBuilder.toIndexTimeInMinutes((Timestamp) value);
-          doc.addField(name, t);
-        }
-      } else {
-        for (Object value : values.getValues()) {
-          doc.addField(name, ((Timestamp) value).getTime());
-        }
+      for (Object value : values.getValues()) {
+        doc.addField(name, ((Timestamp) value).getTime());
       }
     } else if (type == FieldType.EXACT
         || type == FieldType.PREFIX
         || type == FieldType.FULL_TEXT) {
       for (Object value : values.getValues()) {
-        doc.addField(name, (String) value);
+        doc.addField(name, value);
       }
     } else {
       throw QueryBuilder.badFieldType(type);
diff --git a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java
index 7474d0f..38de6ee 100644
--- a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java
+++ b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.index.ChangeSchemas;
 import com.google.gerrit.server.index.FieldDef.FillArgs;
 import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.IndexConfig;
 import com.google.gerrit.server.index.IndexModule;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Provider;
@@ -49,6 +50,7 @@
 
   @Override
   protected void configure() {
+    bind(IndexConfig.class).toInstance(IndexConfig.createDefault());
     install(new IndexModule(threads));
     bind(ChangeIndex.class).to(SolrChangeIndex.class);
     listener().to(SolrChangeIndex.class);
diff --git a/gerrit-sshd/BUCK b/gerrit-sshd/BUCK
index fad371a..4774cb3 100644
--- a/gerrit-sshd/BUCK
+++ b/gerrit-sshd/BUCK
@@ -17,6 +17,7 @@
     '//lib:guava',
     '//lib:gwtorm',
     '//lib:jsch',
+    '//lib/auto:auto-value',
     '//lib/commons:codec',
     '//lib/commons:collections',
     '//lib/guice:guice',
@@ -51,6 +52,7 @@
     '//gerrit-server:server',
     '//lib:guava',
     '//lib:junit',
+    '//lib/mina:sshd',
   ],
   source_under_test = [':sshd'],
 )
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
index 40f7c7d..c0fd2ac 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
@@ -16,9 +16,9 @@
 
 import com.google.common.util.concurrent.Atomics;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RequestCleanup;
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.git.WorkQueue.CancelableRunnable;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gerrit.sshd.SshScope.Context;
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gerrit.util.cli.EndOfOptionsHandler;
@@ -111,18 +110,22 @@
     task = Atomics.newReference();
   }
 
+  @Override
   public void setInputStream(final InputStream in) {
     this.in = in;
   }
 
+  @Override
   public void setOutputStream(final OutputStream out) {
     this.out = out;
   }
 
+  @Override
   public void setErrorStream(final OutputStream err) {
     this.err = err;
   }
 
+  @Override
   public void setExitCallback(final ExitCallback callback) {
     this.exit = callback;
   }
@@ -475,7 +478,7 @@
     }
 
     @Override
-    public NameKey getProjectNameKey() {
+    public Project.NameKey getProjectNameKey() {
       return projectName;
     }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index f394766..56441fba 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -95,6 +95,7 @@
   @Override
   public CommandFactory get() {
     return new CommandFactory() {
+      @Override
       public Command createCommand(final String requestCommand) {
         return new Trampoline(requestCommand);
       }
@@ -121,31 +122,38 @@
       task = Atomics.newReference();
     }
 
+    @Override
     public void setInputStream(final InputStream in) {
       this.in = in;
     }
 
+    @Override
     public void setOutputStream(final OutputStream out) {
       this.out = out;
     }
 
+    @Override
     public void setErrorStream(final OutputStream err) {
       this.err = err;
     }
 
+    @Override
     public void setExitCallback(final ExitCallback callback) {
       this.exit = callback;
     }
 
+    @Override
     public void setSession(final ServerSession session) {
       final SshSession s = session.getAttribute(SshSession.KEY);
       this.ctx = sshScope.newContext(schemaFactory, s, commandLine);
     }
 
+    @Override
     public void start(final Environment env) throws IOException {
       this.env = env;
       final Context ctx = this.ctx;
       task.set(startExecutor.submit(new Runnable() {
+        @Override
         public void run() {
           try {
             onStart();
@@ -245,7 +253,7 @@
   }
 
   /** Split a command line into a string array. */
-  static public String[] split(String commandLine) {
+  public static String[] split(String commandLine) {
     final List<String> list = new ArrayList<>();
     boolean inquote = false;
     boolean inDblQuote = false;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
index 78f006b..a63abee 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
@@ -54,6 +54,7 @@
     this.shell = shell;
   }
 
+  @Override
   public Command create() {
     return shell.get();
   }
@@ -77,27 +78,33 @@
       this.sshScope = sshScope;
     }
 
+    @Override
     public void setInputStream(final InputStream in) {
       this.in = in;
     }
 
+    @Override
     public void setOutputStream(final OutputStream out) {
       this.out = out;
     }
 
+    @Override
     public void setErrorStream(final OutputStream err) {
       this.err = err;
     }
 
+    @Override
     public void setExitCallback(final ExitCallback callback) {
       this.exit = callback;
     }
 
+    @Override
     public void setSession(final ServerSession session) {
       SshSession s = session.getAttribute(SshSession.KEY);
       this.context = sshScope.newContext(schemaFactory, s, "");
     }
 
+    @Override
     public void start(final Environment env) throws IOException {
       Context old = sshScope.set(context);
       String message;
@@ -115,6 +122,7 @@
       exit.onExit(127);
     }
 
+    @Override
     public void destroy() {
     }
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
index 334f155..b5378f1 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
@@ -16,12 +16,11 @@
 
 import com.google.common.base.Preconditions;
 import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.inject.Inject;
 import com.google.inject.binder.LinkedBindingBuilder;
 
 import org.apache.sshd.server.Command;
 
-import javax.inject.Inject;
-
 public abstract class PluginCommandModule extends CommandModule {
   private CommandName command;
 
@@ -39,6 +38,7 @@
 
   protected abstract void configureCommands();
 
+  @Override
   protected LinkedBindingBuilder<Command> command(String subCmd) {
     return bind(Commands.key(command, subCmd));
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SingleCommandPluginModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SingleCommandPluginModule.java
index 0c5fca3..14911b5 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SingleCommandPluginModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SingleCommandPluginModule.java
@@ -16,12 +16,11 @@
 
 import com.google.common.base.Preconditions;
 import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.inject.Inject;
 import com.google.inject.binder.LinkedBindingBuilder;
 
 import org.apache.sshd.server.Command;
 
-import javax.inject.Inject;
-
 /**
  * Binds one SSH command to the plugin name itself.
  * <p>
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
index afb2537..8c43438a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
@@ -58,6 +58,7 @@
     }
   }
 
+  @Override
   public void setPluginName(String name) {
     command = Commands.named(name);
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
index 7b910b2..39eb720 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
@@ -45,6 +45,7 @@
 import org.apache.sshd.common.KeyExchange;
 import org.apache.sshd.common.KeyPairProvider;
 import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.Random;
 import org.apache.sshd.common.RequestHandler;
 import org.apache.sshd.common.Session;
 import org.apache.sshd.common.Signature;
@@ -102,6 +103,8 @@
 import org.apache.sshd.server.kex.DHG1;
 import org.apache.sshd.server.kex.DHG14;
 import org.apache.sshd.server.session.SessionFactory;
+import org.bouncycastle.crypto.prng.RandomGenerator;
+import org.bouncycastle.crypto.prng.VMPCRandomGenerator;
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -142,6 +145,7 @@
  */
 @Singleton
 public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
+  @SuppressWarnings("hiding") // Don't use AbstractCloseable's logger.
   private static final Logger log = LoggerFactory.getLogger(SshDaemon.class);
 
   public static enum SshSessionBackend {
@@ -153,7 +157,7 @@
   private final List<String> advertised;
   private final boolean keepAlive;
   private final List<HostKey> hostKeys;
-  private volatile IoAcceptor acceptor;
+  private volatile IoAcceptor daemonAcceptor;
   private final Config cfg;
 
   @Inject
@@ -219,7 +223,7 @@
             : Nio2ServiceFactoryFactory.class.getName());
 
     if (SecurityUtils.isBouncyCastleRegistered()) {
-      initProviderBouncyCastle();
+      initProviderBouncyCastle(cfg);
     } else {
       initProviderJce();
     }
@@ -289,26 +293,26 @@
   }
 
   public IoAcceptor getIoAcceptor() {
-    return acceptor;
+    return daemonAcceptor;
   }
 
   @Override
   public synchronized void start() {
-    if (acceptor == null && !listen.isEmpty()) {
+    if (daemonAcceptor == null && !listen.isEmpty()) {
       checkConfig();
       if (sessionFactory == null) {
         sessionFactory = createSessionFactory();
       }
       sessionFactory.setServer(this);
-      acceptor = createAcceptor();
+      daemonAcceptor = createAcceptor();
 
       try {
         String listenAddress = cfg.getString("sshd", null, "listenAddress");
         boolean rewrite = !Strings.isNullOrEmpty(listenAddress)
             && listenAddress.endsWith(":0");
-        acceptor.bind(listen);
+        daemonAcceptor.bind(listen);
         if (rewrite) {
-          SocketAddress bound = Iterables.getOnlyElement(acceptor.getBoundAddresses());
+          SocketAddress bound = Iterables.getOnlyElement(daemonAcceptor.getBoundAddresses());
           cfg.setString("sshd", null, "listenAddress", format((InetSocketAddress)bound));
         }
       } catch (IOException e) {
@@ -326,14 +330,14 @@
 
   @Override
   public synchronized void stop() {
-    if (acceptor != null) {
+    if (daemonAcceptor != null) {
       try {
-        acceptor.close(true).await();
+        daemonAcceptor.close(true).await();
         log.info("Stopped Gerrit SSHD");
       } catch (InterruptedException e) {
         log.warn("Exception caught while closing", e);
       } finally {
-        acceptor = null;
+        daemonAcceptor = null;
       }
     }
   }
@@ -396,11 +400,69 @@
     return r.toString();
   }
 
-  private void initProviderBouncyCastle() {
+  private void initProviderBouncyCastle(Config cfg) {
     setKeyExchangeFactories(Arrays.<NamedFactory<KeyExchange>> asList(
         new DHG14.Factory(), new DHG1.Factory()));
-    setRandomFactory(new SingletonRandomFactory(
-        new BouncyCastleRandom.Factory()));
+    NamedFactory<Random> factory;
+    if (cfg.getBoolean("sshd", null, "testUseInsecureRandom", false)) {
+      factory = new InsecureBouncyCastleRandom.Factory();
+    } else {
+      factory = new BouncyCastleRandom.Factory();
+    }
+    setRandomFactory(new SingletonRandomFactory(factory));
+  }
+
+  private static class InsecureBouncyCastleRandom implements Random {
+    private static class Factory implements NamedFactory<Random> {
+      @Override
+      public String getName() {
+        return "INSECURE_bouncycastle";
+      }
+
+      @Override
+      public Random create() {
+        return new InsecureBouncyCastleRandom();
+      }
+    }
+
+    private final RandomGenerator random;
+
+    private InsecureBouncyCastleRandom() {
+      random = new VMPCRandomGenerator();
+      random.addSeedMaterial(1234);
+    }
+
+    @Override
+    public void fill(byte[] bytes, int start, int len) {
+      random.nextBytes(bytes, start, len);
+    }
+
+    @Override
+    public int random(int n) {
+      if (n > 0) {
+        if ((n & -n) == n) {
+          return (int)((n * (long) next(31)) >> 31);
+        }
+        int bits, val;
+        do {
+          bits = next(31);
+          val = bits % n;
+        } while (bits - val + (n-1) < 0);
+        return val;
+      }
+      throw new IllegalArgumentException();
+    }
+
+    final protected int next(int numBits) {
+      int bytes = (numBits+7)/8;
+      byte next[] = new byte[bytes];
+      int ret = 0;
+      random.nextBytes(next);
+      for (int i = 0; i < bytes; i++) {
+        ret = (next[i] & 0xFF) | (ret << 8);
+      }
+      return ret >>> (bytes*8 - numBits);
+    }
   }
 
   private void initProviderJce() {
@@ -441,8 +503,8 @@
 
     a.add(null);
     a.add(new CipherNone.Factory());
-    setCipherFactories(filter(cfg, "cipher", a.toArray(new NamedFactory[a
-        .size()])));
+    setCipherFactories(filter(cfg, "cipher",
+        (NamedFactory<Cipher>[])a.toArray(new NamedFactory[a.size()])));
   }
 
   private void initMacs(final Config cfg) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index 2ec67a4..bacb167 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -90,6 +90,7 @@
     }
   }
 
+  @Override
   public void evict(String username) {
     if (username != null) {
       cache.invalidate(username);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
index 82394af..439b8c8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.Multimap;
 import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.audit.SshAuditEvent;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -25,7 +26,6 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.gerrit.server.util.SystemLog;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gerrit.sshd.SshScope.Context;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -268,11 +268,11 @@
   }
 
   private String extractWhat(DispatchCommand dcmd) {
-    String commandName = dcmd.getCommandName();
+    StringBuilder commandName = new StringBuilder(dcmd.getCommandName());
     String[] args = dcmd.getArguments();
     for (int i = 1; i < args.length; i++) {
-      commandName = commandName + "." + args[i];
+      commandName.append(".").append(args[i]);
     }
-    return commandName;
+    return commandName.toString();
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
index 4f9fe33..f134d48 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.server.plugins.Plugin;
 import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.plugins.StartPluginListener;
+import com.google.inject.Inject;
 import com.google.inject.Key;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -25,8 +26,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.inject.Inject;
-
 @Singleton
 class SshPluginStarterCallback
     implements StartPluginListener, ReloadPluginListener {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
index f8b5ddb..cd09cfa 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd;
 
 import com.google.common.collect.Maps;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -23,7 +24,6 @@
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Key;
@@ -181,8 +181,10 @@
 
   /** Returns exactly one instance per command executed. */
   public static final Scope REQUEST = new Scope() {
+    @Override
     public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) {
       return new Provider<T>() {
+        @Override
         public T get() {
           return requireContext().get(key, creator);
         }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
index 62efaa0..d4bb353 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
@@ -21,10 +21,10 @@
 import com.google.gerrit.sshd.SshScope.Context;
 
 import org.apache.commons.codec.binary.Base64;
-import org.apache.sshd.common.future.CloseFuture;
-import org.apache.sshd.common.future.SshFutureListener;
 import org.apache.sshd.common.KeyPairProvider;
 import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.future.CloseFuture;
+import org.apache.sshd.common.future.SshFutureListener;
 import org.apache.sshd.common.util.Buffer;
 import org.apache.sshd.server.session.ServerSession;
 import org.eclipse.jgit.lib.Constants;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ApproveOption.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ApproveOption.java
index fea16cd..59892a3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ApproveOption.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ApproveOption.java
@@ -21,10 +21,10 @@
 import org.kohsuke.args4j.CmdLineParser;
 import org.kohsuke.args4j.Option;
 import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.FieldSetter;
 import org.kohsuke.args4j.spi.OneArgumentOptionHandler;
 import org.kohsuke.args4j.spi.OptionHandler;
 import org.kohsuke.args4j.spi.Setter;
-import org.kohsuke.args4j.spi.FieldSetter;
 
 import java.lang.annotation.Annotation;
 import java.lang.reflect.AnnotatedElement;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java
index 0e13fd6..5eda57c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java
@@ -57,6 +57,7 @@
 
   protected abstract RestModifyView<RevisionResource, Input> createView();
 
+  @Override
   protected final void run() throws UnloggedFailure {
     try {
       RevisionResource revision = revisions.parse(
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CloseConnection.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CloseConnection.java
new file mode 100644
index 0000000..af772f8
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CloseConnection.java
@@ -0,0 +1,97 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.sshd.AdminHighPriorityCommand;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.gerrit.sshd.SshDaemon;
+import com.google.gerrit.sshd.SshSession;
+import com.google.inject.Inject;
+
+import org.apache.sshd.common.future.CloseFuture;
+import org.apache.sshd.common.io.IoAcceptor;
+import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.server.session.ServerSession;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Close specified SSH connections */
+@AdminHighPriorityCommand
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "close-connection",
+  description = "Close the specified SSH connection", runsAt = MASTER_OR_SLAVE)
+final class CloseConnection extends SshCommand {
+
+  private static final Logger log = LoggerFactory.getLogger(CloseConnection.class);
+
+  @Inject
+  private SshDaemon sshDaemon;
+
+  @Argument(index = 0, multiValued = true, required = true,
+      metaVar = "SESSION_ID", usage = "List of SSH session IDs to be closed")
+  private final List<String> sessionIds = new ArrayList<>();
+
+  @Option(name = "--wait",
+      usage = "wait for connection to close before exiting")
+  private boolean wait;
+
+  @Override
+  protected void run() throws Failure {
+    IoAcceptor acceptor = sshDaemon.getIoAcceptor();
+    if (acceptor == null) {
+      throw new Failure(1, "fatal: sshd no longer running");
+    }
+    for (String sessionId : sessionIds) {
+      boolean connectionFound = false;
+      int id = (int) Long.parseLong(sessionId, 16);
+      for (IoSession io : acceptor.getManagedSessions().values()) {
+        ServerSession serverSession =
+            (ServerSession) ServerSession.getSession(io, true);
+        SshSession sshSession =
+            serverSession != null
+                ? serverSession.getAttribute(SshSession.KEY)
+                : null;
+        if (sshSession != null && sshSession.getSessionId() == id) {
+          connectionFound = true;
+          stdout.println("closing connection " + sessionId + "...");
+          CloseFuture future = io.close(true);
+          if (wait) {
+            try {
+              future.await();
+              stdout.println("closed connection " + sessionId);
+            } catch (InterruptedException e) {
+              log.warn("Wait for connection to close interrupted: "
+                  + e.getMessage());
+            }
+          }
+          break;
+        }
+      }
+      if (!connectionFound) {
+        stderr.print("close connection " + sessionId + ": no such connection\n");
+      }
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CommandUtils.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CommandUtils.java
new file mode 100644
index 0000000..1e89986
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CommandUtils.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.BaseCommand.UnloggedFailure;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class CommandUtils {
+  public static PatchSet parsePatchSet(final String patchIdentity, ReviewDb db,
+      ProjectControl projectControl, String branch)
+      throws UnloggedFailure, OrmException {
+    // By commit?
+    //
+    if (patchIdentity.matches("^([0-9a-fA-F]{4," + RevId.LEN + "})$")) {
+      final RevId id = new RevId(patchIdentity);
+      final ResultSet<PatchSet> patches;
+      if (id.isComplete()) {
+        patches = db.patchSets().byRevision(id);
+      } else {
+        patches = db.patchSets().byRevisionRange(id, id.max());
+      }
+
+      final Set<PatchSet> matches = new HashSet<>();
+      for (final PatchSet ps : patches) {
+        final Change change = db.changes().get(ps.getId().getParentKey());
+        if (inProject(change, projectControl) && inBranch(change, branch)) {
+          matches.add(ps);
+        }
+      }
+
+      switch (matches.size()) {
+        case 1:
+          return matches.iterator().next();
+        case 0:
+          throw error("\"" + patchIdentity + "\" no such patch set");
+        default:
+          throw error("\"" + patchIdentity + "\" matches multiple patch sets");
+      }
+    }
+
+    // By older style change,patchset?
+    //
+    if (patchIdentity.matches("^[1-9][0-9]*,[1-9][0-9]*$")) {
+      final PatchSet.Id patchSetId;
+      try {
+        patchSetId = PatchSet.Id.parse(patchIdentity);
+      } catch (IllegalArgumentException e) {
+        throw error("\"" + patchIdentity + "\" is not a valid patch set");
+      }
+      final PatchSet patchSet = db.patchSets().get(patchSetId);
+      if (patchSet == null) {
+        throw error("\"" + patchIdentity + "\" no such patch set");
+      }
+      if (projectControl != null || branch != null) {
+        final Change change = db.changes().get(patchSetId.getParentKey());
+        if (!inProject(change, projectControl)) {
+          throw error("change " + change.getId() + " not in project "
+              + projectControl.getProject().getName());
+        }
+        if (!inBranch(change, branch)) {
+          throw error("change " + change.getId() + " not in branch "
+              + change.getDest().get());
+        }
+      }
+      return patchSet;
+    }
+
+    throw error("\"" + patchIdentity + "\" is not a valid patch set");
+  }
+
+  private static boolean inProject(final Change change,
+      ProjectControl projectControl) {
+    if (projectControl == null) {
+      // No --project option, so they want every project.
+      return true;
+    }
+    return projectControl.getProject().getNameKey().equals(change.getProject());
+  }
+
+  private static boolean inBranch(final Change change, String branch) {
+    if (branch == null) {
+      // No --branch option, so they want every branch.
+      return true;
+    }
+    return change.getDest().get().equals(branch);
+  }
+
+  public static UnloggedFailure error(final String msg) {
+    return new UnloggedFailure(1, msg);
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index fb39dee5..4e151b3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -23,8 +23,8 @@
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput.ConfigValue;
-import com.google.gerrit.extensions.common.InheritableBoolean;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.project.SuggestParentCandidates;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
 import org.kohsuke.args4j.Argument;
@@ -87,26 +86,34 @@
   @Option(name = "--change-id", usage = "if change-id is required")
   private InheritableBoolean requireChangeID = InheritableBoolean.INHERIT;
 
+  @Option(name = "--new-change-for-all-not-in-target", usage = "if a new change will be created for every commit not in target branch")
+  private InheritableBoolean createNewChangeForAllNotInTarget = InheritableBoolean.INHERIT;
+
   @Option(name = "--use-contributor-agreements", aliases = {"--ca"}, usage = "if contributor agreement is required")
-  void setUseContributorArgreements(boolean on) {
+  void setUseContributorArgreements(@SuppressWarnings("unused") boolean on) {
     contributorAgreements = InheritableBoolean.TRUE;
   }
 
   @Option(name = "--use-signed-off-by", aliases = {"--so"}, usage = "if signed-off-by is required")
-  void setUseSignedOffBy(boolean on) {
+  void setUseSignedOffBy(@SuppressWarnings("unused") boolean on) {
     signedOffBy = InheritableBoolean.TRUE;
   }
 
   @Option(name = "--use-content-merge", usage = "allow automatic conflict resolving within files")
-  void setUseContentMerge(boolean on) {
+  void setUseContentMerge(@SuppressWarnings("unused") boolean on) {
     contentMerge = InheritableBoolean.TRUE;
   }
 
   @Option(name = "--require-change-id", aliases = {"--id"}, usage = "if change-id is required")
-  void setRequireChangeId(boolean on) {
+  void setRequireChangeId(@SuppressWarnings("unused") boolean on) {
     requireChangeID = InheritableBoolean.TRUE;
   }
 
+  @Option(name = "--create-new-change-for-all-not-in-target", aliases = {"--ncfa"}, usage = "if a new change will be created for every commit not in target branch")
+  void setNewChangeForAllNotInTarget(@SuppressWarnings("unused") boolean on) {
+    createNewChangeForAllNotInTarget = InheritableBoolean.TRUE;
+  }
+
   @Option(name = "--branch", aliases = {"-b"}, metaVar = "BRANCH", usage = "initial branch name\n"
       + "(default: master)")
   private List<String> branch;
@@ -166,6 +173,7 @@
         input.useSignedOffBy = signedOffBy;
         input.useContentMerge = contentMerge;
         input.requireChangeId = requireChangeID;
+        input.createNewChangeForAllNotInTarget = createNewChangeForAllNotInTarget;
         input.branches = branch;
         input.createEmptyCommit = createEmptyCommit;
         input.maxObjectSizeLimit = maxObjectSizeLimit;
@@ -182,7 +190,7 @@
           stdout.print(parent + "\n");
         }
       }
-    } catch (RestApiException | OrmException | NoSuchProjectException err) {
+    } catch (RestApiException | NoSuchProjectException err) {
       throw new UnloggedFailure(1, "fatal: " + err.getMessage(), err);
     }
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index f905c5b..f75eb2b 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -31,12 +31,14 @@
   protected void configure() {
     final CommandName git = Commands.named("git");
     final CommandName gerrit = Commands.named("gerrit");
+    final CommandName logging = Commands.named(gerrit, "logging");
     final CommandName plugin = Commands.named(gerrit, "plugin");
     final CommandName testSubmit = Commands.named(gerrit, "test-submit");
 
     command(gerrit).toProvider(new DispatchCommandProvider(gerrit));
     command(gerrit, AproposCommand.class);
     command(gerrit, BanCommitCommand.class);
+    command(gerrit, CloseConnection.class);
     command(gerrit, FlushCaches.class);
     command(gerrit, ListProjectsCommand.class);
     command(gerrit, ListMembersCommand.class);
@@ -98,5 +100,11 @@
     command(gerrit, CreateAccountCommand.class);
     command(testSubmit, TestSubmitRuleCommand.class);
     command(testSubmit, TestSubmitTypeCommand.class);
+
+    command(logging).toProvider(new DispatchCommandProvider(logging));
+    command(logging, SetLoggingLevelCommand.class);
+    command(logging, ListLoggingLevelCommand.class);
+    alias(logging, "ls", ListLoggingLevelCommand.class);
+    alias(logging, "set", SetLoggingLevelCommand.class);
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
index a1099dc..c533f4f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
@@ -24,16 +24,13 @@
 import com.google.gerrit.server.git.GarbageCollection;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.sshd.BaseCommand;
 import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
-import org.apache.sshd.server.Environment;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
-import java.io.IOException;
-import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -41,7 +38,7 @@
 @RequiresCapability(GlobalCapability.RUN_GC)
 @CommandMetaData(name = "gc", description = "Run Git garbage collection",
   runsAt = MASTER_OR_SLAVE)
-public class GarbageCollectionCommand extends BaseCommand {
+public class GarbageCollectionCommand extends SshCommand {
 
   @Option(name = "--all", usage = "runs the Git garbage collection for all projects")
   private boolean all;
@@ -59,23 +56,10 @@
   @Inject
   private GarbageCollection.Factory garbageCollectionFactory;
 
-  private PrintWriter stdout;
-
   @Override
-  public void start(Environment env) throws IOException {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        stdout = toPrintWriter(out);
-        try {
-          parseCommandLine();
-          verifyCommandLine();
-          runGC();
-        } finally {
-          stdout.flush();
-        }
-      }
-    });
+  public void run() throws Exception {
+    verifyCommandLine();
+    runGC();
   }
 
   private void verifyCommandLine() throws UnloggedFailure {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
index f0169a8..82ad16f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -16,7 +16,7 @@
 
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -28,43 +28,36 @@
 import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gerrit.server.group.ListGroups;
 import com.google.gerrit.server.ioutil.ColumnFormatter;
-import com.google.gerrit.sshd.BaseCommand;
 import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.apache.sshd.server.Environment;
 import org.kohsuke.args4j.Option;
 
 import java.io.PrintWriter;
 
 @CommandMetaData(name = "ls-groups", description = "List groups visible to the caller",
   runsAt = MASTER_OR_SLAVE)
-public class ListGroupsCommand extends BaseCommand {
+public class ListGroupsCommand extends SshCommand {
   @Inject
   private MyListGroups impl;
 
   @Override
-  public void start(final Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        parseCommandLine(impl);
-        if (impl.getUser() != null && !impl.getProjects().isEmpty()) {
-          throw new UnloggedFailure(1, "fatal: --user and --project options are not compatible.");
-        }
-        final PrintWriter stdout = toPrintWriter(out);
-        try {
-          impl.display(stdout);
-        } finally {
-          stdout.flush();
-        }
-      }
-    });
+  public void run() throws Exception {
+    if (impl.getUser() != null && !impl.getProjects().isEmpty()) {
+      throw new UnloggedFailure(1, "fatal: --user and --project options are not compatible.");
+    }
+    impl.display(stdout);
   }
 
-  private static class MyListGroups extends ListGroups {
+  @Override
+  protected void parseCommandLine() throws UnloggedFailure {
+    parseCommandLine(impl);
+  }
+
+    private static class MyListGroups extends ListGroups {
     @Option(name = "--verbose", aliases = {"-v"},
         usage = "verbose output format with tab-separated columns for the " +
             "group name, UUID, description, owner group name, " +
@@ -86,7 +79,7 @@
     void display(final PrintWriter out) throws OrmException {
       final ColumnFormatter formatter = new ColumnFormatter(out, '\t');
       for (final GroupInfo info : get()) {
-        formatter.addColumn(Objects.firstNonNull(info.name, "n/a"));
+        formatter.addColumn(MoreObjects.firstNonNull(info.name, "n/a"));
         if (verboseOutput) {
           AccountGroup o = info.ownerId != null
               ? groupCache.get(new AccountGroup.UUID(Url.decode(info.ownerId)))
@@ -96,7 +89,7 @@
           formatter.addColumn(Strings.nullToEmpty(info.description));
           formatter.addColumn(o != null ? o.getName() : "n/a");
           formatter.addColumn(o != null ? o.getGroupUUID().get() : "");
-          formatter.addColumn(Boolean.toString(Objects.firstNonNull(
+          formatter.addColumn(Boolean.toString(MoreObjects.firstNonNull(
               info.options.visibleToAll, Boolean.FALSE)));
         }
         formatter.nextLine();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java
new file mode 100644
index 0000000..bc6bc17
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.kohsuke.args4j.Argument;
+
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.TreeMap;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "ls-level", description = "list the level of loggers",
+  runsAt = MASTER_OR_SLAVE)
+public class ListLoggingLevelCommand extends SshCommand {
+
+  @Argument(index = 0, required = false, metaVar = "NAME", usage = "used to match loggers")
+  private String name;
+
+  @SuppressWarnings("unchecked")
+  @Override
+  protected void run() {
+    Map<String, String> logs = new TreeMap<>();
+    for (Enumeration<Logger> logger = LogManager.getCurrentLoggers(); logger
+        .hasMoreElements();) {
+      Logger log = logger.nextElement();
+      if (name == null || log.getName().contains(name)) {
+        logs.put(log.getName(), log.getEffectiveLevel().toString());
+      }
+    }
+    for (Map.Entry<String, String> e : logs.entrySet()) {
+      stdout.println(e.getKey() + ": " + e.getValue());
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
index f8cf8dd..b24c4bfc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
@@ -16,51 +16,42 @@
 
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
-import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupDetailFactory.Factory;
 import com.google.gerrit.server.group.ListMembers;
 import com.google.gerrit.server.ioutil.ColumnFormatter;
-import com.google.gerrit.sshd.BaseCommand;
 import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
 
-import org.apache.sshd.server.Environment;
 import org.kohsuke.args4j.Argument;
 
 import java.io.PrintWriter;
 import java.util.List;
 
-import javax.inject.Inject;
-
 /**
  * Implements a command that allows the user to see the members of a group.
  */
 @CommandMetaData(name = "ls-members", description = "List the members of a given group",
   runsAt = MASTER_OR_SLAVE)
-public class ListMembersCommand extends BaseCommand {
+public class ListMembersCommand extends SshCommand {
   @Inject
   ListMembersCommandImpl impl;
 
   @Override
-  public void start(Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        parseCommandLine(impl);
-        final PrintWriter stdout = toPrintWriter(out);
-        try {
-          impl.display(stdout);
-        } finally {
-          stdout.flush();
-        }
-      }
-    });
+  public void run() throws Exception {
+    impl.display(stdout);
+  }
+
+  @Override
+  protected void parseCommandLine() throws UnloggedFailure {
+    parseCommandLine(impl);
   }
 
   private static class ListMembersCommandImpl extends ListMembers {
@@ -72,13 +63,12 @@
     @Inject
     protected ListMembersCommandImpl(GroupCache groupCache,
         Factory groupDetailFactory,
-        AccountInfo.Loader.Factory accountLoaderFactory,
-        AccountCache accountCache) {
+        AccountLoader.Factory accountLoaderFactory) {
       super(groupCache, groupDetailFactory, accountLoaderFactory);
       this.groupCache = groupCache;
     }
 
-    void display(PrintWriter writer) throws UnloggedFailure, OrmException {
+    void display(PrintWriter writer) throws OrmException {
       AccountGroup group = groupCache.get(new AccountGroup.NameKey(name));
       String errorText = "Group not found or not visible\n";
 
@@ -88,32 +78,28 @@
         return;
       }
 
-      try {
-        List<AccountInfo> members = apply(group.getGroupUUID());
-        ColumnFormatter formatter = new ColumnFormatter(writer, '\t');
-        formatter.addColumn("id");
-        formatter.addColumn("username");
-        formatter.addColumn("full name");
-        formatter.addColumn("email");
-        formatter.nextLine();
-        for (AccountInfo member : members) {
-          if (member == null) {
-            continue;
-          }
-
-          formatter.addColumn(member._id.toString());
-          formatter.addColumn(Objects.firstNonNull(member.username, "n/a"));
-          formatter.addColumn(Objects.firstNonNull(
-              Strings.emptyToNull(member.name), "n/a"));
-          formatter.addColumn(Objects.firstNonNull(member.email, "n/a"));
-          formatter.nextLine();
+      List<AccountInfo> members = apply(group.getGroupUUID());
+      ColumnFormatter formatter = new ColumnFormatter(writer, '\t');
+      formatter.addColumn("id");
+      formatter.addColumn("username");
+      formatter.addColumn("full name");
+      formatter.addColumn("email");
+      formatter.nextLine();
+      for (AccountInfo member : members) {
+        if (member == null) {
+          continue;
         }
 
-        formatter.finish();
-      } catch (MethodNotAllowedException e) {
-        writer.write(errorText);
-        writer.flush();
+        formatter.addColumn(Integer.toString(member._accountId));
+        formatter.addColumn(MoreObjects.firstNonNull(
+            member.username, "n/a"));
+        formatter.addColumn(MoreObjects.firstNonNull(
+            Strings.emptyToNull(member.name), "n/a"));
+        formatter.addColumn(MoreObjects.firstNonNull(member.email, "n/a"));
+        formatter.nextLine();
       }
+
+      formatter.finish();
     }
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
index 78034fc..134a719 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -17,37 +17,34 @@
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
 import com.google.gerrit.server.project.ListProjects;
-import com.google.gerrit.sshd.BaseCommand;
 import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
-import org.apache.sshd.server.Environment;
-
 import java.util.List;
 
 @CommandMetaData(name = "ls-projects", description = "List projects visible to the caller",
   runsAt = MASTER_OR_SLAVE)
-final class ListProjectsCommand extends BaseCommand {
+final class ListProjectsCommand extends SshCommand {
   @Inject
   private ListProjects impl;
 
   @Override
-  public void start(final Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        parseCommandLine(impl);
-        if (!impl.getFormat().isJson()) {
-          List<String> showBranch = impl.getShowBranch();
-          if (impl.isShowTree() && (showBranch != null) && !showBranch.isEmpty()) {
-            throw new UnloggedFailure(1, "fatal: --tree and --show-branch options are not compatible.");
-          }
-          if (impl.isShowTree() && impl.isShowDescription()) {
-            throw new UnloggedFailure(1, "fatal: --tree and --description options are not compatible.");
-          }
-        }
-        impl.display(out);
+  public void run() throws Exception {
+    if (!impl.getFormat().isJson()) {
+      List<String> showBranch = impl.getShowBranch();
+      if (impl.isShowTree() && (showBranch != null) && !showBranch.isEmpty()) {
+        throw new UnloggedFailure(1, "fatal: --tree and --show-branch options are not compatible.");
       }
-    });
+      if (impl.isShowTree() && impl.isShowDescription()) {
+        throw new UnloggedFailure(1, "fatal: --tree and --description options are not compatible.");
+      }
+    }
+    impl.display(out);
+  }
+
+  @Override
+  protected void parseCommandLine() throws UnloggedFailure {
+    parseCommandLine(impl);
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
index 49120f7..799686d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
@@ -44,7 +44,7 @@
   private String name;
 
   @Option(name = "-")
-  void useInput(boolean on) {
+  void useInput(@SuppressWarnings("unused") boolean on) {
     source = "-";
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
index 9f6bb50..c3c710d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
@@ -19,29 +19,24 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.plugins.ListPlugins;
-import com.google.gerrit.sshd.BaseCommand;
 import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
-import org.apache.sshd.server.Environment;
-
-import java.io.IOException;
-
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @CommandMetaData(name = "ls", description = "List the installed plugins",
   runsAt = MASTER_OR_SLAVE)
-final class PluginLsCommand extends BaseCommand {
+final class PluginLsCommand extends SshCommand {
   @Inject
   private ListPlugins impl;
 
   @Override
-  public void start(Environment env) throws IOException {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        parseCommandLine(impl);
-        impl.display(out);
-      }
-    });
+  public void run() throws Exception {
+    impl.display(stdout);
+  }
+
+  @Override
+  protected void parseCommandLine() throws UnloggedFailure {
+    parseCommandLine(impl);
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
index 63b7ab6..f65a0c9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.server.query.change.QueryProcessor;
+import com.google.gerrit.server.query.change.OutputStreamQuery;
+import com.google.gerrit.server.query.change.OutputStreamQuery.OutputFormat;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
@@ -27,10 +28,10 @@
 @CommandMetaData(name = "query", description = "Query the change database")
 class Query extends SshCommand {
   @Inject
-  private QueryProcessor processor;
+  private OutputStreamQuery processor;
 
   @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
-  void setFormat(QueryProcessor.OutputFormat format) {
+  void setFormat(OutputFormat format) {
     processor.setOutput(out, format);
   }
 
@@ -97,7 +98,7 @@
 
   @Override
   protected void parseCommandLine() throws UnloggedFailure {
-    processor.setOutput(out, QueryProcessor.OutputFormat.TEXT);
+    processor.setOutput(out, OutputFormat.TEXT);
     super.parseCommandLine();
     if (processor.getIncludeFiles() &&
         !(processor.getIncludePatchSets() || processor.getIncludeCurrentPatchSet())) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
index 39ec81f5..8f26bba 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonObject;
 import com.google.gwtorm.jdbc.JdbcSchema;
@@ -224,20 +224,15 @@
       return;
     }
 
-    try {
-      final String[] types = {"TABLE", "VIEW"};
-      ResultSet rs = meta.getTables(null, null, null, types);
-      try {
-        if (outputFormat == OutputFormat.PRETTY) {
-          println("                     List of relations");
-        }
-        showResultSet(rs, false, 0,
-            Identity.create(rs, "TABLE_SCHEM"),
-            Identity.create(rs, "TABLE_NAME"),
-            Identity.create(rs, "TABLE_TYPE"));
-      } finally {
-        rs.close();
+    final String[] types = {"TABLE", "VIEW"};
+    try (ResultSet rs = meta.getTables(null, null, null, types)) {
+      if (outputFormat == OutputFormat.PRETTY) {
+        println("                     List of relations");
       }
+      showResultSet(rs, false, 0,
+          Identity.create(rs, "TABLE_SCHEM"),
+          Identity.create(rs, "TABLE_NAME"),
+          Identity.create(rs, "TABLE_TYPE"));
     } catch (SQLException e) {
       error(e);
     }
@@ -260,91 +255,81 @@
       return;
     }
 
-    try {
-      ResultSet rs = meta.getColumns(null, null, tableName, null);
-      try {
-        if (!rs.next()) {
-          throw new SQLException("Table " + tableName + " not found");
-        }
-
-        if (outputFormat == OutputFormat.PRETTY) {
-          println("                     Table " + tableName);
-        }
-        showResultSet(rs, true, 0,
-            Identity.create(rs, "COLUMN_NAME"),
-            new Function("TYPE") {
-              @Override
-              String apply(final ResultSet rs) throws SQLException {
-                String type = rs.getString("TYPE_NAME");
-                switch (rs.getInt("DATA_TYPE")) {
-                  case java.sql.Types.CHAR:
-                  case java.sql.Types.VARCHAR:
-                    type += "(" + rs.getInt("COLUMN_SIZE") + ")";
-                    break;
-                }
-
-                String def = rs.getString("COLUMN_DEF");
-                if (def != null && !def.isEmpty()) {
-                  type += " DEFAULT " + def;
-                }
-
-                int nullable = rs.getInt("NULLABLE");
-                if (nullable == DatabaseMetaData.columnNoNulls) {
-                  type += " NOT NULL";
-                }
-                return type;
-              }
-            });
-      } finally {
-        rs.close();
+    try (ResultSet rs = meta.getColumns(null, null, tableName, null)) {
+      if (!rs.next()) {
+        throw new SQLException("Table " + tableName + " not found");
       }
+
+      if (outputFormat == OutputFormat.PRETTY) {
+        println("                     Table " + tableName);
+      }
+      showResultSet(rs, true, 0,
+          Identity.create(rs, "COLUMN_NAME"),
+          new Function("TYPE") {
+            @Override
+            String apply(final ResultSet rs) throws SQLException {
+              String type = rs.getString("TYPE_NAME");
+              switch (rs.getInt("DATA_TYPE")) {
+                case java.sql.Types.CHAR:
+                case java.sql.Types.VARCHAR:
+                  type += "(" + rs.getInt("COLUMN_SIZE") + ")";
+                  break;
+              }
+
+              String def = rs.getString("COLUMN_DEF");
+              if (def != null && !def.isEmpty()) {
+                type += " DEFAULT " + def;
+              }
+
+              int nullable = rs.getInt("NULLABLE");
+              if (nullable == DatabaseMetaData.columnNoNulls) {
+                type += " NOT NULL";
+              }
+              return type;
+            }
+          });
     } catch (SQLException e) {
       error(e);
       return;
     }
 
-    try {
-      ResultSet rs = meta.getIndexInfo(null, null, tableName, false, true);
-      try {
-        Map<String, IndexInfo> indexes = new TreeMap<>();
-        while (rs.next()) {
-          final String indexName = rs.getString("INDEX_NAME");
-          IndexInfo def = indexes.get(indexName);
-          if (def == null) {
-            def = new IndexInfo();
-            def.name = indexName;
-            indexes.put(indexName, def);
-          }
-
-          if (!rs.getBoolean("NON_UNIQUE")) {
-            def.unique = true;
-          }
-
-          final int pos = rs.getInt("ORDINAL_POSITION");
-          final String col = rs.getString("COLUMN_NAME");
-          String desc = rs.getString("ASC_OR_DESC");
-          if ("D".equals(desc)) {
-            desc = " DESC";
-          } else {
-            desc = "";
-          }
-          def.addColumn(pos, col + desc);
-
-          String filter = rs.getString("FILTER_CONDITION");
-          if (filter != null && !filter.isEmpty()) {
-            def.filter.append(filter);
-          }
+    try (ResultSet rs = meta.getIndexInfo(null, null, tableName, false, true)) {
+      Map<String, IndexInfo> indexes = new TreeMap<>();
+      while (rs.next()) {
+        final String indexName = rs.getString("INDEX_NAME");
+        IndexInfo def = indexes.get(indexName);
+        if (def == null) {
+          def = new IndexInfo();
+          def.name = indexName;
+          indexes.put(indexName, def);
         }
 
-        if (outputFormat == OutputFormat.PRETTY) {
-          println("");
-          println("Indexes on " + tableName + ":");
-          for (IndexInfo def : indexes.values()) {
-            println("  " + def);
-          }
+        if (!rs.getBoolean("NON_UNIQUE")) {
+          def.unique = true;
         }
-      } finally {
-        rs.close();
+
+        final int pos = rs.getInt("ORDINAL_POSITION");
+        final String col = rs.getString("COLUMN_NAME");
+        String desc = rs.getString("ASC_OR_DESC");
+        if ("D".equals(desc)) {
+          desc = " DESC";
+        } else {
+          desc = "";
+        }
+        def.addColumn(pos, col + desc);
+
+        String filter = rs.getString("FILTER_CONDITION");
+        if (filter != null && !filter.isEmpty()) {
+          def.filter.append(filter);
+        }
+      }
+
+      if (outputFormat == OutputFormat.PRETTY) {
+        println("");
+        println("Indexes on " + tableName + ":");
+        for (IndexInfo def : indexes.values()) {
+          println("  " + def);
+        }
       }
     } catch (SQLException e) {
       error(e);
@@ -366,11 +351,8 @@
 
     try {
       if (hasResultSet) {
-        final ResultSet rs = statement.getResultSet();
-        try {
+        try (ResultSet rs = statement.getResultSet()) {
           showResultSet(rs, false, start);
-        } finally {
-          rs.close();
         }
 
       } else {
@@ -727,7 +709,7 @@
     print(help.toString());
   }
 
-  private static abstract class Function {
+  private abstract static class Function {
     final String name;
 
     Function(final String name) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index f39cd36..3ac72a7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Maps;
+import com.google.common.io.CharStreams;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.extensions.api.GerritApi;
@@ -26,12 +27,10 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
@@ -39,8 +38,8 @@
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.util.cli.CmdLineParser;
+import com.google.gson.JsonSyntaxException;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -50,6 +49,8 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -75,7 +76,9 @@
   @Argument(index = 0, required = true, multiValued = true, metaVar = "{COMMIT | CHANGE,PATCHSET}", usage = "list of commits or patch sets to review")
   void addPatchSetId(final String token) {
     try {
-      patchSets.add(parsePatchSet(token));
+      PatchSet ps = CommandUtils.parsePatchSet(token, db, projectControl,
+          branch);
+      patchSets.add(ps);
     } catch (UnloggedFailure e) {
       throw new IllegalArgumentException(e.getMessage(), e);
     } catch (OrmException e) {
@@ -101,6 +104,9 @@
   @Option(name = "--restore", usage = "restore the specified abandoned change(s)")
   private boolean restoreChange;
 
+  @Option(name = "--rebase", usage = "rebase the specified change(s)")
+  private boolean rebaseChange;
+
   @Option(name = "--submit", aliases = "-s", usage = "submit the specified patch set(s)")
   private boolean submitChange;
 
@@ -110,11 +116,14 @@
   @Option(name = "--delete", usage = "delete the specified draft patch set(s)")
   private boolean deleteDraftPatchSet;
 
+  @Option(name = "--json", aliases = "-j", usage = "read review input json from stdin")
+  private boolean json;
+
   @Option(name = "--label", aliases = "-l", usage = "custom label(s) to assign", metaVar = "LABEL=VALUE")
   void addLabel(final String token) {
     LabelVote v = LabelVote.parseWithEquals(token);
-    LabelType.checkName(v.getLabel()); // Disallow SUBM.
-    customLabels.put(v.getLabel(), v.getValue());
+    LabelType.checkName(v.label()); // Disallow SUBM.
+    customLabels.put(v.label(), v.value());
   }
 
   @Inject
@@ -147,6 +156,9 @@
       if (deleteDraftPatchSet) {
         throw error("abandon and delete actions are mutually exclusive");
       }
+      if (rebaseChange) {
+        throw error("abandon and rebase actions are mutually exclusive");
+      }
     }
     if (publishPatchSet) {
       if (restoreChange) {
@@ -159,17 +171,55 @@
         throw error("publish and delete actions are mutually exclusive");
       }
     }
-    if (deleteDraftPatchSet) {
-      if (submitChange) {
-        throw error("delete and submit actions are mutually exclusive");
+    if (json) {
+      if (restoreChange) {
+        throw error("json and restore actions are mutually exclusive");
       }
+      if (submitChange) {
+        throw error("json and submit actions are mutually exclusive");
+      }
+      if (deleteDraftPatchSet) {
+        throw error("json and delete actions are mutually exclusive");
+      }
+      if (publishPatchSet) {
+        throw error("json and publish actions are mutually exclusive");
+      }
+      if (abandonChange) {
+        throw error("json and abandon actions are mutually exclusive");
+      }
+      if (changeComment != null) {
+        throw error("json and message are mutually exclusive");
+      }
+      if (rebaseChange) {
+        throw error("json and rebase actions are mutually exclusive");
+      }
+    }
+    if (rebaseChange) {
+      if (deleteDraftPatchSet) {
+        throw error("rebase and delete actions are mutually exclusive");
+      }
+      if (submitChange) {
+        throw error("rebase and submit actions are mutually exclusive");
+      }
+    }
+    if (deleteDraftPatchSet && submitChange) {
+      throw error("delete and submit actions are mutually exclusive");
     }
 
     boolean ok = true;
+    ReviewInput input = null;
+    if (json) {
+      input = reviewFromJson();
+    }
+
     for (final PatchSet patchSet : patchSets) {
       try {
-        reviewPatchSet(patchSet);
-      } catch (UnloggedFailure e) {
+        if (input != null) {
+          applyReview(patchSet, input);
+        } else {
+          reviewPatchSet(patchSet);
+        }
+      } catch (RestApiException | UnloggedFailure e) {
         ok = false;
         writeError("error: " + e.getMessage() + "\n");
       } catch (NoSuchChangeException e) {
@@ -184,21 +234,30 @@
     }
 
     if (!ok) {
-      throw new UnloggedFailure(1, "one or more reviews failed;"
-          + " review output above");
+      throw error("one or more reviews failed; review output above");
     }
   }
 
   private void applyReview(PatchSet patchSet,
-      final ReviewInput review) throws Exception {
+      final ReviewInput review) throws RestApiException {
     gApi.get().changes()
         .id(patchSet.getId().getParentKey().get())
         .revision(patchSet.getRevision().get())
         .review(review);
   }
 
-  private void reviewPatchSet(final PatchSet patchSet) throws Exception {
+  private ReviewInput reviewFromJson() throws UnloggedFailure {
+    try (InputStreamReader r =
+          new InputStreamReader(in, StandardCharsets.UTF_8)) {
+      return OutputFormat.JSON.newGson().
+          fromJson(CharStreams.toString(r), ReviewInput.class);
+    } catch (IOException | JsonSyntaxException e) {
+      writeError(e.getMessage() + '\n');
+      throw error("internal error while reading review input");
+    }
+  }
 
+  private void reviewPatchSet(final PatchSet patchSet) throws Exception {
     if (changeComment == null) {
       changeComment = "";
     }
@@ -242,6 +301,10 @@
         applyReview(patchSet, review);
       }
 
+      if (rebaseChange){
+        revisionApi(patchSet).rebase();
+      }
+
       if (submitChange) {
         revisionApi(patchSet).submit();
       }
@@ -251,8 +314,7 @@
       } else if (deleteDraftPatchSet) {
         revisionApi(patchSet).delete();
       }
-    } catch (IllegalStateException | InvalidChangeOperationException
-        | RestApiException e) {
+    } catch (IllegalStateException | RestApiException e) {
       throw error(e.getMessage());
     }
   }
@@ -265,83 +327,6 @@
     return changeApi(patchSet).revision(patchSet.getRevision().get());
   }
 
-  private PatchSet parsePatchSet(final String patchIdentity)
-      throws UnloggedFailure, OrmException {
-    // By commit?
-    //
-    if (patchIdentity.matches("^([0-9a-fA-F]{4," + RevId.LEN + "})$")) {
-      final RevId id = new RevId(patchIdentity);
-      final ResultSet<PatchSet> patches;
-      if (id.isComplete()) {
-        patches = db.patchSets().byRevision(id);
-      } else {
-        patches = db.patchSets().byRevisionRange(id, id.max());
-      }
-
-      final Set<PatchSet> matches = new HashSet<>();
-      for (final PatchSet ps : patches) {
-        final Change change = db.changes().get(ps.getId().getParentKey());
-        if (inProject(change) && inBranch(change)) {
-          matches.add(ps);
-        }
-      }
-
-      switch (matches.size()) {
-        case 1:
-          return matches.iterator().next();
-        case 0:
-          throw error("\"" + patchIdentity + "\" no such patch set");
-        default:
-          throw error("\"" + patchIdentity + "\" matches multiple patch sets");
-      }
-    }
-
-    // By older style change,patchset?
-    //
-    if (patchIdentity.matches("^[1-9][0-9]*,[1-9][0-9]*$")) {
-      final PatchSet.Id patchSetId;
-      try {
-        patchSetId = PatchSet.Id.parse(patchIdentity);
-      } catch (IllegalArgumentException e) {
-        throw error("\"" + patchIdentity + "\" is not a valid patch set");
-      }
-      final PatchSet patchSet = db.patchSets().get(patchSetId);
-      if (patchSet == null) {
-        throw error("\"" + patchIdentity + "\" no such patch set");
-      }
-      if (projectControl != null || branch != null) {
-        final Change change = db.changes().get(patchSetId.getParentKey());
-        if (!inProject(change)) {
-          throw error("change " + change.getId() + " not in project "
-              + projectControl.getProject().getName());
-        }
-        if (!inBranch(change)) {
-          throw error("change " + change.getId() + " not in branch "
-              + change.getDest().get());
-        }
-      }
-      return patchSet;
-    }
-
-    throw error("\"" + patchIdentity + "\" is not a valid patch set");
-  }
-
-  private boolean inProject(final Change change) {
-    if (projectControl == null) {
-      // No --project option, so they want every project.
-      return true;
-    }
-    return projectControl.getProject().getNameKey().equals(change.getProject());
-  }
-
-  private boolean inBranch(final Change change) {
-    if (branch == null) {
-      // No --branch option, so they want every branch.
-      return true;
-    }
-    return change.getDest().get().equals(branch);
-  }
-
   @Override
   protected void parseCommandLine() throws UnloggedFailure {
     optionList = new ArrayList<>();
@@ -355,15 +340,16 @@
     }
 
     for (LabelType type : allProjectsControl.getLabelTypes().getLabelTypes()) {
-      String usage;
-      usage = "score for " + type.getName() + "\n";
+      StringBuilder usage = new StringBuilder("score for ")
+        .append(type.getName())
+        .append("\n");
 
       for (LabelValue v : type.getValues()) {
-        usage += v.format() + "\n";
+        usage.append(v.format()).append("\n");
       }
 
       final String name = "--" + type.getName().toLowerCase();
-      optionList.add(new ApproveOption(name, usage, type));
+      optionList.add(new ApproveOption(name, usage.toString(), type));
     }
 
     super.parseCommandLine();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
index 65f876f..bdc4cef 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
@@ -84,6 +84,7 @@
   @Override
   public void start(final Environment env) {
     startThread(new Runnable() {
+      @Override
       public void run() {
         runImp();
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index 5088424..1cb442d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -14,7 +14,11 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.common.base.Strings;
+import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RawInput;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -34,13 +38,12 @@
 import com.google.gerrit.server.account.PutActive;
 import com.google.gerrit.server.account.PutHttpPassword;
 import com.google.gerrit.server.account.PutName;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.server.account.PutPreferred;
 import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
-import org.apache.sshd.server.Environment;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -56,7 +59,8 @@
 
 /** Set a user's account settings. **/
 @CommandMetaData(name = "set-account", description = "Change an account's settings")
-final class SetAccountCommand extends BaseCommand {
+@RequiresCapability(GlobalCapability.MODIFY_ACCOUNT)
+final class SetAccountCommand extends SshCommand {
 
   @Argument(index = 0, required = true, metaVar = "USER", usage = "full name, email-address, ssh username or account id")
   private Account.Id id;
@@ -76,6 +80,9 @@
   @Option(name = "--delete-email", metaVar = "EMAIL", usage = "email addresses to delete from the account")
   private List<String> deleteEmails = new ArrayList<>();
 
+  @Option(name = "--preferred-email", metaVar = "EMAIL", usage = "a registered email address from the account")
+  private String preferredEmail;
+
   @Option(name = "--add-ssh-key", metaVar = "-|KEY", usage = "public keys to add to the account")
   private List<String> addSshKeys = new ArrayList<>();
 
@@ -85,8 +92,8 @@
   @Option(name = "--http-password", metaVar = "PASSWORD", usage = "password for HTTP authentication for the account")
   private String httpPassword;
 
-  @Inject
-  private IdentifiedUser currentUser;
+  @Option(name = "--clear-http-password", usage = "clear HTTP password for the account")
+  private boolean clearHttpPassword;
 
   @Inject
   private IdentifiedUser.GenericFactory genericUserFactory;
@@ -95,52 +102,42 @@
   private CreateEmail.Factory createEmailFactory;
 
   @Inject
-  private Provider<GetEmails> getEmailsProvider;
+  private GetEmails getEmails;
 
   @Inject
-  private Provider<DeleteEmail> deleteEmailProvider;
+  private DeleteEmail deleteEmail;
 
   @Inject
-  private Provider<PutName> putNameProvider;
+  private PutPreferred putPreferred;
 
   @Inject
-  private Provider<PutHttpPassword> putHttpPasswordProvider;
+  private PutName putName;
 
   @Inject
-  private Provider<PutActive> putActiveProvider;
+  private PutHttpPassword putHttpPassword;
 
   @Inject
-  private Provider<DeleteActive> deleteActiveProvider;
+  private PutActive putActive;
 
   @Inject
-  private Provider<AddSshKey> addSshKeyProvider;
+  private DeleteActive deleteActive;
 
   @Inject
-  private Provider<GetSshKeys> getSshKeysProvider;
+  private AddSshKey addSshKey;
 
   @Inject
-  private Provider<DeleteSshKey> deleteSshKeyProvider;
+  private GetSshKeys getSshKeys;
+
+  @Inject
+  private DeleteSshKey deleteSshKey;
 
   private IdentifiedUser user;
   private AccountResource rsrc;
 
   @Override
-  public void start(final Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        if (!currentUser.getCapabilities().canAdministrateServer()) {
-          String msg =
-              String.format(
-                  "fatal: %s does not have \"Administrator\" capability.",
-                  currentUser.getUserName());
-          throw new UnloggedFailure(1, msg);
-        }
-        parseCommandLine();
-        validate();
-        setAccount();
-      }
-    });
+  public void run() throws Exception {
+    validate();
+    setAccount();
   }
 
   private void validate() throws UnloggedFailure {
@@ -148,6 +145,11 @@
       throw new UnloggedFailure(1,
           "--active and --inactive options are mutually exclusive.");
     }
+    if (clearHttpPassword && !Strings.isNullOrEmpty(httpPassword)) {
+      throw new UnloggedFailure(1,
+          "--http-password and --clear-http-password options are mutually " +
+          "exclusive.");
+    }
     if (addSshKeys.contains("-") && deleteSshKeys.contains("-")) {
       throw new UnloggedFailure(1, "Only one option may use the stdin");
     }
@@ -157,6 +159,11 @@
     if (deleteEmails.contains("ALL")) {
       deleteEmails = Collections.singletonList("ALL");
     }
+    if (deleteEmails.contains(preferredEmail)) {
+      throw new UnloggedFailure(1,
+          "--preferred-email and --delete-email options are mutually " +
+          "exclusive for the same email address.");
+    }
   }
 
   private void setAccount() throws OrmException, IOException, UnloggedFailure {
@@ -171,23 +178,27 @@
         deleteEmail(email);
       }
 
+      if (preferredEmail != null) {
+        putPreferred(preferredEmail);
+      }
+
       if (fullName != null) {
         PutName.Input in = new PutName.Input();
         in.name = fullName;
-        putNameProvider.get().apply(rsrc, in);
+        putName.apply(rsrc, in);
       }
 
-      if (httpPassword != null) {
+      if (httpPassword != null || clearHttpPassword) {
         PutHttpPassword.Input in = new PutHttpPassword.Input();
         in.httpPassword = httpPassword;
-        putHttpPasswordProvider.get().apply(rsrc, in);
+        putHttpPassword.apply(rsrc, in);
       }
 
       if (active) {
-        putActiveProvider.get().apply(rsrc, null);
+        putActive.apply(rsrc, null);
       } else if (inactive) {
         try {
-          deleteActiveProvider.get().apply(rsrc, null);
+          deleteActive.apply(rsrc, null);
         } catch (ResourceNotFoundException e) {
           // user is already inactive
         }
@@ -208,7 +219,7 @@
   }
 
   private void addSshKeys(List<String> sshKeys) throws RestApiException,
-      UnloggedFailure, OrmException, IOException {
+      OrmException, IOException {
     for (final String sshKey : sshKeys) {
       AddSshKey.Input in = new AddSshKey.Input();
       in.raw = new RawInput() {
@@ -227,13 +238,13 @@
           return sshKey.length();
         }
       };
-      addSshKeyProvider.get().apply(rsrc, in);
+      addSshKey.apply(rsrc, in);
     }
   }
 
   private void deleteSshKeys(List<String> sshKeys) throws RestApiException,
       OrmException {
-    List<SshKeyInfo> infos = getSshKeysProvider.get().apply(rsrc);
+    List<SshKeyInfo> infos = getSshKeys.apply(rsrc);
     if (sshKeys.contains("ALL")) {
       for (SshKeyInfo i : infos) {
         deleteSshKey(i);
@@ -250,10 +261,10 @@
     }
   }
 
-  private void deleteSshKey(SshKeyInfo i) throws OrmException {
+  private void deleteSshKey(SshKeyInfo i) throws AuthException, OrmException {
     AccountSshKey sshKey = new AccountSshKey(
         new AccountSshKey.Id(user.getAccountId(), i.seq), i.sshPublicKey);
-    deleteSshKeyProvider.get().apply(
+    deleteSshKey.apply(
         new AccountResource.SshKey(user, sshKey), null);
   }
 
@@ -269,35 +280,44 @@
     }
   }
 
-  private void deleteEmail(String email) throws UnloggedFailure,
-      RestApiException, OrmException {
+  private void deleteEmail(String email) throws RestApiException, OrmException {
     if (email.equals("ALL")) {
-      List<EmailInfo> emails = getEmailsProvider.get().apply(rsrc);
-      DeleteEmail deleteEmail = deleteEmailProvider.get();
+      List<EmailInfo> emails = getEmails.apply(rsrc);
       for (EmailInfo e : emails) {
         deleteEmail.apply(new AccountResource.Email(user, e.email),
             new DeleteEmail.Input());
       }
     } else {
-      deleteEmailProvider.get().apply(new AccountResource.Email(user, email),
+      deleteEmail.apply(new AccountResource.Email(user, email),
           new DeleteEmail.Input());
     }
   }
 
+  private void putPreferred(String email) throws RestApiException,
+      OrmException {
+    for (EmailInfo e : getEmails.apply(rsrc)) {
+      if (e.email.equals(email)) {
+        putPreferred.apply(new AccountResource.Email(user, email), null);
+        return;
+      }
+    }
+    stderr.println("preferred email not found: " + email);
+  }
+
   private List<String> readSshKey(final List<String> sshKeys)
       throws UnsupportedEncodingException, IOException {
     if (!sshKeys.isEmpty()) {
-      String sshKey;
       int idx = sshKeys.indexOf("-");
       if (idx >= 0) {
-        sshKey = "";
+        StringBuilder sshKey = new StringBuilder();
         BufferedReader br =
             new BufferedReader(new InputStreamReader(in, "UTF-8"));
         String line;
         while ((line = br.readLine()) != null) {
-          sshKey += line + "\n";
+          sshKey.append(line)
+            .append("\n");
         }
-        sshKeys.set(idx, sshKey);
+        sshKeys.set(idx, sshKey.toString());
       }
     }
     return sshKeys;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
new file mode 100644
index 0000000..49edf14
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.apache.log4j.PropertyConfigurator;
+import org.apache.log4j.helpers.Loader;
+import org.kohsuke.args4j.Argument;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Enumeration;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "set-level", description = "Change the level of loggers",
+  runsAt = MASTER_OR_SLAVE)
+public class SetLoggingLevelCommand extends SshCommand {
+  private static final String LOG_CONFIGURATION = "log4j.properties";
+  private static final String JAVA_OPTIONS_LOG_CONFIG = "log4j.configuration";
+
+  private static enum LevelOption {
+    ALL,
+    TRACE,
+    DEBUG,
+    INFO,
+    WARN,
+    ERROR,
+    FATAL,
+    OFF,
+    RESET,
+  }
+
+  @Argument(index = 0, required = true, metaVar = "LEVEL", usage = "logging level to set to")
+  private LevelOption level;
+
+  @Argument(index = 1, required = false, metaVar = "NAME", usage = "used to match loggers")
+  private String name;
+
+  @SuppressWarnings("unchecked")
+  @Override
+  protected void run() throws MalformedURLException {
+    if (level == LevelOption.RESET) {
+      reset();
+    } else {
+      for (Enumeration<Logger> logger = LogManager.getCurrentLoggers(); logger
+          .hasMoreElements();) {
+        Logger log = logger.nextElement();
+        if (name == null || log.getName().contains(name)) {
+          log.setLevel(Level.toLevel(level.name()));
+        }
+      }
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private static void reset() throws MalformedURLException {
+    for (Enumeration<Logger> logger = LogManager.getCurrentLoggers();
+        logger.hasMoreElements();) {
+      logger.nextElement().setLevel(null);
+    }
+
+    String path = System.getProperty(JAVA_OPTIONS_LOG_CONFIG);
+    if (Strings.isNullOrEmpty(path)) {
+      PropertyConfigurator.configure(Loader.getResource(LOG_CONFIGURATION));
+    } else {
+      PropertyConfigurator.configure(new URL(path));
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
index cc9e6ed..923865a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
@@ -16,7 +16,7 @@
 
 import com.google.common.base.Function;
 import com.google.common.base.Joiner;
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -119,7 +119,7 @@
                 new Function<Account.Id, String>() {
                   @Override
                   public String apply(Account.Id accountId) {
-                    return Objects.firstNonNull(accountCache.get(accountId)
+                    return MoreObjects.firstNonNull(accountCache.get(accountId)
                         .getAccount().getPreferredEmail(), "n/a");
                   }
                 }))).getBytes(ENC));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
index cc72e96..3fd9b4d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -16,9 +16,9 @@
 
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.extensions.api.projects.ProjectState;
-import com.google.gerrit.extensions.common.InheritableBoolean;
-import com.google.gerrit.extensions.common.SubmitType;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
@@ -66,42 +66,42 @@
   private InheritableBoolean requireChangeID;
 
   @Option(name = "--use-contributor-agreements", aliases = {"--ca"}, usage = "if contributor agreement is required")
-  void setUseContributorArgreements(boolean on) {
+  void setUseContributorArgreements(@SuppressWarnings("unused") boolean on) {
     contributorAgreements = InheritableBoolean.TRUE;
   }
 
   @Option(name = "--no-contributor-agreements", aliases = {"--nca"}, usage = "if contributor agreement is not required")
-  void setNoContributorArgreements(boolean on) {
+  void setNoContributorArgreements(@SuppressWarnings("unused") boolean on) {
     contributorAgreements = InheritableBoolean.FALSE;
   }
 
   @Option(name = "--use-signed-off-by", aliases = {"--so"}, usage = "if signed-off-by is required")
-  void setUseSignedOffBy(boolean on) {
+  void setUseSignedOffBy(@SuppressWarnings("unused") boolean on) {
     signedOffBy = InheritableBoolean.TRUE;
   }
 
   @Option(name = "--no-signed-off-by", aliases = {"--nso"}, usage = "if signed-off-by is not required")
-  void setNoSignedOffBy(boolean on) {
+  void setNoSignedOffBy(@SuppressWarnings("unused") boolean on) {
     signedOffBy = InheritableBoolean.FALSE;
   }
 
   @Option(name = "--use-content-merge", usage = "allow automatic conflict resolving within files")
-  void setUseContentMerge(boolean on) {
+  void setUseContentMerge(@SuppressWarnings("unused") boolean on) {
     contentMerge = InheritableBoolean.TRUE;
   }
 
   @Option(name = "--no-content-merge", usage = "don't allow automatic conflict resolving within files")
-  void setNoContentMerge(boolean on) {
+  void setNoContentMerge(@SuppressWarnings("unused") boolean on) {
     contentMerge = InheritableBoolean.FALSE;
   }
 
   @Option(name = "--require-change-id", aliases = {"--id"}, usage = "if change-id is required")
-  void setRequireChangeId(boolean on) {
+  void setRequireChangeId(@SuppressWarnings("unused") boolean on) {
     requireChangeID = InheritableBoolean.TRUE;
   }
 
   @Option(name = "--no-change-id", aliases = {"--nid"}, usage = "if change-id is not required")
-  void setNoChangeId(boolean on) {
+  void setNoChangeId(@SuppressWarnings("unused") boolean on) {
     requireChangeID = InheritableBoolean.FALSE;
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index 8ce935c..8521058 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.DeleteReviewer;
@@ -29,6 +30,8 @@
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
@@ -78,6 +81,9 @@
   private ReviewDb db;
 
   @Inject
+  private Provider<InternalChangeQuery> queryProvider;
+
+  @Inject
   private ReviewerResource.Factory reviewerFactory;
 
   @Inject
@@ -87,7 +93,10 @@
   private Provider<DeleteReviewer> deleteReviewerProvider;
 
   @Inject
-  private ChangeControl.Factory changeControlFactory;
+  private Provider<CurrentUser> userProvider;
+
+  @Inject
+  private ChangeControl.GenericFactory changeControlFactory;
 
   @Inject
   private ChangesCollection changesCollection;
@@ -166,21 +175,10 @@
 
     // By newer style changeKey?
     //
-    boolean changeKeyParses = false;
-    if (idstr.matches("^I[0-9a-fA-F]*$")) {
-      Change.Key key;
-      try {
-        key = Change.Key.parse(idstr);
-        changeKeyParses = true;
-      } catch (IllegalArgumentException e) {
-        key = null;
-        changeKeyParses = false;
-      }
-
-      if (changeKeyParses) {
-        for (Change change : db.changes().byKeyRange(key, key.max())) {
-          matchChange(matched, change);
-        }
+    boolean changeKeyParses = idstr.matches("^I[0-9a-f]*$");
+    if (changeKeyParses) {
+      for (ChangeData cd : queryProvider.get().byKeyPrefix(idstr)) {
+        matchChange(matched, cd.change());
       }
     }
 
@@ -248,7 +246,8 @@
     try {
       if (change != null
           && inProject(change)
-          && changeControlFactory.controlFor(change).isVisible(db)) {
+          && changeControlFactory.controlFor(change,
+                userProvider.get()).isVisible(db)) {
         matched.add(change.getId());
       }
     } catch (NoSuchChangeException e) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
index 1f3a49a..1c1fb60 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
@@ -32,7 +33,6 @@
 import com.google.gerrit.server.config.ListCaches;
 import com.google.gerrit.server.config.ListCaches.CacheInfo;
 import com.google.gerrit.server.config.ListCaches.CacheType;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.sshd.SshDaemon;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
index 3d1ed3f..108df96 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -16,12 +16,12 @@
 
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.util.IdGenerator;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.sshd.SshDaemon;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
index 147d52a..b069cd4 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -16,14 +16,14 @@
 
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.ListTasks;
 import com.google.gerrit.server.config.ListTasks.TaskInfo;
 import com.google.gerrit.server.git.WorkQueue.Task;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gerrit.sshd.AdminHighPriorityCommand;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
@@ -108,7 +108,7 @@
 
           stdout.print(String.format("%8s %-12s %-4s %s\n",
               task.id, start, startTime(task.startTime),
-              Objects.firstNonNull(remoteName, "n/a")));
+              MoreObjects.firstNonNull(remoteName, "n/a")));
         }
       }
       stdout.print("----------------------------------------------"
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
index 6da858a..889d3fb 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
@@ -16,12 +16,13 @@
 
 import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 
-import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.ChangeListener;
+import com.google.gerrit.common.EventListener;
+import com.google.gerrit.common.EventSource;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.events.ChangeEvent;
+import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.EventTypes;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.CancelableRunnable;
 import com.google.gerrit.sshd.BaseCommand;
@@ -51,27 +52,34 @@
   private IdentifiedUser currentUser;
 
   @Inject
-  private ChangeHooks hooks;
+  private EventSource source;
 
   @Inject
   @StreamCommandExecutor
   private WorkQueue.Executor pool;
 
   /** Queue of events to stream to the connected user. */
-  private final LinkedBlockingQueue<ChangeEvent> queue =
+  private final LinkedBlockingQueue<Event> queue =
       new LinkedBlockingQueue<>(MAX_EVENTS);
 
   private final Gson gson = new Gson();
 
   /** Special event to notify clients they missed other events. */
-  private final Object droppedOutputEvent = new Object() {
-    @SuppressWarnings("unused")
-    final String type = "dropped-output";
-  };
+  private static final class DroppedOutputEvent extends Event {
+    public DroppedOutputEvent() {
+      super("dropped-output");
+    }
+  }
 
-  private final ChangeListener listener = new ChangeListener() {
+  private static final DroppedOutputEvent droppedOutputEvent = new DroppedOutputEvent();
+
+  static {
+    EventTypes.registerClass(droppedOutputEvent);
+  }
+
+  private final EventListener listener = new EventListener() {
     @Override
-    public void onChangeEvent(final ChangeEvent event) {
+    public void onEvent(final Event event) {
       offer(event);
     }
   };
@@ -124,12 +132,12 @@
     }
 
     stdout = toPrintWriter(out);
-    hooks.addChangeListener(listener, currentUser);
+    source.addEventListener(listener, currentUser);
   }
 
   @Override
   protected void onExit(final int rc) {
-    hooks.removeChangeListener(listener);
+    source.removeEventListener(listener);
 
     synchronized (taskLock) {
       done = true;
@@ -140,7 +148,7 @@
 
   @Override
   public void destroy() {
-    hooks.removeChangeListener(listener);
+    source.removeEventListener(listener);
 
     final boolean exit;
     synchronized (taskLock) {
@@ -157,7 +165,7 @@
     }
   }
 
-  private void offer(final ChangeEvent event) {
+  private void offer(final Event event) {
     synchronized (taskLock) {
       if (!queue.offer(event)) {
         dropped = true;
@@ -169,9 +177,9 @@
     }
   }
 
-  private ChangeEvent poll() {
+  private Event poll() {
     synchronized (taskLock) {
-      ChangeEvent event = queue.poll();
+      Event event = queue.poll();
       if (event == null) {
         task = null;
       }
@@ -188,7 +196,7 @@
         // destroy() above, or it closed the stream and is no longer
         // accepting output. Either way terminate this instance.
         //
-        hooks.removeChangeListener(listener);
+        source.removeEventListener(listener);
         flush();
         onExit(0);
         return;
@@ -199,7 +207,7 @@
         dropped = false;
       }
 
-      final ChangeEvent event = poll();
+      final Event event = poll();
       if (event == null) {
         break;
       }
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
index e330834..734b7045 100644
--- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -50,9 +50,9 @@
 import org.kohsuke.args4j.OptionDef;
 import org.kohsuke.args4j.spi.BooleanOptionHandler;
 import org.kohsuke.args4j.spi.EnumOptionHandler;
+import org.kohsuke.args4j.spi.FieldSetter;
 import org.kohsuke.args4j.spi.OptionHandler;
 import org.kohsuke.args4j.spi.Setter;
-import org.kohsuke.args4j.spi.FieldSetter;
 
 import java.io.StringWriter;
 import java.io.Writer;
@@ -131,7 +131,7 @@
 
     char next = '?';
     List<NamedOptionDef> booleans = new ArrayList<>();
-    for (@SuppressWarnings("rawtypes") OptionHandler handler : parser.options) {
+    for (@SuppressWarnings("rawtypes") OptionHandler handler : parser.optionsList) {
       if (handler.option instanceof NamedOptionDef) {
         NamedOptionDef n = (NamedOptionDef) handler.option;
 
@@ -274,7 +274,7 @@
   @SuppressWarnings("rawtypes")
   private OptionHandler findHandler(String name) {
     if (options == null) {
-      options = index(parser.options);
+      options = index(parser.optionsList);
     }
     return options.get(name);
   }
@@ -318,7 +318,7 @@
 
   private class MyParser extends org.kohsuke.args4j.CmdLineParser {
     @SuppressWarnings("rawtypes")
-    private List<OptionHandler> options;
+    private List<OptionHandler> optionsList;
     private HelpOption help;
 
     MyParser(final Object bean) {
@@ -344,14 +344,14 @@
     @SuppressWarnings("rawtypes")
     private OptionHandler add(OptionHandler handler) {
       ensureOptionsInitialized();
-      options.add(handler);
+      optionsList.add(handler);
       return handler;
     }
 
     private void ensureOptionsInitialized() {
-      if (options == null) {
+      if (optionsList == null) {
         help = new HelpOption();
-        options = Lists.newArrayList();
+        optionsList = Lists.newArrayList();
         addOption(help, help);
       }
     }
diff --git a/gerrit-util-http/BUCK b/gerrit-util-http/BUCK
new file mode 100644
index 0000000..7041c0a
--- /dev/null
+++ b/gerrit-util-http/BUCK
@@ -0,0 +1,20 @@
+java_library(
+  name = 'http',
+  srcs = glob(['src/main/java/**/*.java']),
+  provided_deps = ['//lib:servlet-api-3_1'],
+  visibility = ['PUBLIC'],
+)
+
+java_test(
+  name = 'http_tests',
+  srcs = glob(['src/test/java/**/*.java']),
+  deps = [
+    ':http',
+    '//lib:junit',
+    '//lib:servlet-api-3_1',
+    '//lib/easymock:easymock',
+  ],
+  source_under_test = [':http'],
+  # TODO(sop) Remove after Buck supports Eclipse
+  visibility = ['//tools/eclipse:classpath'],
+)
diff --git a/gerrit-util-http/src/main/java/com/google/gerrit/util/http/RequestUtil.java b/gerrit-util-http/src/main/java/com/google/gerrit/util/http/RequestUtil.java
new file mode 100644
index 0000000..922a8d5
--- /dev/null
+++ b/gerrit-util-http/src/main/java/com/google/gerrit/util/http/RequestUtil.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.util.http;
+
+import javax.servlet.http.HttpServletRequest;
+
+/** Utilities for manipulating HTTP request objects. */
+public class RequestUtil {
+  /** HTTP request attribute for storing the Throwable that caused an error condition. */
+  private static final String ATTRIBUTE_ERROR_TRACE =
+      RequestUtil.class.getName() + "/ErrorTraceThrowable";
+
+  public static void setErrorTraceAttribute(HttpServletRequest req, Throwable t) {
+    req.setAttribute(ATTRIBUTE_ERROR_TRACE, t);
+  }
+
+  public static Throwable getErrorTraceAttribute(HttpServletRequest req) {
+    return (Throwable) req.getAttribute(ATTRIBUTE_ERROR_TRACE);
+  }
+
+  /**
+   * @return the same value as {@link HttpServletRequest#getPathInfo()}, but
+   *     without decoding URL-encoded characters.
+   */
+  public static String getEncodedPathInfo(HttpServletRequest req) {
+    // Based on com.google.guice.ServletDefinition$1#getPathInfo() from:
+    // https://github.com/google/guice/blob/41c126f99d6309886a0ded2ac729033d755e1593/extensions/servlet/src/com/google/inject/servlet/ServletDefinition.java
+    String servletPath = req.getServletPath();
+    int servletPathLength = servletPath.length();
+    String requestUri = req.getRequestURI();
+    String pathInfo = requestUri.substring(req.getContextPath().length())
+        .replaceAll("[/]{2,}", "/");
+    if (pathInfo.startsWith(servletPath)) {
+      pathInfo = pathInfo.substring(servletPathLength);
+      // Corner case: when servlet path & request path match exactly (without
+      // trailing '/'), then pathinfo is null.
+      if (pathInfo.isEmpty() && servletPathLength > 0) {
+        pathInfo = null;
+      }
+    } else {
+      pathInfo = null;
+    }
+    return pathInfo;
+  }
+
+  private RequestUtil() {
+  }
+}
diff --git a/gerrit-util-http/src/test/java/com/google/gerrit/util/http/RequestUtilTest.java b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/RequestUtilTest.java
new file mode 100644
index 0000000..cfa0111
--- /dev/null
+++ b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/RequestUtilTest.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.util.http;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+public class RequestUtilTest {
+  private List<Object> mocks;
+
+  @Before
+  public void setUp() {
+    mocks = Collections.synchronizedList(new ArrayList<>());
+  }
+
+  @After
+  public void tearDown() {
+    for (Object mock : mocks) {
+      verify(mock);
+    }
+  }
+
+  @Test
+  public void emptyContextPath() {
+    assertEquals("/foo/bar", RequestUtil.getEncodedPathInfo(
+        mockRequest("/s/foo/bar", "", "/s")));
+    assertEquals("/foo%2Fbar", RequestUtil.getEncodedPathInfo(
+        mockRequest("/s/foo%2Fbar", "", "/s")));
+  }
+
+  @Test
+  public void emptyServletPath() {
+    assertEquals("/foo/bar", RequestUtil.getEncodedPathInfo(
+        mockRequest("/c/foo/bar", "/c", "")));
+    assertEquals("/foo%2Fbar", RequestUtil.getEncodedPathInfo(
+        mockRequest("/c/foo%2Fbar", "/c", "")));
+  }
+
+  @Test
+  public void trailingSlashes() {
+    assertEquals("/foo/bar/", RequestUtil.getEncodedPathInfo(
+        mockRequest("/c/s/foo/bar/", "/c", "/s")));
+    assertEquals("/foo/bar/", RequestUtil.getEncodedPathInfo(
+        mockRequest("/c/s/foo/bar///", "/c", "/s")));
+    assertEquals("/foo%2Fbar/", RequestUtil.getEncodedPathInfo(
+        mockRequest("/c/s/foo%2Fbar/", "/c", "/s")));
+    assertEquals("/foo%2Fbar/", RequestUtil.getEncodedPathInfo(
+        mockRequest("/c/s/foo%2Fbar///", "/c", "/s")));
+  }
+
+  @Test
+  public void servletPathMatchesRequestPath() {
+    assertEquals(null, RequestUtil.getEncodedPathInfo(
+        mockRequest("/c/s", "/c", "/s")));
+  }
+
+  private HttpServletRequest mockRequest(String uri, String contextPath, String servletPath) {
+    HttpServletRequest req = createMock(HttpServletRequest.class);
+    expect(req.getRequestURI()).andStubReturn(uri);
+    expect(req.getContextPath()).andStubReturn(contextPath);
+    expect(req.getServletPath()).andStubReturn(servletPath);
+    replay(req);
+    mocks.add(req);
+    return req;
+  }
+}
diff --git a/gerrit-util-ssl/src/main/java/com/google/gerrit/util/ssl/BlindSSLSocketFactory.java b/gerrit-util-ssl/src/main/java/com/google/gerrit/util/ssl/BlindSSLSocketFactory.java
index ed31379..b8af85e 100644
--- a/gerrit-util-ssl/src/main/java/com/google/gerrit/util/ssl/BlindSSLSocketFactory.java
+++ b/gerrit-util-ssl/src/main/java/com/google/gerrit/util/ssl/BlindSSLSocketFactory.java
@@ -34,13 +34,16 @@
 
   static {
     final X509TrustManager dummyTrustManager = new X509TrustManager() {
+      @Override
       public X509Certificate[] getAcceptedIssuers() {
         return null;
       }
 
+      @Override
       public void checkClientTrusted(X509Certificate[] chain, String authType) {
       }
 
+      @Override
       public void checkServerTrusted(X509Certificate[] chain, String authType) {
       }
     };
diff --git a/gerrit-war/BUCK b/gerrit-war/BUCK
index f557edc..35f6084 100644
--- a/gerrit-war/BUCK
+++ b/gerrit-war/BUCK
@@ -10,8 +10,10 @@
     '//gerrit-lucene:lucene',
     '//gerrit-oauth:oauth',
     '//gerrit-openid:openid',
+    '//gerrit-pgm:http',
+    '//gerrit-pgm:init',
     '//gerrit-pgm:init-api',
-    '//gerrit-pgm:init-base',
+    '//gerrit-pgm:util',
     '//gerrit-reviewdb:server',
     '//gerrit-server:server',
     '//gerrit-server/src/main/prolog:common',
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index f70edce..b3c3bfb 100644
--- a/gerrit-war/pom.xml
+++ b/gerrit-war/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-war</artifactId>
-  <version>2.10.3</version>
+  <version>2.11</version>
   <packaging>war</packaging>
   <name>Gerrit Code Review - WAR</name>
   <description>Gerrit WAR</description>
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/SiteInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/SiteInitializer.java
index 9665cdd..6bbbd8f 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/SiteInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/SiteInitializer.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.httpd;
 
-import com.google.gerrit.pgm.BaseInit;
+import com.google.gerrit.pgm.init.BaseInit;
 import com.google.gerrit.pgm.init.PluginsDistribution;
 
 import org.slf4j.Logger;
@@ -54,8 +54,7 @@
         return;
       }
 
-      Connection conn = connectToDb();
-      try {
+      try (Connection conn = connectToDb()) {
         File site = getSiteFromReviewDb(conn);
         if (site == null && initPath != null) {
           site = new File(initPath);
@@ -66,8 +65,6 @@
           new BaseInit(site, new ReviewDbDataSourceProvider(), false, false,
               pluginsDistribution, pluginsToInstall).run();
         }
-      } finally {
-        conn.close();
       }
     } catch (Exception e) {
       LOG.error("Site init failed", e);
@@ -80,19 +77,15 @@
   }
 
   private File getSiteFromReviewDb(Connection conn) {
-    try {
-      Statement stmt = conn.createStatement();
-      try {
-        ResultSet rs = stmt.executeQuery("SELECT site_path FROM system_config");
-        if (rs.next()) {
-          return new File(rs.getString(1));
-        }
-      } finally {
-        stmt.close();
+    try (Statement stmt = conn.createStatement();
+        ResultSet rs = stmt.executeQuery(
+          "SELECT site_path FROM system_config")) {
+      if (rs.next()) {
+        return new File(rs.getString(1));
       }
-      return null;
     } catch (SQLException e) {
       return null;
     }
+    return null;
   }
 }
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index f59401c..c579f45 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
-import com.google.gerrit.server.change.MergeabilityChecksExecutorModule;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.AuthConfigModule;
 import com.google.gerrit.server.config.CanonicalWebUrlModule;
@@ -38,6 +37,7 @@
 import com.google.gerrit.server.config.MasterNodeStartup;
 import com.google.gerrit.server.config.RestCacheAdminModule;
 import com.google.gerrit.server.config.SitePath;
+import com.google.gerrit.server.contact.ContactStoreModule;
 import com.google.gerrit.server.contact.HttpContactStoreConnection;
 import com.google.gerrit.server.git.GarbageCollectionRunner;
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
@@ -46,6 +46,7 @@
 import com.google.gerrit.server.index.IndexModule;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
 import com.google.gerrit.server.mail.SmtpEmailSender;
+import com.google.gerrit.server.mime.MimeUtil2Module;
 import com.google.gerrit.server.patch.DiffExecutorModule;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
 import com.google.gerrit.server.plugins.PluginRestApiModule;
@@ -55,6 +56,7 @@
 import com.google.gerrit.server.schema.DatabaseModule;
 import com.google.gerrit.server.schema.SchemaModule;
 import com.google.gerrit.server.schema.SchemaVersionCheck;
+import com.google.gerrit.server.securestore.SecureStoreClassName;
 import com.google.gerrit.server.ssh.NoSshModule;
 import com.google.gerrit.server.ssh.SshAddressesModule;
 import com.google.gerrit.solr.SolrIndexModule;
@@ -73,6 +75,7 @@
 import com.google.inject.servlet.GuiceFilter;
 import com.google.inject.servlet.GuiceServletContextListener;
 import com.google.inject.spi.Message;
+import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
@@ -167,7 +170,7 @@
       webInjector = createWebInjector();
 
       PluginGuiceEnvironment env = sysInjector.getInstance(PluginGuiceEnvironment.class);
-      env.setCfgInjector(cfgInjector);
+      env.setDbCfgInjector(dbInjector, cfgInjector);
       if (sshInjector != null) {
         env.setSshInjector(sshInjector);
       }
@@ -205,6 +208,7 @@
 
   private Injector createDbInjector() {
     final List<Module> modules = new ArrayList<>();
+    AbstractModule secureStore = createSecureStoreModule();
     if (sitePath != null) {
       Module sitePathModule = new AbstractModule() {
         @Override
@@ -217,13 +221,13 @@
       Module configModule = new GerritServerConfigModule();
       modules.add(configModule);
 
-      Injector cfgInjector = Guice.createInjector(sitePathModule, configModule);
+      Injector cfgInjector = Guice.createInjector(sitePathModule, configModule, secureStore);
       Config cfg = cfgInjector.getInstance(Key.get(Config.class,
           GerritServerConfig.class));
       String dbType = cfg.getString("database", null, "type");
 
       final DataSourceType dst = Guice.createInjector(new DataSourceModule(),
-          configModule, sitePathModule).getInstance(
+          configModule, sitePathModule, secureStore).getInstance(
             Key.get(DataSourceType.class, Names.named(dbType.toLowerCase())));
       modules.add(new LifecycleModule() {
         @Override
@@ -238,6 +242,7 @@
       });
 
     } else {
+      modules.add(secureStore);
       modules.add(new LifecycleModule() {
         @Override
         protected void configure() {
@@ -279,8 +284,8 @@
     modules.add(new WorkQueue.Module());
     modules.add(new ChangeHookRunner.Module());
     modules.add(new ReceiveCommitsExecutorModule());
-    modules.add(new MergeabilityChecksExecutorModule());
     modules.add(new DiffExecutorModule());
+    modules.add(new MimeUtil2Module());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
     modules.add(new InternalAccountDirectory.Module());
     modules.add(new DefaultCacheFactory.Module());
@@ -311,7 +316,7 @@
     modules.add(new AbstractModule() {
       @Override
       protected void configure() {
-        bind(GerritUiOptions.class).toInstance(new GerritUiOptions(false));
+        bind(GerritOptions.class).toInstance(new GerritOptions(false, false));
       }
     });
     modules.add(GarbageCollectionRunner.module());
@@ -340,6 +345,7 @@
     modules.add(H2CacheBasedWebSession.module());
     modules.add(HttpContactStoreConnection.module());
     modules.add(new HttpPluginModule());
+    modules.add(new ContactStoreModule());
 
     AuthConfig authConfig = cfgInjector.getInstance(AuthConfig.class);
     if (authConfig.getAuthType() == AuthType.OPENID) {
@@ -347,6 +353,7 @@
     } else if (authConfig.getAuthType() == AuthType.OAUTH) {
       modules.add(new OAuthModule());
     }
+    modules.add(sysInjector.getInstance(GetUserFilter.Module.class));
 
     return sysInjector.createChildInjector(modules);
   }
@@ -372,4 +379,16 @@
       manager = null;
     }
   }
+
+  private AbstractModule createSecureStoreModule() {
+    return new AbstractModule() {
+      @Override
+      public void configure() {
+        String secureStoreClassName =
+            GerritServerConfigModule.getSecureStoreClassName(sitePath);
+        bind(String.class).annotatedWith(SecureStoreClassName.class).toProvider(
+            Providers.of(secureStoreClassName));
+      }
+    };
+  }
 }
diff --git a/lib/BUCK b/lib/BUCK
index 42170ee..a880f06 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -26,18 +26,19 @@
 
 maven_jar(
   name = 'gwtorm',
-  id = 'com.google.gerrit:gwtorm:1.14',
-  bin_sha1 = '7e7562d2a8ae233ac9f23ec90dee1a01646483c0',
-  src_sha1 = 'ae991fdefe5e92ee7ed754786b924dc1ec119a8b',
+  id = 'com.google.gerrit:gwtorm:1.14-14-gf54f1f1',
+  bin_sha1 = 'c02267e0245dd06930ea64a2d7c5ddc5ba6d9cfb',
+  src_sha1 = '3d17ae8a173eb34d89098c748f28cddd5080adbc',
   license = 'Apache2.0',
   deps = [':protobuf'],
+  repository = GERRIT,
 )
 
 maven_jar(
   name = 'gwtjsonrpc',
-  id = 'gwtjsonrpc:gwtjsonrpc:1.5',
-  bin_sha1 = '8995287e2c3c866e826d06993904e2c8d7961e4b',
-  src_sha1 = 'c9461f6c0490f26720e3ff15b5607320eab89d96',
+  id = 'gwtjsonrpc:gwtjsonrpc:1.7-2-g272ca32',
+  bin_sha1 = '91be25537f7e53e0b5ff5edb9a42ebfc56f764b6',
+  src_sha1 = '7e6d8892f2e3bf21a9854afcfd2534263636dcbc',
   license = 'Apache2.0',
   repository = GERRIT,
 )
@@ -51,8 +52,8 @@
 
 maven_jar(
   name = 'guava',
-  id = 'com.google.guava:guava:17.0',
-  sha1 = '9c6ef172e8de35fd8d4d8783e4821e57cdef7445',
+  id = 'com.google.guava:guava:18.0',
+  sha1 = 'cce0823396aa693798f8882e64213b1772032b09',
   license = 'Apache2.0',
 )
 
@@ -116,24 +117,24 @@
 
 maven_jar(
   name = 'pegdown',
-  id = 'org.pegdown:pegdown:1.2.1',
-  sha1 = '47689e060d90f90431b5ab2df911452b93930d8c',
+  id = 'org.pegdown:pegdown:1.4.2',
+  sha1 = 'd96db502ed832df867ff5d918f05b51ba3879ea7',
   license = 'Apache2.0',
   deps = [':parboiled-java'],
 )
 
 maven_jar(
   name = 'parboiled-core',
-  id = 'org.parboiled:parboiled-core:1.1.6',
-  sha1 = '11bd0c34fc6ac3c3cbf440ab8180cc6422c044e9',
+  id = 'org.parboiled:parboiled-core:1.1.7',
+  sha1 = 'a60ff9a54cbeb30ec44c89e16ac4c35913cbad5a',
   license = 'Apache2.0',
   attach_source = False,
 )
 
 maven_jar(
   name = 'parboiled-java',
-  id = 'org.parboiled:parboiled-java:1.1.6',
-  sha1 = 'cb2ffa720f75b2fce8cfd1875599319e75ea9557',
+  id = 'org.parboiled:parboiled-java:1.1.7',
+  sha1 = '2298c64ce8ee8e2fb37e97e16d7be52f0c7cf61f',
   license = 'Apache2.0',
   deps = [
     ':parboiled-core',
@@ -142,7 +143,7 @@
     '//lib/ow2:ow2-asm-util',
   ],
   attach_source = False,
-  visibility = [],
+  visibility = ['//gerrit-server:server_tests'],
 )
 
 maven_jar(
@@ -171,8 +172,8 @@
 
 maven_jar(
   name = 'junit',
-  id = 'junit:junit:4.11',
-  sha1 = '4e031bb61df09069aeb2bffb4019e7a5034a4ee0',
+  id = 'junit:junit:4.10',
+  sha1 = 'e4f1766ce7404a08f45d859fb9c226fc9e41a861',
   license = 'DO_NOT_DISTRIBUTE',
   deps = [':hamcrest-core'],
 )
@@ -186,6 +187,17 @@
 )
 
 maven_jar(
+  name = 'truth',
+  id = 'com.google.truth:truth:0.25',
+  sha1 = '503ba892e8482976b81eb2b2df292858fbac3782',
+  license = 'DO_NOT_DISTRIBUTE',
+  deps = [
+    ':guava',
+    ':junit',
+  ],
+)
+
+maven_jar(
   name = 'tukaani-xz',
   id = 'org.tukaani:xz:1.4',
   sha1 = '18a9a2ce6abf32ea1b5fd31dae5210ad93f4e5e3',
diff --git a/lib/antlr/BUCK b/lib/antlr/BUCK
index 732b459..edf153c 100644
--- a/lib/antlr/BUCK
+++ b/lib/antlr/BUCK
@@ -1,11 +1,11 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '3.2'
+VERSION = '3.5.2'
 
 maven_jar(
   name = 'java_runtime',
   id = 'org.antlr:antlr-runtime:' + VERSION,
-  sha1 = '31c746001016c6226bd7356c9f87a6a084ce3715',
+  sha1 = 'cd9cd41361c155f3af0f653009dcecb08d8b4afd',
   license = 'antlr',
 )
 
@@ -18,8 +18,8 @@
 
 maven_jar(
   name = 'stringtemplate',
-  id = 'org.antlr:stringtemplate:' + VERSION,
-  sha1 = '6fe2e3bb57daebd1555494818909f9664376dd6c',
+  id = 'org.antlr:stringtemplate:4.0.2',
+  sha1 = 'e28e09e2d44d60506a7bcb004d6c23ff35c6ac08',
   license = 'antlr',
   attach_source = False,
   visibility = [],
@@ -28,7 +28,7 @@
 maven_jar(
   name = 'tool',
   id = 'org.antlr:antlr:' + VERSION,
-  sha1 = '6b0acabea7bb3da058200a77178057e47e25cb69',
+  sha1 = 'c4a65c950bfc3e7d04309c515b2177c00baf7764',
   license = 'antlr',
   deps = [
     ':java_runtime',
diff --git a/lib/asciidoctor/BUCK b/lib/asciidoctor/BUCK
index b1d5933..66a12c1 100644
--- a/lib/asciidoctor/BUCK
+++ b/lib/asciidoctor/BUCK
@@ -15,6 +15,7 @@
     ':jruby',
     '//lib:args4j',
     '//lib:guava',
+    '//lib/log:api',
   ],
   visibility = ['//tools/eclipse:classpath'],
 )
@@ -42,8 +43,8 @@
 
 maven_jar(
   name = 'asciidoctor',
-  id = 'org.asciidoctor:asciidoctor-java-integration:0.1.4',
-  sha1 = '3596c7142fd30d7b65a0e64ba294f3d9d4bd538f',
+  id = 'org.asciidoctor:asciidoctorj:1.5.0',
+  sha1 = '192df5660f72a0fb76966dcc64193b94fba65f99',
   license = 'Apache2.0',
   visibility = [],
   attach_source = False,
diff --git a/lib/asciidoctor/java/AsciiDoctor.java b/lib/asciidoctor/java/AsciiDoctor.java
index 9e48641..c7562df 100644
--- a/lib/asciidoctor/java/AsciiDoctor.java
+++ b/lib/asciidoctor/java/AsciiDoctor.java
@@ -53,6 +53,9 @@
   @Option(name = "--out-ext", usage = "extension for output files")
   private String outExt = ".html";
 
+  @Option(name = "--base-dir", usage = "base directory")
+  private File basedir;
+
   @Option(name = "--tmp", usage = "temporary output path")
   private File tmpdir;
 
@@ -82,7 +85,7 @@
     OptionsBuilder optionsBuilder = OptionsBuilder.options();
 
     optionsBuilder.backend(backend).docType(DOCTYPE).eruby(ERUBY)
-      .safe(SafeMode.UNSAFE);
+      .safe(SafeMode.UNSAFE).baseDir(basedir);
     // 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
diff --git a/lib/asciidoctor/java/DocIndexer.java b/lib/asciidoctor/java/DocIndexer.java
index 96f3eb5..7eb70c1 100644
--- a/lib/asciidoctor/java/DocIndexer.java
+++ b/lib/asciidoctor/java/DocIndexer.java
@@ -51,7 +51,8 @@
 import java.util.zip.ZipOutputStream;
 
 public class DocIndexer {
-  private static final Version LUCENE_VERSION = Version.LUCENE_48;
+  @SuppressWarnings("deprecation")
+  private static final Version LUCENE_VERSION = Version.LUCENE_4_10_1;
   private static final Pattern SECTION_HEADER = Pattern.compile("^=+ (.*)");
 
   @Option(name = "-o", usage = "output JAR file")
@@ -99,7 +100,7 @@
     RAMDirectory directory = new RAMDirectory();
     IndexWriterConfig config = new IndexWriterConfig(
         LUCENE_VERSION,
-        new StandardAnalyzer(LUCENE_VERSION, CharArraySet.EMPTY_SET));
+        new StandardAnalyzer(CharArraySet.EMPTY_SET));
     config.setOpenMode(OpenMode.CREATE);
     IndexWriter iwriter = new IndexWriter(directory, config);
     for (String inputFile : inputFiles) {
diff --git a/lib/auto/BUCK b/lib/auto/BUCK
new file mode 100644
index 0000000..a596420
--- /dev/null
+++ b/lib/auto/BUCK
@@ -0,0 +1,13 @@
+include_defs('//lib/maven.defs')
+
+maven_jar(
+  name = 'auto-value',
+  id = 'com.google.auto.value:auto-value:1.0',
+  sha1 = '5d13e60f5d190003176ca6ba4a410fae2e3f6315',
+  # Exclude un-relocated dependencies and replace with our own versions; see
+  # https://github.com/google/auto/blob/auto-value-1.0/value/pom.xml#L147
+  exclude = ['org/apache/*'],
+  deps = ['//lib:velocity'],
+  license = 'Apache2.0',
+  visibility = ['PUBLIC'],
+)
diff --git a/lib/auto/auto_value.defs b/lib/auto/auto_value.defs
new file mode 100644
index 0000000..4405747
--- /dev/null
+++ b/lib/auto/auto_value.defs
@@ -0,0 +1,21 @@
+# NOTE: Do not use this file in your build rules; automatically supported by
+# our implementation of java_library.
+
+AUTO_VALUE_DEP = '//lib/auto:auto-value'
+
+# Annotation processor classpath requires transitive dependencies.
+# TODO(dborowitz): Clean this up when buck issue is closed and there is a
+# better supported interface:
+# https://github.com/facebook/buck/issues/85
+AUTO_VALUE_PROCESSOR_DEPS = [
+  '//lib:velocity',
+  '//lib/auto:auto-value',
+  '//lib/commons:collections',
+  '//lib/commons:lang',
+  '//lib/commons:oro',
+]
+
+AUTO_VALUE_PROCESSORS = [
+  'com.google.auto.value.processor.AutoAnnotationProcessor',
+  'com.google.auto.value.processor.AutoValueProcessor',
+]
diff --git a/lib/codemirror/BUCK b/lib/codemirror/BUCK
index e8539c6..dc163c2 100644
--- a/lib/codemirror/BUCK
+++ b/lib/codemirror/BUCK
@@ -1,14 +1,16 @@
 include_defs('//lib/maven.defs')
-include_defs('//lib/codemirror/cm3.defs')
+include_defs('//lib/codemirror/cm.defs')
 include_defs('//lib/codemirror/closure.defs')
 
-VERSION = '28a638a984'
-SHA1 = '68f8f136092a5965778186fb401a33be34cf73ed'
+VERSION = 'd0a2ddaa04'
+SHA1 = '1df573141fcceec039d0260d2d66a5b15d663f9a'
 URL = GERRIT + 'net/codemirror/codemirror-%s.zip' % VERSION
 
 ZIP = 'codemirror-%s.zip' % VERSION
 TOP = 'codemirror-%s' % VERSION
 
+CLOSURE_VERSION = 'v20141120'
+
 CLOSURE_COMPILER_ARGS = [
   '--compilation_level SIMPLE_OPTIMIZATIONS',
   '--warning_level QUIET'
@@ -17,44 +19,75 @@
 genrule(
   name = 'css',
   cmd = ';'.join([
-      ':>$OUT',
-      "echo '/** @license' >>$OUT",
+      "echo '/** @license' >$OUT",
       'unzip -p $(location :zip) %s/LICENSE >>$OUT' % TOP,
       "echo '*/' >>$OUT",
     ] +
     ['unzip -p $(location :zip) %s/%s >>$OUT' % (TOP, n)
-     for n in CM3_CSS + CM3_THEMES]
+     for n in CM_CSS]
   ),
-  deps = [':zip'],
-  out = 'cm3.css',
+  out = 'cm.css',
 )
 
+for n in CM_THEMES:
+  genrule(
+    name = 'theme_%s' % n,
+    cmd = ';'.join([
+        "echo '/** @license' >$OUT",
+        'unzip -p $(location :zip) %s/LICENSE >>$OUT' % TOP,
+        "echo '*/' >>$OUT",
+        'unzip -p $(location :zip) %s/theme/%s.css >>$OUT' % (TOP, n)
+      ]
+    ),
+    out = 'theme_%s.css' % n,
+  )
+
 genrule(
-  name = 'cm3-verbose',
+  name = 'cm-verbose',
   cmd = ';'.join([
-      ':>$OUT',
-      "echo '/** @license' >>$OUT",
+      "echo '/** @license' >$OUT",
       'unzip -p $(location :zip) %s/LICENSE >>$OUT' % TOP,
       "echo '*/' >>$OUT",
     ] +
-    ['unzip -p $(location :zip) %s/%s >>$OUT' % (TOP, n)
-     for n in CM3_JS]
+    ['unzip -p $(location :zip) %s/%s >>$OUT' % (TOP, n) for n in CM_JS] +
+    ['unzip -p $(location :zip) %s/addon/%s >>$OUT' % (TOP, n)
+     for n in CM_ADDONS]
   ),
-  deps = [':zip'],
-  out = 'cm3-verbose.js',
+  out = 'cm-verbose.js',
 )
 
 js_minify(
   name = 'js',
-  generated = [':cm3-verbose'],
+  generated = [':cm-verbose'],
   compiler_args = CLOSURE_COMPILER_ARGS,
-  out = 'cm3.js'
+  out = 'cm.js'
 )
 
+for n in CM_MODES:
+  genrule (
+    name = 'mode_%s_src' % n,
+    cmd = ';'.join([
+      "echo '/** @license' >$OUT",
+      'unzip -p $(location :zip) %s/LICENSE >>$OUT' % TOP,
+      "echo '*/' >>$OUT",
+      'unzip -p $(location :zip) %s/mode/%s/%s.js >>$OUT' % (TOP, n, n),
+      ]),
+    out = 'mode_%s_src.js' %n,
+  )
+  js_minify(
+    name = 'mode_%s_js' % n,
+    generated = [':mode_%s_src' % n],
+    compiler_args = CLOSURE_COMPILER_ARGS,
+    out = 'mode_%s.js' % n,
+  )
+
 prebuilt_jar(
   name = 'codemirror',
   binary_jar = ':jar',
-  deps = ['//lib:LICENSE-codemirror'],
+  deps = [
+    ':jar',
+    '//lib:LICENSE-codemirror',
+  ],
   visibility = ['PUBLIC'],
 )
 
@@ -62,20 +95,14 @@
   name = 'jar',
   cmd = ';'.join([
     'cd $TMP',
-    'unzip -q $(location :zip) %s' %
-    ' '.join(['%s/mode/%s' % (TOP, n) for n in CM3_MODES]),
-    ';'.join(['$(exe :js_minifier) ' +
-    ' '.join(CLOSURE_COMPILER_ARGS) +
-    ' --js_output_file %s/mode/%s.min --js %s/mode/%s'
-    % (TOP, n, TOP, n) for n in CM3_MODES]),
-    ';'.join(['mv %s/mode/%s.min %s/mode/%s' % (TOP, n, TOP, n) for n in CM3_MODES]),
-    'mkdir net',
-    'mv %s net/codemirror' % TOP,
-    'mkdir net/codemirror/lib',
+    'mkdir -p net/codemirror/{lib,mode,theme}',
     'cp $(location :css) net/codemirror/lib',
-    'cp $(location :js) net/codemirror/lib',
-    'zip -qr $OUT *'
-  ]),
+    'cp $(location :js) net/codemirror/lib']
+    + ['cp $(location :mode_%s_js) net/codemirror/mode/%s.js' % (n, n)
+       for n in CM_MODES]
+    + ['cp $(location :theme_%s) net/codemirror/theme/%s.css' % (n, n)
+       for n in CM_THEMES]
+    + ['zip -qr $OUT net/codemirror/{lib,mode,theme}']),
   out = 'codemirror.jar',
 )
 
@@ -87,3 +114,32 @@
     ' -v ' + SHA1,
   out = ZIP,
 )
+
+java_binary(
+  name = 'js_minifier',
+  main_class = 'com.google.javascript.jscomp.CommandLineRunner',
+  deps = [':compiler-jar']
+)
+
+maven_jar(
+  name = 'compiler-jar',
+  id = 'com.google.javascript:closure-compiler:' + CLOSURE_VERSION,
+  sha1 = '369618bf5a96f73e32655dc48919c0f97558d3b1',
+  license = 'Apache2.0',
+  deps = [
+    ':closure-compiler-externs',
+    '//lib:args4j',
+    '//lib:gson',
+    '//lib:guava',
+    '//lib:protobuf',
+  ],
+  visibility = [],
+)
+
+maven_jar(
+  name = 'closure-compiler-externs',
+  id = 'com.google.javascript:closure-compiler-externs:' + CLOSURE_VERSION,
+  sha1 = '247eff337e2737de43c8d963aaaef15bd8cda132',
+  license = 'Apache2.0',
+  visibility = [],
+)
diff --git a/lib/codemirror/closure.defs b/lib/codemirror/closure.defs
index e602b9f..0da1501 100644
--- a/lib/codemirror/closure.defs
+++ b/lib/codemirror/closure.defs
@@ -1,9 +1,3 @@
-# https://code.google.com/p/closure-compiler/wiki/BinaryDownloads?tm=2
-CLOSURE_VERSION = '20140407'
-CLOSURE_COMPILER_URL = 'http://dl.google.com/closure-compiler/compiler-%s.zip' % CLOSURE_VERSION
-COMPILER = 'compiler.jar'
-CLOSURE_COMPILER_SHA1 = 'eeb02bfd45eb4a080b66dd423eaee4bdd1d674e9'
-
 def js_minify(
     name,
     out,
@@ -21,30 +15,4 @@
     cmd = ' '.join(cmd),
     srcs = srcs,
     out = out,
-)
-
-java_binary(
-  name = 'js_minifier',
-  main_class = 'com.google.javascript.jscomp.CommandLineRunner',
-  deps = [':compiler-jar']
-)
-
-prebuilt_jar(
-  name = 'compiler-jar',
-  binary_jar = ':compiler',
-)
-
-genrule(
-  name = 'compiler',
-  cmd = 'unzip -p $(location :closure-compiler-zip) %s >$OUT' % COMPILER,
-  out = COMPILER,
-)
-
-genrule(
-  name = 'closure-compiler-zip',
-  cmd = '$(exe //tools:download_file)' +
-    ' -o $OUT' +
-    ' -u ' + CLOSURE_COMPILER_URL +
-    ' -v ' + CLOSURE_COMPILER_SHA1,
-  out = 'closure-compiler.zip',
-)
+  )
diff --git a/lib/codemirror/cm.defs b/lib/codemirror/cm.defs
new file mode 100644
index 0000000..db87b25
--- /dev/null
+++ b/lib/codemirror/cm.defs
@@ -0,0 +1,82 @@
+CM_CSS = [
+  'lib/codemirror.css',
+  'addon/dialog/dialog.css',
+  'addon/scroll/simplescrollbars.css',
+  'addon/search/matchesonscrollbar.css',
+]
+
+CM_JS = [
+  'lib/codemirror.js',
+  'mode/meta.js',
+  'keymap/vim.js',
+]
+
+CM_ADDONS = [
+  'dialog/dialog.js',
+  'edit/trailingspace.js',
+  'scroll/annotatescrollbar.js',
+  'scroll/simplescrollbars.js',
+  'search/matchesonscrollbar.js',
+  'search/searchcursor.js',
+  'search/search.js',
+  'selection/mark-selection.js',
+  'mode/overlay.js',
+  'mode/simple.js',
+]
+
+# Available themes must be enumerated here,
+# in gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/Theme.java,
+# in gerrit-gwtui/src/main/java/net/codemirror/theme/Themes.java
+CM_THEMES = [
+  'eclipse',
+  'elegant',
+  'midnight',
+  'neat',
+  'night',
+  'twilight',
+]
+
+# Available modes must be enumerated here,
+# in gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java,
+# and in CodeMirror's own mode/meta.js script.
+CM_MODES = [
+  'clike',
+  'clojure',
+  'coffeescript',
+  'commonlisp',
+  'css',
+  'd',
+  'dart',
+  'diff',
+  'dockerfile',
+  'dtd',
+  'erlang',
+  'gas',
+  'gfm',
+  'go',
+  'groovy',
+  'haskell',
+  'htmlmixed',
+  'javascript',
+  'lua',
+  'markdown',
+  'perl',
+  'php',
+  'pig',
+  'properties',
+  'python',
+  'r',
+  'rst',
+  'ruby',
+  'scheme',
+  'shell',
+  'smalltalk',
+  'soy',
+  'sql',
+  'stex',
+  'tcl',
+  'velocity',
+  'verilog',
+  'xml',
+  'yaml',
+]
diff --git a/lib/codemirror/cm3.defs b/lib/codemirror/cm3.defs
deleted file mode 100644
index 84f09c3..0000000
--- a/lib/codemirror/cm3.defs
+++ /dev/null
@@ -1,60 +0,0 @@
-CM3_CSS = [
-  'lib/codemirror.css',
-  'addon/dialog/dialog.css',
-]
-
-CM3_THEMES = [
-  'theme/eclipse.css',
-  'theme/elegant.css',
-  'theme/midnight.css',
-  'theme/neat.css',
-  'theme/night.css',
-  'theme/twilight.css',
-]
-
-CM3_JS = [
-  'lib/codemirror.js',
-  'keymap/vim.js',
-  'addon/dialog/dialog.js',
-  'addon/search/searchcursor.js',
-  'addon/search/search.js',
-  'addon/selection/mark-selection.js',
-  'addon/edit/trailingspace.js',
-]
-
-CM3_MODES = [
-  'clike/clike.js',
-  'clojure/clojure.js',
-  'coffeescript/coffeescript.js',
-  'commonlisp/commonlisp.js',
-  'css/css.js',
-  'd/d.js',
-  'diff/diff.js',
-  'dtd/dtd.js',
-  'erlang/erlang.js',
-  'gas/gas.js',
-  'gfm/gfm.js',
-  'go/go.js',
-  'groovy/groovy.js',
-  'haskell/haskell.js',
-  'htmlmixed/htmlmixed.js',
-  'javascript/javascript.js',
-  'lua/lua.js',
-  'markdown/markdown.js',
-  'perl/perl.js',
-  'php/php.js',
-  'pig/pig.js',
-  'properties/properties.js',
-  'python/python.js',
-  'r/r.js',
-  'ruby/ruby.js',
-  'scheme/scheme.js',
-  'shell/shell.js',
-  'smalltalk/smalltalk.js',
-  'sql/sql.js',
-  'tcl/tcl.js',
-  'velocity/velocity.js',
-  'verilog/verilog.js',
-  'xml/xml.js',
-  'yaml/yaml.js',
-]
diff --git a/lib/commons/BUCK b/lib/commons/BUCK
index 85e404f..fe249fa 100644
--- a/lib/commons/BUCK
+++ b/lib/commons/BUCK
@@ -87,31 +87,3 @@
   license = 'Apache2.0',
 )
 
-maven_jar(
-  name = 'httpclient',
-  id = 'org.apache.httpcomponents:httpclient:4.3.4',
-  bin_sha1 = 'a9a1fef2faefed639ee0d0fba5b3b8e4eb2ff2d8',
-  src_sha1 = '7a14aafed8c5e2c4e360a2c1abd1602efa768b1f',
-  license = 'Apache2.0',
-  deps = [
-    ':codec',
-    ':httpcore',
-    '//lib/log:jcl-over-slf4j',
-  ],
-)
-
-maven_jar(
-  name = 'httpcore',
-  id = 'org.apache.httpcomponents:httpcore:4.3.2',
-  bin_sha1 = '31fbbff1ddbf98f3aa7377c94d33b0447c646b6e',
-  src_sha1 = '4809f38359edeea9487f747e09aa58ec8d3a54c5',
-  license = 'Apache2.0',
-)
-
-maven_jar(
-  name = 'httpmime',
-  id = 'org.apache.httpcomponents:httpmime:4.3.4',
-  bin_sha1 = '54ffde537682aea984c22fbcf0106f21397c5f9b',
-  src_sha1 = '0651e21152b0963661068f948d84ed08c18094f8',
-  license = 'Apache2.0',
-)
diff --git a/lib/gwt/BUCK b/lib/gwt/BUCK
index 8d2b718..bc80aa5 100644
--- a/lib/gwt/BUCK
+++ b/lib/gwt/BUCK
@@ -1,11 +1,11 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '2.6.1'
+VERSION = '2.7.0'
 
 maven_jar(
   name = 'user',
   id = 'com.google.gwt:gwt-user:' + VERSION,
-  sha1 = 'c078b1b8cc0281214b0eb458d2c283d039374fad',
+  sha1 = 'bdc7af42581745d3d79c2efe0b514f432b998a5b',
   license = 'Apache2.0',
   attach_source = False,
 )
@@ -13,17 +13,26 @@
 maven_jar(
   name = 'dev',
   id = 'com.google.gwt:gwt-dev:' + VERSION,
-  sha1 = 'db237e4be0aa1fe43425d2c51ab5485dba211ddd',
+  sha1 = 'c2c3dd5baf648a0bb199047a818be5e560f48982',
   license = 'Apache2.0',
   deps = [
     ':javax-validation',
     ':javax-validation_src',
+    ':json',
   ],
   attach_source = False,
   exclude = ['org/eclipse/jetty/*'],
 )
 
 maven_jar(
+  name = 'json',
+  id = 'org.json:json:20140107',
+  sha1 = 'd1ffca6e2482b002702c6a576166fd685e3370e3',
+  license = 'DO_NOT_DISTRIBUTE',
+  attach_source = False,
+)
+
+maven_jar(
   name = 'javax-validation',
   id = 'javax.validation:validation-api:1.0.0.GA',
   bin_sha1 = 'b6bd7f9d78f6fdaa3c37dae18a4bd298915f328e',
@@ -51,4 +60,3 @@
   license = 'Apache2.0',
   visibility = [],
 )
-
diff --git a/lib/httpcomponents/BUCK b/lib/httpcomponents/BUCK
new file mode 100644
index 0000000..50e463d
--- /dev/null
+++ b/lib/httpcomponents/BUCK
@@ -0,0 +1,31 @@
+include_defs('//lib/maven.defs')
+
+maven_jar(
+  name = 'httpclient',
+  id = 'org.apache.httpcomponents:httpclient:4.3.4',
+  bin_sha1 = 'a9a1fef2faefed639ee0d0fba5b3b8e4eb2ff2d8',
+  src_sha1 = '7a14aafed8c5e2c4e360a2c1abd1602efa768b1f',
+  license = 'Apache2.0',
+  deps = [
+    '//lib/commons:codec',
+    ':httpcore',
+    '//lib/log:jcl-over-slf4j',
+  ],
+)
+
+maven_jar(
+  name = 'httpcore',
+  id = 'org.apache.httpcomponents:httpcore:4.3.2',
+  bin_sha1 = '31fbbff1ddbf98f3aa7377c94d33b0447c646b6e',
+  src_sha1 = '4809f38359edeea9487f747e09aa58ec8d3a54c5',
+  license = 'Apache2.0',
+)
+
+maven_jar(
+  name = 'httpmime',
+  id = 'org.apache.httpcomponents:httpmime:4.3.4',
+  bin_sha1 = '54ffde537682aea984c22fbcf0106f21397c5f9b',
+  src_sha1 = '0651e21152b0963661068f948d84ed08c18094f8',
+  license = 'Apache2.0',
+)
+
diff --git a/lib/jetty/BUCK b/lib/jetty/BUCK
index 13e9774..891fcec 100644
--- a/lib/jetty/BUCK
+++ b/lib/jetty/BUCK
@@ -1,24 +1,21 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '9.2.1.v20140609'
+VERSION = '9.2.9.v20150224'
 EXCLUDE = ['about.html']
 
 maven_jar(
   name = 'servlet',
   id = 'org.eclipse.jetty:jetty-servlet:' + VERSION,
-  sha1 = 'f2327faaf09a3f306babc209f9a7ae01b1528464',
+  sha1 = '1797875a3cc524d181733f323866a5f7bbca03a7',
   license = 'Apache2.0',
-  deps = [
-    ':security',
-    '//lib:servlet-api-3_1',
-  ],
+  deps = [':security'],
   exclude = EXCLUDE,
 )
 
 maven_jar(
   name = 'security',
   id = 'org.eclipse.jetty:jetty-security:' + VERSION,
-  sha1 = '8ac8cc9e5c66eb6022cbe80f4e22d4e42dc5e643',
+  sha1 = '1747a52b01afbf96b58b0ae0f352185560768fc2',
   license = 'Apache2.0',
   deps = [':server'],
   exclude = EXCLUDE,
@@ -26,11 +23,10 @@
 )
 
 maven_jar(
-  name = 'webapp',
-  id = 'org.eclipse.jetty:jetty-webapp:' + VERSION,
-  sha1 = '906e0f4ba7a0cebb8af61513c8511981ba2ccf6e',
+  name = 'servlets',
+  id = 'org.eclipse.jetty:jetty-servlets:' + VERSION,
+  sha1 = '9b04f638c23a4db7c8e2dbfe31ab7370ce972ade',
   license = 'Apache2.0',
-  deps = [':xml'],
   exclude = EXCLUDE,
   visibility = [
     '//tools/eclipse:classpath',
@@ -39,18 +35,9 @@
 )
 
 maven_jar(
-  name = 'xml',
-  id = 'org.eclipse.jetty:jetty-xml:' + VERSION,
-  sha1 = '0d589789eb98d31160d11413b6323b9ea4569046',
-  license = 'Apache2.0',
-  exclude = EXCLUDE,
-  visibility = [],
-)
-
-maven_jar(
   name = 'server',
   id = 'org.eclipse.jetty:jetty-server:' + VERSION,
-  sha1 = 'd02c51c4f8eec3174b09b6e978feaaf05c3dc4ea',
+  sha1 = 'd30a52e992c3484569f58763f55097a1da3202ee',
   license = 'Apache2.0',
   exported_deps = [
     ':continuation',
@@ -62,7 +49,7 @@
 maven_jar(
   name = 'jmx',
   id = 'org.eclipse.jetty:jetty-jmx:' + VERSION,
-  sha1 = '1258d5ac618b120026da8a82283e6cb8ff4638a6',
+  sha1 = 'e0a9df505fbcc7c0481209325a106b922097468d',
   license = 'Apache2.0',
   exported_deps = [
     ':continuation',
@@ -74,7 +61,7 @@
 maven_jar(
   name = 'continuation',
   id = 'org.eclipse.jetty:jetty-continuation:' + VERSION,
-  sha1 = 'e5bf20cdcd9c2878677f3c0f43baea2725f8c59e',
+  sha1 = '476cae89c420170549b4851ed58dca25f349d16d',
   license = 'Apache2.0',
   exclude = EXCLUDE,
 )
@@ -82,7 +69,7 @@
 maven_jar(
   name = 'http',
   id = 'org.eclipse.jetty:jetty-http:' + VERSION,
-  sha1 = 'a132617cb898afc9d4ce5d586e11ad90b9831fff',
+  sha1 = '8b30ddc8304df24a36efbfa267acc24b7403b692',
   license = 'Apache2.0',
   exported_deps = [':io'],
   exclude = EXCLUDE,
@@ -91,7 +78,7 @@
 maven_jar(
   name = 'io',
   id = 'org.eclipse.jetty:jetty-io:' + VERSION,
-  sha1 = '8465fe92159632e9f0a1bfe6951dba8163ac0b12',
+  sha1 = '06a4a23ee9decf2762d052bc2ae0501c08cc9023',
   license = 'Apache2.0',
   exported_deps = [':util'],
   exclude = EXCLUDE,
@@ -101,7 +88,7 @@
 maven_jar(
   name = 'util',
   id = 'org.eclipse.jetty:jetty-util:' + VERSION,
-  sha1 = '4ae7ac5d3cfcb21bc288dd3f4ec3ba2823442f0d',
+  sha1 = 'b5fb774a02158e9f66fed949581159a8d0dfcbe1',
   license = 'Apache2.0',
   exclude = EXCLUDE,
   visibility = [],
diff --git a/lib/local.defs b/lib/local.defs
new file mode 100644
index 0000000..6eec581
--- /dev/null
+++ b/lib/local.defs
@@ -0,0 +1,33 @@
+def local_jar(
+    name,
+    jar,
+    src = None,
+    deps = [],
+    visibility = ['PUBLIC']):
+  binjar = name + '.jar'
+  srcjar = name + '-src.jar'
+  genrule(
+    name = '%s__local_bin' % name,
+    cmd = 'ln -s %s $OUT' % jar,
+    out = binjar)
+  if src:
+    genrule(
+      name = '%s__local_src' % name,
+      cmd = 'ln -s %s $OUT' % src,
+      out = srcjar)
+    prebuilt_jar(
+      name = '%s_src' % name,
+      binary_jar = ':%s__local_src' % name,
+      visibility = visibility,
+    )
+  else:
+    srcjar = None
+
+  prebuilt_jar(
+    name = name,
+    deps = deps,
+    binary_jar = ':%s__local_bin' % name,
+    source_jar = ':%s__local_src' % name if srcjar else None,
+    visibility = visibility,
+ )
+
diff --git a/lib/lucene/BUCK b/lib/lucene/BUCK
index 9ccc5aa..9026f79 100644
--- a/lib/lucene/BUCK
+++ b/lib/lucene/BUCK
@@ -1,11 +1,11 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '4.8.1'
+VERSION = '4.10.2'
 
 maven_jar(
   name = 'core',
   id = 'org.apache.lucene:lucene-core:' + VERSION,
-  sha1 = 'a549eef6316a2c38d4cda932be809107deeaf8a7',
+  sha1 = 'c01e3d675d277e0a93e7890d03cc3246b2cdecaa',
   license = 'Apache2.0',
   exclude = [
     'META-INF/LICENSE.txt',
@@ -16,7 +16,7 @@
 maven_jar(
   name = 'analyzers-common',
   id = 'org.apache.lucene:lucene-analyzers-common:' + VERSION,
-  sha1 = '6e3731524351c83cd21022a23bee5e87f0575555',
+  sha1 = 'f977f8c443e8f4e9d1fd7fdfda80a6cf60b3e7c2',
   license = 'Apache2.0',
   exclude = [
     'META-INF/LICENSE.txt',
@@ -27,6 +27,6 @@
 maven_jar(
   name = 'query-parser',
   id = 'org.apache.lucene:lucene-queryparser:' + VERSION,
-  sha1 = 'f3e105d74137906fdeb2c7bc4dd68c08564778f9',
+  sha1 = 'd70f54e1060d553ba7aeb4d49a71fd0c068499e8',
   license = 'Apache2.0',
 )
diff --git a/lib/maven.defs b/lib/maven.defs
index 23708ca..4edba9c 100644
--- a/lib/maven.defs
+++ b/lib/maven.defs
@@ -12,7 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-ATLASSIAN = 'ATLASSIAN:'
+include_defs('//lib/local.defs')
+
 GERRIT = 'GERRIT:'
 GERRIT_API = 'GERRIT_API:'
 MAVEN_CENTRAL = 'MAVEN_CENTRAL:'
@@ -139,35 +140,3 @@
       visibility = visibility,
     )
 
-def local_jar(
-    name,
-    jar,
-    src = None,
-    deps = [],
-    visibility = ['PUBLIC']):
-  binjar = name + '.jar'
-  srcjar = name + '-src.jar'
-  genrule(
-    name = '%s__local_bin' % name,
-    cmd = 'ln -s %s $OUT' % jar,
-    out = binjar)
-  if src:
-    genrule(
-      name = '%s__local_src' % name,
-      cmd = 'ln -s %s $OUT' % src,
-      out = srcjar)
-    prebuilt_jar(
-      name = '%s_src' % name,
-      binary_jar = ':%s__local_src' % name,
-      visibility = visibility,
-    )
-  else:
-    srcjar = None
-
-  prebuilt_jar(
-    name = name,
-    deps = deps,
-    binary_jar = ':%s__local_bin' % name,
-    source_jar = ':%s__local_src' % name if srcjar else None,
-    visibility = visibility,
-  )
diff --git a/lib/openid/BUCK b/lib/openid/BUCK
index c6c8baf..728698b 100644
--- a/lib/openid/BUCK
+++ b/lib/openid/BUCK
@@ -8,7 +8,7 @@
   deps = [
     ':nekohtml',
     ':xerces',
-    '//lib/commons:httpclient',
+    '//lib/httpcomponents:httpclient',
     '//lib/log:jcl-over-slf4j',
     '//lib/guice:guice',
   ],
diff --git a/lib/ow2/BUCK b/lib/ow2/BUCK
index 81d1618..61b4e1b 100644
--- a/lib/ow2/BUCK
+++ b/lib/ow2/BUCK
@@ -1,32 +1,32 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '4.1'
+VERSION = '5.0.3'
 
 maven_jar(
   name = 'ow2-asm',
   id = 'org.ow2.asm:asm:' + VERSION,
-  sha1 = 'ad568238ee36a820bd6c6806807e8a14ea34684d',
+  sha1 = 'dcc2193db20e19e1feca8b1240dbbc4e190824fa',
   license = 'ow2',
 )
 
 maven_jar(
   name = 'ow2-asm-analysis',
   id = 'org.ow2.asm:asm-analysis:' + VERSION,
-  sha1 = '73401033069e4714f57b60aeae02f97210aaa64e',
+  sha1 = 'c7126aded0e8e13fed5f913559a0dd7b770a10f3',
   license = 'ow2',
 )
 
 maven_jar(
   name = 'ow2-asm-tree',
   id = 'org.ow2.asm:asm-tree:' + VERSION,
-  sha1 = '51085abcc4cb6c6e1cb5551e6f999eb8e31c5b2d',
+  sha1 = '287749b48ba7162fb67c93a026d690b29f410bed',
   license = 'ow2',
 )
 
 maven_jar(
   name = 'ow2-asm-util',
   id = 'org.ow2.asm:asm-util:' + VERSION,
-  sha1 = '6344065cb0f94e2b930a95e6656e040ebc11df08',
+  sha1 = '1512e5571325854b05fb1efce1db75fcced54389',
   license = 'ow2',
 )
 
diff --git a/lib/solr/BUCK b/lib/solr/BUCK
index afaa948..cd39742 100644
--- a/lib/solr/BUCK
+++ b/lib/solr/BUCK
@@ -9,8 +9,8 @@
   deps = [
     ':noggit',
     ':zookeeper',
-    '//lib/commons:httpclient',
-    '//lib/commons:httpmime',
+    '//lib/httpcomponents:httpclient',
+    '//lib/httpcomponents:httpmime',
     '//lib/commons:io',
   ],
 )
diff --git a/plugins/commit-message-length-validator b/plugins/commit-message-length-validator
index a93b856..7923b67 160000
--- a/plugins/commit-message-length-validator
+++ b/plugins/commit-message-length-validator
@@ -1 +1 @@
-Subproject commit a93b85656a68b6a71fe7cf0bb0cc4ed8143657bf
+Subproject commit 7923b67392164dcc65ada85f723fa5111b265484
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
index 5ac8e34..bb20992 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit 5ac8e3475cc284bd1c2159cfded076c959462845
+Subproject commit bb209927b3cfc3073ce5af81c4a8436068be9e0d
diff --git a/plugins/download-commands b/plugins/download-commands
index 6287d6a..baa09c2 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 6287d6a8941f68ba8a3a8c27f2a979c02ede489a
+Subproject commit baa09c2e265a2b264a5fb4571e7eefda04def0c4
diff --git a/plugins/replication b/plugins/replication
index 16db002..1fc7654 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 16db002a0bfe6559a6395dabf155b7a8bfadc968
+Subproject commit 1fc7654ab88d138454afbb1f6a11790394f9154d
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 6170241..603b7b3 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 61702414c046dd6b811c9137b765f9db422f83db
+Subproject commit 603b7b3885a1d2953ca891f58d2a6095e72e5313
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index 73c2381..082bf62 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit 73c2381c5768f216b3a4abb1c623f8d0134a9600
+Subproject commit 082bf62238d4d815636a329cc1ef4d86b36f982e
diff --git a/tools/BUCK b/tools/BUCK
index 08ced89..ee26062 100644
--- a/tools/BUCK
+++ b/tools/BUCK
@@ -21,7 +21,7 @@
   visibility = ['PUBLIC'],
 )
 
-python_library(
+python_test(
   name = 'util_test',
   srcs = ['util_test.py'],
   deps = [':util'],
@@ -44,13 +44,3 @@
   visibility = ['PUBLIC'],
 )
 
-java_test(
-  name = 'python_tests',
-  srcs = glob(['PythonTestCaller.java']),
-  deps = [
-    '//lib:guava',
-    '//lib:junit',
-    ':util',
-    ':util_test',
-  ],
-)
diff --git a/tools/PythonTestCaller.java b/tools/PythonTestCaller.java
deleted file mode 100644
index deabeb4..0000000
--- a/tools/PythonTestCaller.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import static org.junit.Assert.assertTrue;
-
-import com.google.common.io.ByteStreams;
-import com.google.common.base.Splitter;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import org.junit.Test;
-
-public class PythonTestCaller {
-
-  @Test
-  public void resolveUrl() throws Exception {
-    PythonTestCaller.pythonUnit("tools", "util_test");
-  }
-
-  private static void pythonUnit(String d, String sut) throws Exception {
-    ProcessBuilder b =
-        new ProcessBuilder(Splitter.on(' ').splitToList(
-                "python -m unittest " + sut))
-            .directory(new File(d))
-            .redirectErrorStream(true);
-    Process p = null;
-    InputStream i = null;
-    byte[] out;
-    try {
-      p = b.start();
-      i = p.getInputStream();
-      out = ByteStreams.toByteArray(i);
-    } catch (IOException e) {
-      throw new Exception(e);
-    } finally {
-      if (p != null) {
-        p.getOutputStream().close();
-      }
-      if (i != null) {
-        i.close();
-      }
-    }
-    int value;
-    try {
-      value = p.waitFor();
-    } catch (InterruptedException e) {
-      throw new Exception("interrupted waiting for process");
-    }
-    String err = new String(out, "UTF-8");
-    if (value != 0) {
-      System.err.print(err);
-    }
-    assertTrue(err, value == 0);
-  }
-}
diff --git a/tools/checkstyle.xml b/tools/checkstyle.xml
new file mode 100644
index 0000000..a9f8cfe
--- /dev/null
+++ b/tools/checkstyle.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN" "http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
+
+<!--
+    This configuration file was written by the eclipse-cs plugin configuration editor
+-->
+<!--
+    Checkstyle-Configuration: Google Checks for Gerrit
+    Description:
+Checkstyle configuration based on the Google coding conventions (https://google-styleguide.googlecode.com/svn-history/r130/trunk/javaguide.html),
+edited to remove noisy warnings.
+-->
+<module name="Checker">
+  <property name="severity" value="warning"/>
+  <property name="charset" value="UTF-8"/>
+  <module name="TreeWalker">
+    <module name="OuterTypeFilename"/>
+    <module name="LineLength">
+      <property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
+      <property name="max" value="100"/>
+    </module>
+    <module name="OneTopLevelClass"/>
+    <module name="NoLineWrap"/>
+    <module name="EmptyBlock">
+      <property name="option" value="TEXT"/>
+      <property name="tokens" value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
+    </module>
+    <module name="NeedBraces"/>
+    <module name="LeftCurly">
+      <property name="maxLineLength" value="100"/>
+    </module>
+    <module name="RightCurly"/>
+    <module name="RightCurly">
+      <property name="option" value="alone"/>
+      <property name="tokens" value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO, STATIC_INIT, INSTANCE_INIT"/>
+    </module>
+    <module name="WhitespaceAround">
+      <property name="severity" value="ignore"/>
+      <property name="allowEmptyConstructors" value="true"/>
+      <property name="allowEmptyMethods" value="true"/>
+      <property name="allowEmptyTypes" value="true"/>
+      <property name="allowEmptyLoops" value="true"/>
+      <message key="ws.notFollowed" value="WhitespaceAround: ''{0}'' is not followed by whitespace."/>
+      <message key="ws.notPreceded" value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
+      <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
+    </module>
+    <module name="OneStatementPerLine"/>
+    <module name="MultipleVariableDeclarations"/>
+    <module name="ArrayTypeStyle"/>
+    <module name="MissingSwitchDefault"/>
+    <module name="FallThrough"/>
+    <module name="UpperEll"/>
+    <module name="ModifierOrder"/>
+    <module name="EmptyLineSeparator">
+      <property name="severity" value="ignore"/>
+      <property name="allowNoEmptyLineBetweenFields" value="true"/>
+      <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
+    </module>
+    <module name="SeparatorWrap">
+      <property name="severity" value="ignore"/>
+      <property name="option" value="nl"/>
+      <property name="tokens" value="DOT"/>
+      <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
+    </module>
+    <module name="SeparatorWrap">
+      <property name="severity" value="ignore"/>
+      <property name="option" value="EOL"/>
+      <property name="tokens" value="COMMA"/>
+      <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
+    </module>
+    <module name="NoFinalizer"/>
+    <module name="GenericWhitespace">
+      <property name="severity" value="ignore"/>
+      <message key="ws.followed" value="GenericWhitespace ''{0}'' is followed by whitespace."/>
+      <message key="ws.illegalFollow" value="GenericWhitespace ''{0}'' should followed by whitespace."/>
+      <message key="ws.preceded" value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
+      <message key="ws.notPreceded" value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
+      <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
+    </module>
+    <module name="Indentation">
+      <property name="severity" value="ignore"/>
+      <property name="basicOffset" value="2"/>
+      <property name="caseIndent" value="2"/>
+      <property name="arrayInitIndent" value="2"/>
+      <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
+    </module>
+    <module name="MethodParamPad">
+      <property name="severity" value="ignore"/>
+      <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
+    </module>
+    <module name="OperatorWrap">
+      <property name="severity" value="ignore"/>
+      <property name="option" value="NL"/>
+      <property name="tokens" value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR, LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR "/>
+      <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
+    </module>
+  </module>
+  <module name="FileTabCharacter">
+    <property name="severity" value="ignore"/>
+    <property name="eachLine" value="true"/>
+    <metadata name="net.sf.eclipsecs.core.lastEnabledSeverity" value="inherit"/>
+  </module>
+</module>
diff --git a/tools/default.defs b/tools/default.defs
index 27efa11..30518c4 100644
--- a/tools/default.defs
+++ b/tools/default.defs
@@ -14,9 +14,60 @@
 
 # Rule definitions loaded by default into every BUCK file.
 
+include_defs('//lib/auto/auto_value.defs')
 include_defs('//tools/gwt-constants.defs')
+include_defs('//tools/java_doc.defs')
+include_defs('//tools/java_sources.defs')
 import copy
 
+# Set defaults on java rules:
+#  - Add AutoValue annotation processing support.
+#  - Treat source files as UTF-8.
+
+_buck_java_library = java_library
+def java_library(*args, **kwargs):
+  _munge_args(kwargs)
+  _buck_java_library(*args, **kwargs)
+
+_buck_java_test = java_test
+def java_test(*args, **kwargs):
+  _munge_args(kwargs)
+  _buck_java_test(*args, **kwargs)
+
+
+# Munge kwargs to set Gerrit-specific defaults.
+def _munge_args(kwargs):
+  _set_auto_value(kwargs)
+  _set_extra_arguments(kwargs)
+
+def _set_extra_arguments(kwargs):
+  ext = 'extra_arguments'
+  if ext not in kwargs:
+    kwargs[ext] = []
+  extra_args = kwargs[ext]
+
+  for arg in extra_args:
+    if arg.startswith('-encoding'):
+      return
+
+  extra_args.extend(['-encoding', 'UTF-8'])
+
+def _set_auto_value(kwargs):
+  apk = 'annotation_processors'
+  if apk not in kwargs:
+    kwargs[apk] = []
+  aps = kwargs.get(apk, [])
+
+  apdk = 'annotation_processor_deps'
+  if apdk not in kwargs:
+    kwargs[apdk] = []
+  apds = kwargs.get(apdk, [])
+
+  if AUTO_VALUE_DEP in kwargs.get('deps', []):
+    aps.extend(AUTO_VALUE_PROCESSORS)
+    apds.extend(AUTO_VALUE_PROCESSOR_DEPS)
+
+
 def genantlr(
     name,
     srcs,
@@ -38,6 +89,11 @@
     kw['resources'] += [gwt_xml]
   if 'srcs' in kw:
     kw['resources'] += kw['srcs']
+
+  # Buck does not accept duplicate resources. Callers may have
+  # included gwt_xml or srcs as part of resources, so de-dupe.
+  kw['resources'] = list(set(kw['resources']))
+
   java_library(**kw)
 
 def gerrit_extension(
@@ -73,7 +129,7 @@
     type = 'plugin',
     visibility = ['PUBLIC']):
   from multiprocessing import cpu_count
-  mf_cmd = 'v=$(git describe HEAD);'
+  mf_cmd = 'v=\$(git describe HEAD);'
   if manifest_file:
     mf_src = [manifest_file]
     mf_cmd += 'sed "s:@VERSION@:$v:g" $SRCS >$OUT'
@@ -92,20 +148,29 @@
     srcs = mf_src,
     out = 'MANIFEST.MF',
   )
-  gwt_deps = []
   static_jars = []
   if gwt_module:
-    gwt_deps = GWT_PLUGIN_DEPS
     static_jars = [':%s-static-jar' % name]
   java_library(
     name = name + '__plugin',
     srcs = srcs,
     resources = resources,
     deps = deps,
-    provided_deps = ['//gerrit-%s-api:lib' % type] + provided_deps + gwt_deps,
+    provided_deps = ['//gerrit-%s-api:lib' % type] +
+      provided_deps +
+      GWT_PLUGIN_DEPS,
     visibility = ['PUBLIC'],
   )
   if gwt_module:
+    java_library(
+      name = name + '__gwt_module',
+      srcs = [],
+      resources = list(set(srcs + resources)),
+      deps = deps,
+      provided_deps = ['//lib/gwt:dev'] +
+        GWT_PLUGIN_DEPS,
+      visibility = ['PUBLIC'],
+    )
     prebuilt_jar(
       name = '%s-static-jar' % name,
       binary_jar = ':%s-static' % name,
@@ -122,8 +187,8 @@
     gwt_binary(
       name = name + '__gwt_application',
       modules = [gwt_module],
-      deps = gwt_deps,
-      module_deps = [':%s__plugin' % name],
+      deps = GWT_PLUGIN_DEPS,
+      module_deps = [':%s__gwt_module' % name],
       local_workers = cpu_count(),
       strict = True,
       experimental_args = GWT_COMPILER_ARGS,
@@ -139,52 +204,3 @@
     ] + static_jars,
     visibility = visibility,
   )
-
-def java_sources(
-    name,
-    srcs,
-    visibility = []
-  ):
-  java_library(
-    name = name,
-    resources = srcs,
-    visibility = visibility,
-  )
-
-def java_doc(
-    name,
-    title,
-    pkg,
-    paths,
-    srcs = [],
-    deps = [],
-    visibility = [],
-    do_it_wrong = False,
-  ):
-  if do_it_wrong:
-    sourcepath = paths
-  else:
-    sourcepath = ['$SRCDIR/' + n for n in paths]
-  genrule(
-    name = name,
-    cmd = ' '.join([
-      'while ! test -f .buckconfig; do cd ..; done;',
-      'javadoc',
-      '-quiet',
-      '-protected',
-      '-encoding UTF-8',
-      '-charset UTF-8',
-      '-notimestamp',
-      '-windowtitle "' + title + '"',
-      '-link http://docs.oracle.com/javase/7/docs/api',
-      '-subpackages ' + pkg,
-      '-sourcepath ',
-      ':'.join(sourcepath),
-      ' -classpath ',
-      ':'.join(['$(location %s)' % n for n in deps]),
-      '-d $TMP',
-    ]) + ';jar cf $OUT -C $TMP .',
-    srcs = srcs,
-    out = name + '.jar',
-    visibility = visibility,
-)
diff --git a/tools/eclipse/BUCK b/tools/eclipse/BUCK
index 4e76b029..865c9d7 100644
--- a/tools/eclipse/BUCK
+++ b/tools/eclipse/BUCK
@@ -11,14 +11,16 @@
     '//gerrit-main:main_lib',
     '//gerrit-patch-jgit:jgit_patch_tests',
     '//gerrit-plugin-gwtui:gwtui-api-lib',
+    '//gerrit-reviewdb:client_tests',
     '//gerrit-server:server',
     '//gerrit-server:server_tests',
     '//lib/asciidoctor:asciidoc_lib',
     '//lib/asciidoctor:doc_indexer_lib',
+    '//lib/auto:auto-value',
     '//lib/bouncycastle:bcprov',
     '//lib/bouncycastle:bcpg',
     '//lib/bouncycastle:bcpkix',
-    '//lib/jetty:webapp',
+    '//lib/jetty:servlets',
     '//lib/prolog:compiler_lib',
     '//Documentation:index_lib',
   ] + scan_plugins(),
diff --git a/tools/eclipse/gerrit_gwt_debug.launch b/tools/eclipse/gerrit_gwt_debug.launch
index c09997f..3533211 100644
--- a/tools/eclipse/gerrit_gwt_debug.launch
+++ b/tools/eclipse/gerrit_gwt_debug.launch
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
-<listEntry value="/gerrit/buck-out/gen/lib/gwt/dev/gwt-dev-2.6.1.jar"/>
+<listEntry value="/gerrit/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritGwtDebugLauncher.java"/>
 </listAttribute>
 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
 <listEntry value="1"/>
@@ -10,8 +10,13 @@
 <listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
 </listAttribute>
 <booleanAttribute key="org.eclipse.jdt.launching.ATTR_USE_START_ON_FIRST_THREAD" value="true"/>
-<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.google.gwt.dev.DevMode"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-startupUrl /&#10;-war ${resource_loc:/gerrit}/buck-out/gen/gerrit-gwtui/ui_dbg__tmp/war&#10;-server com.google.gerrit.gwtdebug.GerritDebugLauncher&#10;com.google.gerrit.GerritGwtUI"/>
+<listAttribute key="org.eclipse.jdt.launching.CLASSPATH">
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry containerPath=&quot;org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7&quot; javaProject=&quot;gerrit&quot; path=&quot;1&quot; type=&quot;4&quot;/&gt;&#10;"/>
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry id=&quot;org.eclipse.jdt.launching.classpathentry.defaultClasspath&quot;&gt;&#10;&lt;memento exportedEntriesOnly=&quot;false&quot; project=&quot;gerrit&quot;/&gt;&#10;&lt;/runtimeClasspathEntry&gt;&#10;"/>
+</listAttribute>
+<booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.google.gerrit.gwtdebug.GerritGwtDebugLauncher"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-noprecompile -src ${resource_loc:/gerrit} -workDir ${resource_loc:/gerrit}/buck-out/gen/gerrit-gwtui com.google.gerrit.GerritGwtUI -- --console-log --show-stack-trace -d ${resource_loc:/gerrit}/../gerrit_testsite"/>
 <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit"/>
-<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx256M&#10;-XX:MaxPermSize=128M&#10;-Dgwt.persistentunitcachedir=${resource_loc:/gerrit}/buck-out/gen/gerrit-gwtui/ui_dbg__tmp/unit_cache&#10;-Dgerrit.source_root=${resource_loc:/gerrit}&#10;-Dgerrit.site_path=${resource_loc:/gerrit}/../gerrit_testsite&#10;-da:com.google.gwtexpui.globalkey.client.KeyCommandSet"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx1024M&#10;-XX:MaxPermSize=256M&#10;-Dgerrit.disable-gwtui-recompile=true"/>
 </launchConfiguration>
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index 2008316..dd6f248 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -37,15 +37,28 @@
 
 opts = OptionParser()
 opts.add_option('--src', action='store_true')
+opts.add_option('--plugins', help='create eclipse projects for plugins',
+                action='store_true')
 args, _ = opts.parse_args()
 
-def gen_project():
-  p = path.join(ROOT, '.project')
+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 gen_project(name='gerrit', root=ROOT):
+  p = path.join(root, '.project')
   with open(p, 'w') as fd:
     print("""\
 <?xml version="1.0" encoding="UTF-8"?>
 <projectDescription>
-  <name>gerrit</name>
+  <name>""" + name + """</name>
   <buildSpec>
     <buildCommand>
       <name>org.eclipse.jdt.core.javabuilder</name>
@@ -57,22 +70,30 @@
 </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 gen_plugin_classpath(root):
+  p = path.join(root, '.classpath')
+  with open(p, 'w') as fd:
+    if path.exists(path.join(root, 'src', 'test', 'java')):
+      testpath = """
+  <classpathentry kind="src" path="src/test/java"\
+ out="buck-out/eclipse/test"/>"""
+    else:
+      testpath = ""
+    print("""\
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+  <classpathentry kind="src" path="src/main/java"/>%(testpath)s
+  <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+  <classpathentry combineaccessrules="false" kind="src" path="/gerrit"/>
+  <classpathentry kind="output" path="buck-out/eclipse/classes"/>
+</classpath>""" % {"testpath": testpath}, file=fd)
 
+def gen_classpath():
   def make_classpath():
     impl = minidom.getDOMImplementation()
     return impl.createDocument(None, 'classpath', None)
 
-  def classpathentry(kind, path, src=None, out=None):
+  def classpathentry(kind, path, src=None, out=None, exported=None):
     e = doc.createElement('classpathentry')
     e.setAttribute('kind', kind)
     e.setAttribute('path', path)
@@ -80,6 +101,8 @@
       e.setAttribute('sourcepath', src)
     if out:
       e.setAttribute('output', out)
+    if exported:
+      e.setAttribute('exported', 'true')
     doc.documentElement.appendChild(e)
 
   doc = make_classpath()
@@ -87,9 +110,10 @@
   lib = set()
   gwt_src = set()
   gwt_lib = set()
+  plugins = set()
 
   java_library = re.compile(r'[^/]+/gen/(.*)/lib__[^/]+__output/[^/]+[.]jar$')
-  for p in query_classpath(MAIN):
+  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)
@@ -108,7 +132,7 @@
     else:
       lib.add(p)
 
-  for p in query_classpath(GWT):
+  for p in _query_classpath(GWT):
     m = java_library.match(p)
     if m:
       gwt_src.add(m.group(1))
@@ -119,6 +143,9 @@
     if s.startswith('lib/'):
       out = 'buck-out/eclipse/lib'
     elif s.startswith('plugins/'):
+      if args.plugins:
+        plugins.add(s)
+        continue
       out = 'buck-out/eclipse/' + s
 
     p = path.join(s, 'java')
@@ -138,15 +165,17 @@
         if path.exists(p):
           classpathentry('src', p, out=o)
 
-  for libs in [lib, gwt_lib]:
+  for libs in [gwt_lib, 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)
-
+      if args.plugins:
+        classpathentry('lib', j, s, exported=True)
+      else:
+        classpathentry('lib', j, s)
   for s in sorted(gwt_src):
     p = path.join(ROOT, s, 'src', 'main', 'java')
     classpathentry('lib', p, out='buck-out/eclipse/gwtsrc')
@@ -158,6 +187,30 @@
   with open(p, 'w') as fd:
     doc.writexml(fd, addindent='\t', newl='\n', encoding='UTF-8')
 
+  if args.plugins:
+    for plugin in plugins:
+      plugindir = path.join(ROOT, plugin)
+      try:
+        gen_project(plugin.replace('plugins/', ""), plugindir)
+        gen_plugin_classpath(plugindir)
+      except (IOError, OSError) as err:
+        print('error generating project for %s: %s' % (plugin, err),
+              file=sys.stderr)
+
+def gen_factorypath():
+  doc = minidom.getDOMImplementation().createDocument(None, 'factorypath', None)
+  for jar in _query_classpath(['//lib/auto:auto-value']):
+    e = doc.createElement('factorypathentry')
+    e.setAttribute('kind', 'EXTJAR')
+    e.setAttribute('id', path.join(ROOT, jar))
+    e.setAttribute('enabled', 'true')
+    e.setAttribute('runInBatchMode', 'false')
+    doc.documentElement.appendChild(e)
+
+  p = path.join(ROOT, '.factorypath')
+  with open(p, 'w') as fd:
+    doc.writexml(fd, addindent='\t', newl='\n', encoding='UTF-8')
+
 try:
   if args.src:
     try:
@@ -167,6 +220,7 @@
 
   gen_project()
   gen_classpath()
+  gen_factorypath()
 
   try:
     targets = ['//tools:buck.properties'] + MAIN + GWT
diff --git a/tools/gerrit.importorder b/tools/gerrit.importorder
new file mode 100644
index 0000000..831c5fe
--- /dev/null
+++ b/tools/gerrit.importorder
@@ -0,0 +1,9 @@
+#Organize Import Order
+#Wed Jan 14 10:19:45 JST 2015
+6=javax
+5=java
+4=org
+3=net
+2=junit
+1=com
+0=com.google
diff --git a/tools/gwt-constants.defs b/tools/gwt-constants.defs
index cc09d3e..a406aa8 100644
--- a/tools/gwt-constants.defs
+++ b/tools/gwt-constants.defs
@@ -5,7 +5,14 @@
   '-XdisableCastChecking',
 ]
 
-GWT_PLUGIN_DEPS = [
+GWT_COMMON_DEPS = [
+  '//lib/ow2:ow2-asm',
+  '//lib/ow2:ow2-asm-analysis',
+  '//lib/ow2:ow2-asm-util',
+  '//lib/ow2:ow2-asm-tree',
+]
+
+GWT_PLUGIN_DEPS = GWT_COMMON_DEPS + [
   '//gerrit-plugin-gwtui:gwtui-api-lib',
   '//lib/gwt:user',
 ]
diff --git a/tools/java_doc.defs b/tools/java_doc.defs
new file mode 100644
index 0000000..514a730
--- /dev/null
+++ b/tools/java_doc.defs
@@ -0,0 +1,38 @@
+def java_doc(
+    name,
+    title,
+    pkgs,
+    paths,
+    srcs = [],
+    deps = [],
+    visibility = [],
+    do_it_wrong = False,
+  ):
+  if do_it_wrong:
+    sourcepath = paths
+  else:
+    sourcepath = ['$SRCDIR/' + n for n in paths]
+  genrule(
+    name = name,
+    cmd = ' '.join([
+      'while ! test -f .buckconfig; do cd ..; done;',
+      'javadoc',
+      '-quiet',
+      '-protected',
+      '-encoding UTF-8',
+      '-charset UTF-8',
+      '-notimestamp',
+      '-windowtitle "' + title + '"',
+      '-link http://docs.oracle.com/javase/7/docs/api',
+      '-subpackages ',
+      ':'.join(pkgs),
+      '-sourcepath ',
+      ':'.join(sourcepath),
+      ' -classpath ',
+      ':'.join(['$(location %s)' % n for n in deps]),
+      '-d $TMP',
+    ]) + ';jar cf $OUT -C $TMP .',
+    srcs = srcs,
+    out = name + '.jar',
+    visibility = visibility,
+)
diff --git a/tools/java_sources.defs b/tools/java_sources.defs
new file mode 100644
index 0000000..0b3974e
--- /dev/null
+++ b/tools/java_sources.defs
@@ -0,0 +1,10 @@
+def java_sources(
+    name,
+    srcs,
+    visibility = []
+  ):
+  java_library(
+    name = name,
+    resources = srcs,
+    visibility = visibility,
+  )
diff --git a/tools/pack_war.py b/tools/pack_war.py
index ba39856..7e7d895 100755
--- a/tools/pack_war.py
+++ b/tools/pack_war.py
@@ -15,7 +15,7 @@
 
 from __future__ import print_function
 from optparse import OptionParser
-from os import getcwd, chdir, makedirs, path, symlink
+from os import chdir, makedirs, path, symlink
 from subprocess import check_call, check_output
 import sys
 
diff --git a/tools/util.py b/tools/util.py
index f3cc8ce..ec895dd 100644
--- a/tools/util.py
+++ b/tools/util.py
@@ -15,7 +15,6 @@
 from os import path
 
 REPO_ROOTS = {
-  'ATLASSIAN': 'https://maven.atlassian.com/content/repositories/atlassian-3rdparty',
   'GERRIT': 'http://gerrit-maven.storage.googleapis.com',
   'GERRIT_API': 'https://gerrit-api.commondatastorage.googleapis.com/release',
   'MAVEN_CENTRAL': 'http://repo1.maven.org/maven2',
diff --git a/tools/version.py b/tools/version.py
index a994bd8..28f6b65 100755
--- a/tools/version.py
+++ b/tools/version.py
@@ -19,6 +19,12 @@
 import re
 import sys
 
+version_text = """# Maven style API version (e.g. '2.x-SNAPSHOT').
+# Used by :api_install and :api_deploy targets
+# when talking to the destination repository.
+#
+GERRIT_VERSION = '%s'
+"""
 parser = OptionParser()
 opts, args = parser.parse_args()
 
@@ -49,3 +55,9 @@
       outfile.write(outxml)
   except IOError as err:
     print('error updating %s: %s' % (pom, err), file=sys.stderr)
+
+try:
+  with open('VERSION', "w") as version_file:
+    version_file.write(version_text % new_version)
+except IOError as err:
+  print('error updating VERSION: %s' % err, file=sys.stderr)
diff --git a/website/releases/index.html b/website/releases/index.html
index b8d7905..8aedc6b 100644
--- a/website/releases/index.html
+++ b/website/releases/index.html
@@ -39,7 +39,7 @@
 
 <script>
 $.getJSON(
-'https://www.googleapis.com/storage/v1beta2/b/gerrit-releases/o?projection=noAcl&fields=items(name%2Csize)&callback=?',
+'https://www.googleapis.com/storage/v1/b/gerrit-releases/o?projection=noAcl&fields=items(name%2Csize)&callback=?',
 function(data) {
   var doc = document;
   var frg = doc.createDocumentFragment();