Merge branch 'stable-2.10'

* stable-2.10:
  Update 2.10 release notes
  Update JGit to 3.5.3.201412180710-r
  Release notes for Gerrit 2.9.4
  Set version to 2.9.4
  Update JGit to 3.4.2.201412180340-r

Conflicts:
	lib/jgit/BUCK

Change-Id: I174c722d5004ab507fdfc402de0a65f5057583a3
diff --git a/.buckconfig b/.buckconfig
index 43bfa5e..1bf9b36 100644
--- a/.buckconfig
+++ b/.buckconfig
@@ -6,7 +6,7 @@
   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
diff --git a/.buckversion b/.buckversion
index a0c6bc2..22b7472 100644
--- a/.buckversion
+++ b/.buckversion
@@ -1 +1 @@
-0fe4569e871fd6588f7cbfb4b1d4a14baa791a9f
+fcf6eac2ded897981abe0cfb1ed866350c4f8510
diff --git a/.gitignore b/.gitignore
index fec4747..b356144 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
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 0000000..5c83a96
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,17 @@
+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>
+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>
+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>
+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@gmail.com>         Ulrik Sjolin <ulrik.sjolin@gmail.com>
+Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>  Ulrik Sjolin <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..286d59b 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.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..1a0c0db 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -838,6 +838,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
 
@@ -1152,7 +1163,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 +1176,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 +1237,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 +1245,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-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..a876aa9 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -138,6 +138,12 @@
 link:cmd-show-queue.html[gerrit show-queue]::
 	Display the background work queues, including replication.
 
+link:cmd-logging-ls-level.html[gerrit logging ls-level]::
+    List loggers and their logging level.
+
+link:cmd-logging-set-level.html[gerrit logging set-level]::
+    Set the logging level of loggers.
+
 link:cmd-plugin-install.html[gerrit plugin add]::
     Alias for 'gerrit plugin install'.
 
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-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..ba3d609 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,28 @@
 
 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]
+
+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 +89,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 +106,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 +214,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 +229,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-contact.txt b/Documentation/config-contact.txt
index 58df8ea..e0795be 100644
--- a/Documentation/config-contact.txt
+++ b/Documentation/config-contact.txt
@@ -142,7 +142,6 @@
 Full-Name: John Doe
 Preferred-Email: jdoe@example.com
 Identity: jd15@some-isp.com
-Identity: jdoe@example.com <https://www.google.com/accounts/o8/id?id=AIt18axxafvda821aQZaHDF1k8akbalk218sak>
 Identity: jdoe@example.com <http://jdoe.blogger.com/>
 Address:
 	123 Any Street
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index a50c788..0fefc82 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -755,6 +755,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
 
@@ -810,6 +830,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
 
@@ -825,19 +860,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
@@ -977,6 +1015,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
@@ -1490,15 +1537,17 @@
 
 [[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::
@@ -1506,6 +1555,22 @@
 Default change screen UI to direct users to. Valid values are
 `OLD_UI` and `CHANGE_SCREEN2`. Default is `CHANGE_SCREEN2`.
 
+[[gerrit.disableReverseDnsLookup]]gerrit.disableReverseDnsLookup::
++
+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
 
@@ -1717,6 +1782,11 @@
 Optional filename for the ref update hook, if not specified then
 `ref-update` will be used.
 
+[[hooks.hashtagsChangedHook]]hooks.hashtagsChangedHook::
++
+Optional filename for the hashtags changed hook, if not specified then
+`hashtags-changed` will be used.
+
 [[hooks.syncHookTimeout]]hooks.syncHookTimeout::
 +
 Optional timeout value in seconds for synchronous hooks, if not specified
@@ -1743,6 +1813,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
@@ -2011,9 +2099,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
 
@@ -2382,6 +2481,91 @@
 must have the DWORD value `allowtgtsessionkey` set to 1 and the account must not
 have local administrator privileges.
 
+[[ldap.useConnectionPooling]]ldap.useConnectionPooling::
++
+_(Optional)_ Enable the LDAP connection pooling or not.
++
+If it is true, the LDAP service provider maintains a pool of (possibly)
+previously used connections and assigns them to a Context instance as
+needed. When a Context instance is done with a connection (closed or
+garbage collected), the connection is returned to the pool for future use.
++
+For details, see link:http://docs.oracle.com/javase/tutorial/jndi/ldap/pool.html[
+LDAP connection management (Pool)] and link:http://docs.oracle.com/javase/tutorial/jndi/ldap/config.html[
+LDAP connection management (Configuration)]
++
+By default, false.
+
+[[ldap.connectTimeout]]ldap.connectTimeout::
++
+_(Optional)_ Specify how long to wait for a pooled connection.
+This is also used to specify a timeout period for establishment
+of the LDAP connection.
++
+The value is in the usual time-unit format like "1 s", "100 ms",
+etc...
++
+By default there is no timeout and Gerrit will wait indefinitely.
+
+[[ldap.poolAuthentication]]ldap.poolAuthentication::
++
+_(Optional)_  A list of space-separated authentication types of
+connections that may be pooled. Valid types are "none", "simple",
+and "DIGEST-MD5".
++
+Default is "none simple".
+
+[[ldap.poolDebug]]ldap.poolDebug::
++
+_(Optional)_  A string that indicates the level of debug output
+to produce. Valid values are "fine" (trace connection creation
+and removal) and "all" (all debugging information).
+
+[[ldap.poolInitsize]]ldap.poolInitsize::
++
+_(Optional)_ The string representation of an integer that
+represents the number of connections per connection identity
+to create when initially creating a connection for the identity.
++
+Default is 1.
+
+[[ldap.poolMaxsize]]ldap.poolMaxsize::
++
+_(Optional)_ The string representation of an integer that
+represents the maximum number of connections per connection
+identity that can be maintained concurrently.
++
+Default is 0, means that there is no maximum size: A request for
+a pooled connection will use an existing pooled idle connection
+or a newly created pooled connection.
+
+[[ldap.poolPrefsize]]ldap.poolPrefsize::
++
+_(Optional)_ The string representation of an integer that
+represents the preferred number of connections per connection
+identity that should be maintained concurrently.
++
+Default is 0, means that there is no preferred size: A request
+for a pooled connection will result in a newly created connection
+only if no idle ones are available.
+
+[[ldap.poolProtocol]]ldap.poolProtocol::
++
+_(Optional)_  A list of space-separated protocol types of
+connections that may be pooled. Valid types are "plain" and "ssl".
++
+Default is "plain".
+
+[[ldap.poolTimeout]]ldap.poolTimeout::
++
+_(Optional)_ Specify how long an idle connection may remain
+in the pool without being closed and removed from the pool.
++
+The value is in the usual time-unit format like "1 s", "100 ms",
+etc...
++
+By default there is no timeout.
+
 [[mimetype]]
 === Section mimetype
 
@@ -2530,6 +2714,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
@@ -2603,6 +2802,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
 
@@ -3013,6 +3237,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
@@ -3020,6 +3254,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
 
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index 8d58d36..96dbff5 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -125,6 +125,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-project-config.txt b/Documentation/config-project-config.txt
index 43ede06..a0b38be 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -247,6 +247,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 8c82091..561309b 100644
--- a/Documentation/config-sso.txt
+++ b/Documentation/config-sso.txt
@@ -43,20 +43,50 @@
 * `http://` -- trust all OpenID providers using the HTTP protocol
 * `https://` -- trust all OpenID providers using the HTTPS protocol
 
-To trust only Google Accounts:
+To trust only Yahoo!:
 ====
-  git config --file $site_path/etc/gerrit.config auth.trustedOpenID 'https://www.google.com/accounts/o8/id?id='
+  git config --file $site_path/etc/gerrit.config auth.trustedOpenID https://me.yahoo.com
 ====
 
 === 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..fca6bde 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,13 @@
 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.
+
 
 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..fb04bf4 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.
@@ -12,6 +14,7 @@
 ----
   git clone https://gerrit.googlesource.com/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:
@@ -275,6 +280,7 @@
 The following groups of tests are currently supported:
 
 * api
+* edit
 * git
 * pgm
 * rest
@@ -288,6 +294,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 +391,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 +465,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 +503,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 +573,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 +613,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..c356dab 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,17 @@
     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`
   * 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 +235,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 +251,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 +267,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 +305,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 +323,18 @@
 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.
 
 GERRIT
 ------
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index 384bb74..f70d10d 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -47,50 +47,31 @@
 * 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
+* Select in Eclipse Run -> Debug Configurations `gerrit_gwt_debug.launch`
+* Only once: add bookmarks for `Dev Mode On/Off` from codeserver URL:
+`http://localhost:9876/` to your bookmark bar
+* Make sure to activate source maps feature in your browser
+* Load Gerrit page `http://localhost:8080`
+* Open developer tools, source tab
+* Click on `Dev Mode On` bookmark to incrementally recompile changed files
+* Select `gerrit_ui` module to compile (the `Compile` button can also be used
+as a bookmarklet).
+* Navigate on the left to: sourcemaps/gerrit_ui folder (`Ctrl+O` key shortcut
+can be used)
+* Select a file, for example com.google.gerrit.client.change.ChangeScreen2
+and put a breakpoint
+* Navigate in application in change screen and confirm hitting the breakpoint
+* Select `Dev Mode Off` when the debugging session is finished
 
-* Modify the name to be unique.
-
-* 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.
-
-* Switch to Common tab.
-* Change Save as to be Local file.
-* Close the Debug Configurations dialog and save the changes when prompted.
-
-
-[[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]
+After changing the client side code:
+* click `Dev Mode On` then `Compile` to reflect your changes in debug session
+* Hitting `F5` in the browser will just load the last compile output, without
+recompiling
 
 GERRIT
 ------
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 507e0e4..75bee9f 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -300,7 +300,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 {
@@ -656,9 +656,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`
@@ -1259,6 +1256,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:
 
@@ -1733,29 +1750,36 @@
 ----
 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 getPathSetWebLink(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
 
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..eb6592e 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -325,19 +325,34 @@
 [[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 +384,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:
@@ -415,11 +405,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/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-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 61d2e241..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-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/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..433eb4a 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
@@ -58,25 +59,25 @@
 . High availability
 . Replication
 . link:https://gerrit-review.googlesource.com/#/admin/projects/?filter=plugins%252F[Plugins]
-. link:dev-design.html[System Design]
 . 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-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..fcda916c 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()`
diff --git a/Documentation/json.txt b/Documentation/json.txt
index b45f404..af942a2 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.
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/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..3fefb5c 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -264,7 +264,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
@@ -428,7 +428,7 @@
     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:
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..3cbf3eb 100644
--- a/Documentation/rest-api-access.txt
+++ b/Documentation/rest-api-access.txt
@@ -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..11e4831 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
 --
@@ -1024,6 +1062,7 @@
   )]}'
   {
     "context": 10,
+    "theme": "DEFAULT",
     "ignore_whitespace": "IGNORE_ALL_SPACE",
     "intraline_difference": true,
     "line_length": 100,
@@ -1052,6 +1091,7 @@
 
   {
     "context": 10,
+    "theme": "ECLIPSE",
     "ignore_whitespace": "IGNORE_ALL_SPACE",
     "intraline_difference": true,
     "line_length": 100,
@@ -1075,6 +1115,7 @@
   )]}'
   {
     "context": 10,
+    "theme": "ECLIPSE",
     "ignore_whitespace": "IGNORE_ALL_SPACE",
     "intraline_difference": true,
     "line_length": 100,
@@ -1120,7 +1161,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 +1249,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 +1270,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 +1289,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 +1301,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
@@ -1318,7 +1358,7 @@
 === PreferencesInfo
 The `PreferencesInfo` entity contains information about a user's preferences.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |=====================================
 |Field Name              ||Description
 |`changes_per_page`               ||
@@ -1367,7 +1407,7 @@
 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"]
+[options="header",cols="1,^1,5"]
 |=====================================
 |Field Name              ||Description
 |`changes_per_page`               |optional|
@@ -1416,49 +1456,58 @@
 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 +1515,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 +1584,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 +1606,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|
@@ -1571,7 +1623,7 @@
 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 +1635,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..4afe4f7 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -55,7 +55,6 @@
     "mergeable": true,
     "insertions": 0,
     "deletions": 0,
-    "_sortkey": "002cbc25000004e5",
     "_number": 4711,
     "owner": {
       "name": "John Doe"
@@ -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"
@@ -177,7 +174,6 @@
         "mergeable": true,
         "insertions": 4,
         "deletions": 7,
-        "_sortkey": "001e7057000006dc",
         "_number": 1756,
         "owner": {
           "name": "John Doe"
@@ -295,9 +291,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
@@ -325,7 +328,6 @@
       "mergeable": true,
       "insertions": 16,
       "deletions": 7,
-      "_sortkey": "001c9bf400000061",
       "_number": 97,
       "owner": {
         "name": "Shawn Pearce"
@@ -334,6 +336,7 @@
       "revisions": {
         "184ebe53805e102605d11f6b143486d15c23a09c": {
           "_number": 1,
+          "ref": "refs/changes/97/97/1",
           "fetch": {
             "git": {
               "url": "git://localhost/gerrit",
@@ -461,7 +464,6 @@
     "mergeable": true,
     "insertions": 34,
     "deletions": 101,
-    "_sortkey": "0023412400000f7d",
     "_number": 3965,
     "owner": {
       "name": "John Doe"
@@ -514,7 +516,6 @@
     "mergeable": true,
     "insertions": 126,
     "deletions": 11,
-    "_sortkey": "0023412400000f7d",
     "_number": 3965,
     "owner": {
       "_account_id": 1000096,
@@ -758,7 +759,6 @@
     "mergeable": true,
     "insertions": 3,
     "deletions": 310,
-    "_sortkey": "0023412400000f7d",
     "_number": 3965,
     "owner": {
       "name": "John Doe"
@@ -817,7 +817,6 @@
     "mergeable": true,
     "insertions": 2,
     "deletions": 13,
-    "_sortkey": "0023412400000f7d",
     "_number": 3965,
     "owner": {
       "name": "John Doe"
@@ -874,7 +873,6 @@
     "mergeable": false,
     "insertions": 33,
     "deletions": 9,
-    "_sortkey": "0024cf9a000012bf",
     "_number": 4799,
     "owner": {
       "name": "John Doe"
@@ -883,6 +881,7 @@
     "revisions": {
       "27cc4558b5a3d3387dd11ee2df7a117e7e581822": {
         "_number": 2,
+        "ref": "refs/changes/99/4799/2",
         "fetch": {
           "http": {
             "url": "http://gerrit:8080/myProject",
@@ -966,7 +965,6 @@
     "mergeable": true,
     "insertions": 6,
     "deletions": 4,
-    "_sortkey": "0023412400000f7d",
     "_number": 3965,
     "owner": {
       "name": "John Doe"
@@ -1028,7 +1026,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"
@@ -1124,10 +1121,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 +1131,404 @@
   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 in Change Edit
+--
+'POST /changes/link:#change-id[\{change-id\}]/edit
+--
+
+Creates empty change edit or restores file content 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.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "restore_path": "foo"
+  }
+----
+
+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. 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. When
+specified file was deleted in the change edit "`204 No Content`" is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: text/plain;charset=ISO-8859-1
+  X-FYI-Content-Encoding: base64
+
+  RnJvbSA3ZGFkY2MxNTNmZGVhMTdhYTg0ZmYzMmE2ZTI0NWRiYjY...
+----
+
+[[get-edit-file-mime-type]]
+=== Retrieve file content MIME type from Change Edit
+--
+'GET /changes/link:#change-id[\{change-id\}]/edit/path%2fto%2ffile/type
+--
+
+Retrieves content MIME type of a file from a change edit.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit/foo%2fbar%2fbaz%2fqux.txt/type HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "text/plain"
+----
+
+[[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==
+----
+
+[[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
@@ -1418,6 +1809,8 @@
   }
 ----
 
+Adding query parameter `links` (for example `/changes/.../commit?links`)
+returns a link:#commit-info[CommitInfo] with the additional field `web_links`.
 
 [[get-review]]
 === Get Review
@@ -1460,7 +1853,6 @@
     "mergeable": true,
     "insertions": 34,
     "deletions": 45,
-    "_sortkey": "0023412400000f7d",
     "_number": 3965,
     "owner": {
       "_account_id": 1000096,
@@ -1543,6 +1935,7 @@
     "revisions": {
       "674ac754f91e64a0efb8087e59a176484bd534d1": {
       "_number": 2,
+      "ref": "refs/changes/65/3965/2",
       "fetch": {
         "http": {
           "url": "http://gerrit/myProject",
@@ -1724,7 +2117,6 @@
     "mergeable": false,
     "insertions": 21,
     "deletions": 21,
-    "_sortkey": "0024cf9a000012bf",
     "_number": 4799,
     "owner": {
       "name": "John Doe"
@@ -1733,6 +2125,7 @@
     "revisions": {
       "27cc4558b5a3d3387dd11ee2df7a117e7e581822": {
         "_number": 2,
+        "ref": "refs/changes/99/4799/2",
         "fetch": {
           "http": {
             "url": "http://gerrit:8080/myProject",
@@ -1933,9 +2326,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
@@ -2410,6 +2800,32 @@
   Ly8gQ29weXJpZ2h0IChDKSAyMDEwIFRoZSBBbmRyb2lkIE9wZW4gU291cmNlIFByb2plY...
 ----
 
+[[get-content-type]]
+=== Get Content MIME Type
+--
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#file-id[\{file-id\}]/type'
+--
+
+Gets the content MIME type of a file from a certain revision.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/readme.txt/type HTTP/1.0
+----
+
+The content MIME type is returned as string. The content type for the commit
+message (`/COMMIT_MSG`) is always returned as "text/plain".
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "text/plain"
+----
+
 [[get-diff]]
 === Get Diff
 --
@@ -2544,6 +2960,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
@@ -2665,7 +3084,6 @@
     "mergeable": true,
     "insertions": 12,
     "deletions": 11,
-    "_sortkey": "0023412400000f7d",
     "_number": 3965,
     "owner": {
       "name": "John Doe"
@@ -2679,7 +3097,10 @@
 'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/message'
 --
 
-Edit commit message.
+Edit commit message. *Warning*: as of Gerrit 2.11 this REST endpoint is
+deprecated and will be removed in a future version.
+Use link:#put-change-edit-message[put commit message] and
+link:#publish-edit[publish edit] instead.
 
 The commit message must be provided in the request body inside a
 link:#cherrypick-input[CherryPickInput] entity.
@@ -2716,7 +3137,6 @@
     "mergeable": true,
     "insertions": 261,
     "deletions": 101,
-    "_sortkey": "0023412400000f7d",
     "_number": 3965,
     "owner": {
       "name": "John Doe"
@@ -2777,7 +3197,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 +3211,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 +3236,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 +3259,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|
@@ -2854,7 +3274,7 @@
 === GroupBaseInfo
 The `GroupBaseInfo` entity contains base information about the group.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",cols="1,6"]
 |==========================
 |Field Name    |Description
 |`id`          |The id of the group.
@@ -2865,7 +3285,7 @@
 === 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 +3321,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,7 +3358,13 @@
 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]]
@@ -2947,7 +3372,7 @@
 The `RelatedChangesInfo` entity contains information about related
 changes.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",cols="1,6"]
 |===========================
 |Field Name                |Description
 |`changes`                 |A list of
@@ -2962,10 +3387,11 @@
 The `RelatedChangeAndCommitInfo` entity contains information about
 a related change and commit.
 
-[options="header",width="50%",cols="1,^1,5"]
+[options="header",cols="1,^1,5"]
 |===========================
 |Field Name                ||Description
 |`change_id`               |optional|The Change-Id of the change.
+|`status`                  |optional|The status of the change.
 |`commit`                  ||The commit as a
 link:#commit-info[CommitInfo] entity.
 |`_change_number`          |optional|The change number.
@@ -2978,7 +3404,7 @@
 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 +3423,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 +3434,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 +3469,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 +3506,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 +3519,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 +3534,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 +3544,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 +3568,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 +3584,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 +3603,9 @@
 |`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.
 |==========================
 
 [[diff-intraline-info]]
@@ -3184,12 +3622,29 @@
 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.
+|=======================
+
 [[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 +3659,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 +3683,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.
@@ -3252,7 +3707,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 +3717,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 +3742,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 +3757,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`   ||
@@ -3319,7 +3774,7 @@
 === 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 +3786,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 +3798,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 +3811,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 +3856,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 +3869,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 +3888,47 @@
 [[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.
+|`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`||
@@ -3492,7 +3956,7 @@
 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 +3980,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 +3994,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`||
@@ -3565,7 +4029,7 @@
 === 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. +
@@ -3577,7 +4041,7 @@
 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"]
+[options="header",cols="1,6"]
 |==========================
 |Field Name |Description
 |`branches` | The list of branches this change was merged into.
@@ -3590,13 +4054,78 @@
 === 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.
 |======================
 
+[[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.
+|`actions`     ||
+Actions the caller might be able to perform on this change edit. The
+information is a map of view name to link:#action-info[ActionInfo]
+entities.
+|`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.
+|===========================
+
+[[change-edit-input]]
+=== ChangeEditInput
+The `ChangeEditInput` entity contains information for restoring a
+path within change edit.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`restore_path`|optional|Path to file to restore.
+|===========================
+
+[[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.
+|===========================
+
+[[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.
+|===========================
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 2968ebb..2472a4c 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -375,7 +375,7 @@
   Content-Type: application/json;charset=UTF-8
 
   {
-    "operation": "FLUSH"
+    "operation": "FLUSH",
     "caches": [
       "projects",
       "project_list"
@@ -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`               |
@@ -851,7 +851,7 @@
 === CapabilityInfo
 The `CapabilityInfo` entity contains information about a capability.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",cols="1,6"]
 |=================================
 |Field Name           |Description
 |`id`                 |capability ID
@@ -863,7 +863,7 @@
 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`          ||
@@ -883,7 +883,7 @@
 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..0af08a4 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -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). +
@@ -1172,7 +1171,7 @@
 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"]
+[options="header",cols="1,^1,5"]
 |==========================
 |Field Name   ||Description
 |`_one_group` |optional|
@@ -1186,7 +1185,7 @@
 === 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,7 +1196,7 @@
 === 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`|
@@ -1210,7 +1209,7 @@
 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..e19db86 100644
--- a/Documentation/rest-api-plugins.txt
+++ b/Documentation/rest-api-plugins.txt
@@ -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 4d9e02c..af57d53 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -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",
@@ -725,6 +730,7 @@
     "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",
@@ -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",
@@ -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
 --
@@ -1239,6 +1356,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
 
@@ -1547,6 +1760,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>'.
@@ -1566,13 +1783,16 @@
 === BranchInfo
 The `BranchInfo` entity contains information about a branch.
 
-[options="header",width="50%",cols="1,^2,4"]
+[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.
 |=========================
 
 [[ban-input]]
@@ -1580,7 +1800,7 @@
 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 +1811,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.
@@ -1604,7 +1824,7 @@
 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 +1841,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 +1885,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 +1962,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 +2001,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 +2038,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 +2052,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.
@@ -1837,7 +2065,7 @@
 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 +2077,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 +2089,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 +2107,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 +2128,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 +2143,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 +2157,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,7 +2167,7 @@
 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|
@@ -1970,17 +2198,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 +2227,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 +2240,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 +2256,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 +2268,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/user-changeid.txt b/Documentation/user-changeid.txt
index fff14b4..b0e365f 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:
diff --git a/Documentation/user-inline-edit.txt b/Documentation/user-inline-edit.txt
new file mode 100644
index 0000000..e44fb94
--- /dev/null
+++ b/Documentation/user-inline-edit.txt
@@ -0,0 +1,38 @@
+= Inline Edit
+
+This page explains the workflow for creating and amending changes in the
+browser.
+
+
+[[create-change]]
+== Creating a New Empty 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.
+
+There are two different ways to create an empty 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 an empty
+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"]
+
+
+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..d303703 100644
--- a/Documentation/user-review-ui.txt
+++ b/Documentation/user-review-ui.txt
@@ -22,46 +22,48 @@
 
 image::images/user-review-ui-change-screen-commit-message.png[width=800, link="images/user-review-ui-change-screen-commit-message.png"]
 
+[[edit-commit-message]]
 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.
+modifications of the commit message automatically creates a change edit
+that must be published to become a new patch set. 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.
 +
@@ -73,15 +75,15 @@
 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 +121,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 +139,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 +156,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 +178,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 +191,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 +213,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 +224,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 +236,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
@@ -250,7 +253,7 @@
 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 +267,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 +278,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 +287,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 +306,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 +337,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,6 +359,7 @@
 
 image::images/user-review-ui-change-screen-file-list-comments.png[width=800, link="images/user-review-ui-change-screen-file-list-comments.png"]
 
+[[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.
 
@@ -368,11 +377,13 @@
 
 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 +404,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 +491,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 +513,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 +529,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 +538,7 @@
 note that following the link to an indirect descendant change may
 result in a completely different related changes listing.
 
-** Black Dot:
+** [[merged-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
@@ -536,10 +547,14 @@
 the commit was done on `branch-a`, but was then pushed to
 `refs/for/branch-b`.
 
+** [[abandoned-change]]Dark Red Dot:
++
+Indicates an abandoned change.
+
 +
 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 +566,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,6 +601,10 @@
 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.
@@ -603,6 +622,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 +655,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 +666,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.
 
@@ -700,6 +723,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 +733,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 +741,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 +749,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 +777,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 +817,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 +830,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 +839,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 +858,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 +867,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 +982,7 @@
 
 The following diff preferences can be configured:
 
-- `Theme`:
+- [[theme]]`Theme`:
 +
 Controls the theme that is used to render the file content.
 +
@@ -954,7 +990,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 +1010,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 +1022,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 +1039,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 +1055,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,10 +1148,14 @@
   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.
 
+- The new side-by-side diff screen isn't able to highlight line
+  endings.
+
 - Unified diff view is missing:
 +
 By setting `Diff View (New Change Screen)` in the user preferences to
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 5c54259..a9deba8 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
@@ -186,8 +186,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 +235,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.
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.11.txt b/ReleaseNotes/ReleaseNotes-2.11.txt
new file mode 100644
index 0000000..34f2997
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.11.txt
@@ -0,0 +1,81 @@
+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]
+
+Important Notes
+---------------
+
+
+*WARNING:* This release contains schema changes.  To upgrade:
+----
+  java -jar gerrit.war init -d site_path
+  java -jar gerrit.war reindex --recheck-mergeable -d site_path
+----
+
+*WARNING:* Upgrading to 2.11.x requires the server be first upgraded to 2.1.7 (or
+a later 2.1.x version), and then to 2.11.x.  If you are upgrading from 2.2.x.x or
+later, you may ignore this warning and upgrade directly to 2.11.x.
+
+*WARNING:* The 'Generate HTTP Password' capability has been removed.
+
+
+Release Highlights
+------------------
+
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=505[Issue 505]:
+Inline editing: Changes can be edited directly in the browser.
++
+Files can be added, deleted, restored or edited directly in the browser. Change
+edits can be published, deleted and rebased on top of the latest patch set.
+
+
+New Features
+------------
+
+
+Web UI
+~~~~~~
+
+TODO
+
+Global
+^^^^^^
+
+TODO
+
+REST
+~~~~
+
+TODO
+
+SSH
+~~~
+
+TODO
+
+Plugins
+~~~~~~~
+
+TODO
+
+Other
+~~~~~
+
+* The 'Generate HTTP Password' capability is removed.
++
+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.
+
+Upgrades
+--------
+
+TODO
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index c7aab32..decb334 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 ca9c860..b05afb0 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-SNAPSHOT'
+GERRIT_VERSION = '2.11-SNAPSHOT'
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..96b737f 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,25 +14,38 @@
 
 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.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ListChangesOption;
 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.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.project.Util;
 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;
@@ -41,6 +54,8 @@
 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 +75,9 @@
   public Config baseConfig;
 
   @Inject
+  protected AllProjectsName allProjects;
+
+  @Inject
   protected AccountCreator accounts;
 
   @Inject
@@ -77,6 +95,18 @@
   @Inject
   protected PushOneCommit.Factory pushFactory;
 
+  @Inject
+  protected MetaDataUpdate.Server metaDataUpdateFactory;
+
+  @Inject
+  protected ProjectCache projectCache;
+
+  @Inject
+  protected GroupCache groupCache;
+
+  @Inject
+  protected GitRepositoryManager repoManager;
+
   protected Git git;
   protected GerritServer server;
   protected TestAccount admin;
@@ -166,17 +196,17 @@
     return push.to(git, "refs/for/master");
   }
 
-  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)
@@ -218,4 +248,61 @@
         .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 {
+    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());
+  }
+
+  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/GerritServer.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index 85da2f5..16bb2f6 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,20 @@
 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.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;
@@ -52,6 +50,7 @@
       throws Exception {
     final CyclicBarrier serverStarted = new CyclicBarrier(2);
     final Daemon daemon = new Daemon(new Runnable() {
+      @Override
       public void run() {
         try {
           serverStarted.await();
@@ -83,6 +82,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,8 +121,7 @@
     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 + "/";
@@ -156,7 +155,7 @@
     return (T) f.get(obj);
   }
 
-  private static InetAddress getLocalHost() throws UnknownHostException {
+  private static InetAddress getLocalHost() {
     return InetAddress.getLoopbackAddress();
   }
 
@@ -168,7 +167,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..e316464 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 {
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..f896692 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;
@@ -39,6 +38,7 @@
 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;
@@ -80,6 +80,25 @@
         @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 ReviewDb db;
@@ -89,7 +108,7 @@
   private final String fileName;
   private final String content;
   private String changeId;
-  private String tagName;
+  private Tag tag;
 
   @AssistedInject
   PushOneCommit(ChangeNotes.Factory notesFactory,
@@ -129,21 +148,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,28 +167,37 @@
       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), c, subject);
   }
 
-  public void setTag(final String tagName) {
-    this.tagName = tagName;
+  public void setTag(final Tag tag) {
+    this.tag = tag;
   }
 
   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 PatchSet.Id getPatchSetId() throws OrmException {
@@ -197,9 +222,9 @@
         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()));
+      assertThat(resSubj).isEqualTo(c.getSubject());
+      assertThat(expectedStatus).isEqualTo(c.getStatus());
+      assertThat(expectedTopic).isEqualTo(Strings.emptyToNull(c.getTopic()));
       assertReviewers(c, expectedReviewers);
     }
 
@@ -216,11 +241,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(expectedReviewerIds.isEmpty())
+        .named("missing reviewers: " + expectedReviewerIds)
+        .isTrue();
     }
 
     public void assertOkStatus() {
@@ -233,15 +260,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..584186c 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
@@ -31,6 +31,7 @@
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 
 public class RestSession extends HttpSession {
 
@@ -91,12 +92,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..3df82b6 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,7 +23,6 @@
 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.ReviewInput;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -35,14 +31,11 @@
 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 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 +44,74 @@
 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) {
+  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 +122,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 +168,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 +178,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 +197,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 +207,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 +216,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 +226,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 +247,24 @@
         .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.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 +277,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..db45e4c
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/CheckIT.java
@@ -0,0 +1,88 @@
+// 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.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeStatus;
+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()));
+
+    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));
+
+    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..d2e70c3 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()
@@ -47,7 +45,7 @@
   }
 
   @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 +54,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 +66,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 +82,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..11ed309 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,9 +14,9 @@
 
 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 org.eclipse.jgit.lib.Constants.HEAD;
+import static org.junit.Assert.fail;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -26,13 +26,17 @@
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.MergeableInfo;
+import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
 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;
 
@@ -49,8 +53,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 +62,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 +71,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 +86,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 +99,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 +115,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 +124,31 @@
   }
 
   @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);
+
+    assertThat(cherry.get().subject).contains(in.message);
+    assertThat(cherry.get().topic).isEqualTo("someTopic");
+    cherry.current().review(ReviewInput.approve());
+    cherry.current().submit();
+  }
+
+  @Test
+  public void cherryPickIdenticalTree() throws Exception {
     PushOneCommit.Result r = createChange();
     CherryPickInput in = new CherryPickInput();
     in.destination = "foo";
@@ -139,41 +160,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 +240,53 @@
         .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()
-        .id(r.getChangeId())
-        .current();
+  @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.
+  }
+
+  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);
   }
 
   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..abcdf3f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -0,0 +1,716 @@
+// 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.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_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.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.acceptance.RestSession;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+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.common.ListChangesOption;
+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.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.binary.StringUtils;
+import org.eclipse.jgit.api.Git;
+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 final static String FILE_NAME = "foo";
+  private final static String FILE_NAME2 = "foo2";
+  private final static byte[] CONTENT_OLD = "bar".getBytes(UTF_8);
+  private final static byte[] CONTENT_NEW = "baz".getBytes(UTF_8);
+  private final static byte[] CONTENT_NEW2 = "qux".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 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();
+    String 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(edit.getChange().getProject(), edit
+        .getRevision().get(), FILE_NAME), CONTENT_NEW);
+    assertByteArray(fileUtil.getContent(edit.getChange().getProject(), 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(edit.getChange().getProject(), edit
+        .getRevision().get(), FILE_NAME), CONTENT_NEW);
+    assertByteArray(fileUtil.getContent(edit.getChange().getProject(), 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 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(edit.get().getChange().getProject(), 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\nChange-Id: %s",
+        change.getKey());
+    assertThat(adminSession.put(urlEditMessage(), in).getStatusCode())
+        .isEqualTo(SC_NO_CONTENT);
+    RestResponse r = adminSession.get(urlEditMessage());
+    assertThat(adminSession.get(urlEditMessage()).getStatusCode())
+        .isEqualTo(SC_OK);
+    String content = r.getEntityContent();
+    assertThat(StringUtils.newStringUtf8(Base64.decodeBase64(content)))
+        .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(edit.get().getChange().getProject(),
+          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(edit.get().getChange().getProject(),
+          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(edit.get().getChange().getProject(),
+          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(edit.get().getChange().getProject(), edit.get()
+            .getRevision().get(), FILE_NAME), CONTENT_OLD);
+  }
+
+  @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(edit.get().getChange().getProject(), 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(edit.get().getChange().getProject(), 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(edit.get().getChange().getProject(), 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(edit.get().getChange().getProject(), 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(edit.get().getChange().getProject(), 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(edit.get().getChange().getProject(), 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(edit.get().getChange().getProject(), 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(edit.get().getChange().getProject(), 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.get(urlEditFile());
+    assertThat(r.getStatusCode()).isEqualTo(SC_OK);
+    String content = r.getEntityContent();
+    assertThat(StringUtils.newStringUtf8(Base64.decodeBase64(content)))
+        .isEqualTo(StringUtils.newStringUtf8(CONTENT_NEW2));
+  }
+
+  @Test
+  public void getFileContentTypeRest() 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);
+    RestResponse r = adminSession.get(urlEditFileContentType());
+    assertThat(r.getStatusCode()).isEqualTo(SC_OK);
+    String res = newGson().fromJson(r.getReader(), String.class);
+    assertThat(res).isEqualTo("application/octet-stream");
+  }
+
+  @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(edit.get().getChange().getProject(),
+          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(edit.get().getChange().getProject(), 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(edit.get().getChange().getProject(), 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(edit.get().getChange().getProject(), 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 Iterables.getOnlyElement(db.changes()
+        .byKey(new Change.Key(changeId)));
+  }
+
+  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 urlEditFileContentType() {
+    return urlEdit()
+        + "/"
+        + FILE_NAME
+        + "/type";
+  }
+
+  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);
+  }
+}
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..9887c38 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,9 +14,11 @@
 
 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;
@@ -24,17 +26,30 @@
 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
   }
@@ -154,9 +169,9 @@
     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 +180,9 @@
 
     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
@@ -179,22 +194,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..bce2f65 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,11 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
-  srcs = ['DraftChangeBlockedIT.java', 'SubmitOnPushIT.java'],
+  srcs = [
+    'DraftChangeBlockedIT.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/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..aeb71d8 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");
 
@@ -201,21 +157,19 @@
     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());
+    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,9 @@
         .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());
-  }
-
-  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());
+    assertThat(c.getStatus()).isEqualTo(Change.Status.MERGED);
   }
 
   private PatchSetApproval getSubmitter(PatchSet.Id patchSetId)
@@ -277,9 +216,9 @@
 
   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 {
@@ -288,9 +227,10 @@
       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());
+        assertThat(c.getShortMessage()).isEqualTo(PushOneCommit.SUBJECT);
+        assertThat(c.getAuthorIdent().getEmailAddress()).isEqualTo(admin.email);
+        assertThat(c.getCommitterIdent().getEmailAddress()).isEqualTo(
+            admin.email);
       } finally {
         rw.release();
       }
@@ -305,10 +245,11 @@
       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());
+        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());
       } finally {
         rw.release();
       }
@@ -317,24 +258,38 @@
     }
   }
 
-  private void assertTag(Project.NameKey project, String branch, String tagName)
-      throws IOException {
-    Repository r = repoManager.openRepository(project);
+  private void assertTag(Project.NameKey project, String branch,
+      PushOneCommit.Tag tag) throws IOException {
+    Repository repo = repoManager.openRepository(project);
     try {
-      ObjectId headCommit = r.getRef(branch).getObjectId();
-      ObjectId taggedCommit = r.getRef(tagName).getObjectId();
-      assertEquals(headCommit, taggedCommit);
+      Ref tagRef = repo.getRef(tag.name);
+      assertThat(tagRef).isNotNull();
+      ObjectId taggedCommit = null;
+      if (tag instanceof PushOneCommit.AnnotatedTag) {
+        PushOneCommit.AnnotatedTag annotatedTag = (PushOneCommit.AnnotatedTag)tag;
+        RevWalk rw = new RevWalk(repo);
+        try {
+          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();
+        } finally {
+          rw.dispose();
+        }
+      } else {
+        taggedCommit = tagRef.getObjectId();
+      }
+      ObjectId headCommit = repo.getRef(branch).getObjectId();
+      assertThat(taggedCommit).isNotNull();
+      assertThat(taggedCommit).isEqualTo(headCommit);
     } finally {
-      r.close();
+      repo.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..c1f8de3 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,13 +14,11 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
 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 com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
@@ -31,16 +29,17 @@
 import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeStatus;
 import com.google.gerrit.extensions.common.InheritableBoolean;
+import com.google.gerrit.extensions.common.LabelInfo;
 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;
 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.notedb.ChangeNotes;
 import com.google.gerrit.server.project.PutConfig;
 import com.google.gson.reflect.TypeToken;
@@ -68,10 +67,6 @@
 import java.util.List;
 
 public abstract class AbstractSubmit extends AbstractDaemonTest {
-
-  @Inject
-  private GitRepositoryManager repoManager;
-
   @Inject
   private ChangeNotes.Factory notesFactory;
 
@@ -97,7 +92,7 @@
     Git git = createProject(false);
     PushOneCommit.Result change = createChange(git);
     submit(change.getChangeId());
-    assertEquals(change.getCommitId(), getRemoteHead().getId());
+    assertThat(getRemoteHead().getId()).isEqualTo(change.getCommitId());
   }
 
   protected Git createProject() throws JSchException, IOException,
@@ -123,7 +118,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 +127,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();
   }
 
@@ -179,12 +174,12 @@
     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);
     }
     r.consume();
   }
@@ -193,41 +188,41 @@
     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);
   }
 
   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))));
     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 +230,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 {
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/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..bdabfae 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,14 +14,12 @@
 
 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.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeStatus;
-import com.google.gerrit.server.change.ChangeJson;
 
 import org.apache.http.HttpStatus;
 import org.junit.Test;
@@ -33,8 +31,8 @@
     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 +41,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
@@ -77,15 +75,14 @@
 
   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);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftChangeIT.java
index 3ba5ed7..c623f11 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftChangeIT.java
@@ -14,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;
@@ -23,12 +23,11 @@
 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.apache.http.HttpStatus;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -36,58 +35,54 @@
 public class DeleteDraftChangeIT extends AbstractDaemonTest {
 
   @Test
-  public void deleteChange() throws GitAPIException,
-      IOException, RestApiException {
+  public void deleteChange() throws Exception {
     String changeId = createChange().getChangeId();
     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 = deleteChange(changeId, adminSession);
-    assertEquals("Change is not a draft", r.getEntityContent());
-    assertEquals(409, r.getStatusCode());
+    assertThat(r.getEntityContent()).isEqualTo("Change is not a draft");
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT);
   }
 
   @Test
-  public void deleteDraftChange() throws GitAPIException,
-      IOException, RestApiException, OrmException {
+  public void deleteDraftChange() throws Exception {
     String changeId = createDraftChange();
     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 = deleteChange(changeId, adminSession);
-    assertEquals(204, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
   }
 
   @Test
-  public void publishDraftChange() throws GitAPIException,
-      IOException, RestApiException {
+  public void publishDraftChange() throws Exception {
     String changeId = createDraftChange();
     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 = publishChange(changeId);
-    assertEquals(204, r.getStatusCode());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
     c = get(triplet);
-    assertEquals(ChangeStatus.NEW, c.status);
+    assertThat(c.status).isEqualTo(ChangeStatus.NEW);
   }
 
   @Test
-  public void publishDraftPatchSet() throws GitAPIException,
-      IOException, OrmException, RestApiException {
+  public void publishDraftPatchSet() throws Exception {
     String changeId = createDraftChange();
     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 = publishPatchSet(changeId);
-    assertEquals(204, r.getStatusCode());
-    assertEquals(ChangeStatus.NEW, get(triplet).status);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+    assertThat(get(triplet).status).isEqualTo(ChangeStatus.NEW);
   }
 
-  private String createDraftChange() throws GitAPIException, IOException {
+  private String createDraftChange() throws Exception {
     PushOneCommit push = pushFactory.create(db, admin.getIdent());
     return push.to(git, "refs/drafts/master").getChangeId();
   }
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..6e7dca4 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;
@@ -28,6 +28,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 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,17 @@
     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());
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
     Change change = Iterables.getOnlyElement(db.changes().byKey(
         new Change.Key(changeId)).toList());
-    assertEquals(1, db.patchSets().byChange(change.getId())
-        .toList().size());
+    assertThat(db.patchSets().byChange(change.getId()).toList()).hasSize(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(db.changes().byKey(new Change.Key(changeId)).toList()).isEmpty();
   }
 
   private String createDraftChangeWith2PS() throws GitAPIException,
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..c7cb2d4 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.common.truth.Truth.assertThat;
 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 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..0221f2a 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,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 static org.junit.Assert.assertSame;
 
 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 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 +34,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 +58,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 +83,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 +104,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 +125,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 +146,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 +170,18 @@
     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());
   }
 }
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..621fc17 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,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;
 import com.google.gerrit.extensions.common.SubmitType;
@@ -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..77e9741 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 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..32fa3c5 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 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..43272cb 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,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;
 import com.google.gerrit.extensions.common.SubmitType;
@@ -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..15ada4a 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;
@@ -31,14 +30,11 @@
 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..7f7bac5 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.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,40 @@
 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 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 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 +90,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 +111,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 +143,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 +160,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 +169,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 +184,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 +202,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,7 +214,8 @@
 
   private void assertEmptyCommit(String projectName, String... refs)
       throws RepositoryNotFoundException, IOException {
-    Repository repo = git.openRepository(new Project.NameKey(projectName));
+    Repository repo =
+        repoManager.openRepository(new Project.NameKey(projectName));
     RevWalk rw = new RevWalk(repo);
     TreeWalk tw = new TreeWalk(repo);
     try {
@@ -248,11 +223,10 @@
         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/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..9667c52
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -0,0 +1,193 @@
+// 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.common.Comment;
+import com.google.gerrit.extensions.common.CommentInfo;
+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.lang.reflect.Type;
+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();
+  }
+
+  @Test
+  public void createDraft() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    ReviewInput.CommentInput comment = newCommentInfo(
+        "file1", Comment.Side.REVISION, 1, "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 {
+    String file = "file";
+    String contents = "contents";
+    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, Comment.Side.REVISION, 1, "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 {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    ReviewInput.CommentInput comment = newCommentInfo(
+        "file1", Comment.Side.REVISION, 1, "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 {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    ReviewInput.CommentInput comment = newCommentInfo(
+        "file1", Comment.Side.REVISION, 1, "comment 1");
+    CommentInfo returned = addDraft(changeId, revId, comment);
+    CommentInfo actual = getDraftComment(changeId, revId, returned.id);
+    assertCommentInfo(comment, actual);
+  }
+
+  @Test
+  public void deleteDraft() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    ReviewInput.CommentInput comment = newCommentInfo(
+        "file1", Comment.Side.REVISION, 1, "comment 1");
+    CommentInfo returned = addDraft(changeId, revId, comment);
+    deleteDraft(changeId, revId, returned.id);
+    Map<String, List<CommentInfo>> drafts = getDraftComments(changeId, revId);
+    assertThat(drafts).isEmpty();
+  }
+
+  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);
+    if (actual.side == null) {
+      assertThat(Comment.Side.REVISION).isEqualTo(expected.side);
+    }
+  }
+
+  private ReviewInput.CommentInput newCommentInfo(String path,
+      Comment.Side side, int line, String message) {
+    ReviewInput.CommentInput input = new ReviewInput.CommentInput();
+    input.path = path;
+    input.side = side;
+    input.line = line;
+    input.message = message;
+    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..506afb3 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,49 @@
 
 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.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 +65,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 +93,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 +134,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);
+    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).currentPatchSetId();
+  }
+
+  private PatchSet getPatchSet(Change c) throws OrmException {
+    return db.patchSets().get(c.currentPatchSetId());
+  }
+
+  private Change getChange(Commit c) throws OrmException {
     return Iterables.getOnlyElement(
-        db.changes().byKey(new Change.Key(c.getChangeId()))).currentPatchSetId();
+        db.changes().byKey(new Change.Key(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..e9e3ffb 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,24 +39,9 @@
 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
@@ -306,8 +288,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 +309,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..d58ac54 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;
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 98%
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..98beecf 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;
 
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/SiteLibraryLoaderUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
new file mode 100644
index 0000000..d5d4adc
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
@@ -0,0 +1,51 @@
+// 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 = 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);
+    }
+  }
+
+  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/auth/openid/OpenIdUrls.java b/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdUrls.java
index 706f465..79c17f4 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdUrls.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdUrls.java
@@ -18,7 +18,6 @@
   public static final String OPENID_IDENTIFIER = "openid_identifier";
   public static final String LASTID_COOKIE = "gerrit.last_openid";
 
+  public static final String URL_LAUNCHPAD = "https://login.launchpad.net/+openid";
   public static final String URL_YAHOO = "https://me.yahoo.com";
-  public static final String URL_GOOGLE =
-      "https://www.google.com/accounts/o8/id";
 }
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
index f09241d..6ec5d09 100644
--- 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
@@ -32,6 +32,7 @@
       final int owner) {
     List<ApprovalDetail> sorted = new ArrayList<>(ads);
     Collections.sort(sorted, new Comparator<ApprovalDetail>() {
+      @Override
       public int compare(ApprovalDetail o1, ApprovalDetail o2) {
         int byOwner = (o2.account.get() == owner ? 1 : 0)
             - (o1.account.get() == owner ? 1 : 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
index e067f06..1b50a47 100644
--- 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
@@ -29,6 +29,7 @@
   protected boolean canAbandon;
   protected boolean canEditCommitMessage;
   protected boolean canCherryPick;
+  protected boolean canEditHashtags;
   protected boolean canPublish;
   protected boolean canRebase;
   protected boolean canRestore;
@@ -44,6 +45,7 @@
   protected SubmitType submitType;
   protected SubmitTypeRecord submitTypeRecord;
   protected boolean canSubmit;
+  protected boolean mergeable;
   protected List<ChangeMessage> messages;
   protected PatchSet.Id currentPatchSetId;
   protected PatchSetDetail currentDetail;
@@ -93,6 +95,14 @@
     canCherryPick = a;
   }
 
+  public boolean getCanEditHashtags() {
+    return canEditHashtags;
+  }
+
+  public void setCanEditHashtags(final boolean a) {
+    canEditHashtags = a;
+  }
+
   public boolean canPublish() {
     return canPublish;
   }
@@ -265,4 +275,12 @@
   public boolean canEdit() {
     return canEdit;
   }
+
+  public void setMergeable(boolean m) {
+    mergeable = m;
+  }
+
+  public boolean isMergeable() {
+    return mergeable;
+  }
 }
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..a060245 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
@@ -57,6 +57,8 @@
   protected List<String> archiveFormats;
   protected int largeChangeSize;
   protected boolean newFeatures;
+  protected String replyLabel;
+  protected String replyTitle;
 
   public String getLoginUrl() {
     return loginUrl;
@@ -308,4 +310,20 @@
   public void setNewFeatures(boolean n) {
     newFeatures = n;
   }
+
+  public String getReplyTitle() {
+    return replyTitle;
+  }
+
+  public void setReplyTitle(String r) {
+    replyTitle = r;
+  }
+
+  public String getReplyLabel() {
+    return replyLabel;
+  }
+
+  public void setReplyLabel(String r) {
+    replyLabel = r;
+  }
 }
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..4e103b4 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
@@ -66,6 +66,7 @@
       return Collections.unmodifiableList(values);
     }
     Collections.sort(values, new Comparator<LabelValue>() {
+      @Override
       public int compare(LabelValue o1, LabelValue o2) {
         return o1.getValue() - o2.getValue();
       }
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/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/SubmitTypeRecord.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java
index 7b19f4c..187db05 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
@@ -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..aaa641c 100644
--- a/gerrit-extension-api/BUCK
+++ b/gerrit-extension-api/BUCK
@@ -5,9 +5,11 @@
   name = 'client',
   srcs = glob([
     SRC + 'api/projects/ProjectState.java',
+    SRC + 'common/ChangeStatus.java',
     SRC + 'common/InheritableBoolean.java',
     SRC + 'common/ListChangesOption.java',
     SRC + 'common/SubmitType.java',
+    SRC + 'common/Theme.java',
     SRC + 'webui/GerritTopMenu.java',
   ]),
   gwt_xml = SRC + 'Extensions.gwt.xml',
@@ -47,7 +49,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 0b74bc7..a0d9455 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-SNAPSHOT</version>
+  <version>2.11-SNAPSHOT</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/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..78c95b9 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
@@ -20,12 +20,37 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 
 import java.util.EnumSet;
+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,20 +59,49 @@
   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;
 
   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;
 
   /**
+   * 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;
+
+  /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
@@ -103,6 +157,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();
     }
@@ -126,5 +190,25 @@
     public ChangeInfo info() 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..4084946 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
@@ -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-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FixInput.java
similarity index 64%
copy from gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FixInput.java
index cd07320..e87be82 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/FixInput.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 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,16 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd;
+package com.google.gerrit.extensions.api.changes;
 
-public class GerritUiOptions {
-  private final boolean headless;
-
-  public GerritUiOptions(boolean headless) {
-    this.headless = headless;
-  }
-
-  public boolean enableDefaultUi() {
-    return !headless;
-  }
+public class FixInput {
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/HashtagsInput.java
similarity index 65%
copy from gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/HashtagsInput.java
index cd07320..bf84ccb0 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/HashtagsInput.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 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,16 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd;
+package com.google.gerrit.extensions.api.changes;
 
-public class GerritUiOptions {
-  private final boolean headless;
+import com.google.gerrit.extensions.restapi.DefaultInput;
 
-  public GerritUiOptions(boolean headless) {
-    this.headless = headless;
-  }
+import java.util.Set;
 
-  public boolean enableDefaultUi() {
-    return !headless;
-  }
+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/api/changes/RevisionApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 20bcea9..712c560 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,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.gerrit.extensions.common.MergeableInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
@@ -34,6 +35,9 @@
   void setReviewed(String path, boolean reviewed) throws RestApiException;
   Set<String> reviewed() throws RestApiException;
 
+  MergeableInfo mergeable() throws RestApiException;
+  MergeableInfo mergeableOtherBranches() throws RestApiException;
+
   /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
@@ -88,5 +92,15 @@
     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();
+    }
   }
 }
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..daa5de7 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
@@ -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/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/AvatarInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AvatarInfo.java
new file mode 100644
index 0000000..793aa24
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AvatarInfo.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.extensions.common;
+
+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..ee2d3e6 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
@@ -16,6 +16,7 @@
 
 import java.sql.Timestamp;
 import java.util.Collection;
+import java.util.List;
 import java.util.Map;
 
 public class ChangeInfo {
@@ -23,6 +24,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 +35,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/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..e58ffc5
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java
@@ -0,0 +1,74 @@
+// 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;
+
+  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-pgm/src/main/java/com/google/gerrit/pgm/init/InitStep.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/EditInfo.java
similarity index 63%
copy from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitStep.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/EditInfo.java
index 5a9a334..5b2fd78 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitStep.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/EditInfo.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,12 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.pgm.init;
+package com.google.gerrit.extensions.common;
 
-/** A single step in the site initialization process. */
-public interface InitStep {
-  public void run() throws Exception;
+import java.util.Map;
 
-  /** Executed after the site has been initialized */
-  public void postRun() throws Exception;
+public class EditInfo {
+  public CommitInfo commit;
+  public String baseRevision;
+  public Map<String, ActionInfo> actions;
+  public Map<String, FetchInfo> fetch;
+  public Map<String, FileInfo> files;
 }
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..76dd93dd 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/ListChangesOption.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ListChangesOption.java
index f9f8b62..dd3f075 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/common/ListChangesOption.java
@@ -52,7 +52,10 @@
   DOWNLOAD_COMMANDS(13),
 
   /** Include patch set weblinks. */
-  WEB_LINKS(14);
+  WEB_LINKS(14),
+
+  /** Include consistency check results. */
+  CHECK(15);
 
   private final int value;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeableInfo.java
similarity index 64%
copy from gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeableInfo.java
index cd07320..268230f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeableInfo.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 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,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.extensions.common;
 
-public class GerritUiOptions {
-  private final boolean headless;
+import java.util.List;
 
-  public GerritUiOptions(boolean headless) {
-    this.headless = headless;
-  }
-
-  public boolean enableDefaultUi() {
-    return !headless;
-  }
+public class MergeableInfo {
+  public SubmitType submitType;
+  public boolean mergeable;
+  public List<String> mergeableInto;
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
similarity index 65%
copy from gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
index cd07320..369bcda 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 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,16 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd;
+package com.google.gerrit.extensions.common;
 
-public class GerritUiOptions {
-  private final boolean headless;
-
-  public GerritUiOptions(boolean headless) {
-    this.headless = headless;
+public class ProblemInfo {
+  public static enum Status {
+    FIXED, FIX_FAILED;
   }
 
-  public boolean enableDefaultUi() {
-    return !headless;
-  }
+  public String message;
+  public Status status;
+  public String outcome;
 }
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..cbb967c 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,6 @@
 
 package com.google.gerrit.extensions.common;
 
-import java.util.List;
 import java.util.Map;
 
 public class RevisionInfo {
@@ -22,9 +21,9 @@
   public Boolean draft;
   public Boolean hasDraftComments;
   public int _number;
+  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/TagInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/TagInfo.java
new file mode 100644
index 0000000..3e3d8db
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/TagInfo.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;
+
+public class TagInfo {
+  public String ref;
+  public String revision;
+  public String object;
+  public String message;
+  public GitPerson tagger;
+
+  public TagInfo(String ref, String revision) {
+    this.ref = ref;
+    this.revision = revision;
+  }
+
+  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/Theme.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/Theme.java
new file mode 100644
index 0000000..436f911
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/Theme.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.common;
+
+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/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 9ef7d1b..c03e846 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
@@ -194,19 +194,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..e264b31 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
@@ -145,6 +145,7 @@
   public abstract void writeTo(OutputStream os) throws IOException;
 
   /** Close the result and release any resources it holds. */
+  @Override
   public void close() throws IOException {
   }
 
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/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..afe0e82 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);
@@ -52,6 +59,7 @@
   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> {
@@ -100,6 +108,7 @@
       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..c97bddd 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 getPathSetWebLink(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/WebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java
index 19d9ab7..998638c 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 final static String BLANK = "_blank";
+    /**
+     * Opens the link in the frame it was clicked.
+     */
+    public final static String SELF = "_self";
+    /**
+     * Opens link in parent frame.
+     */
+    public final static String PARENT = "_parent";
+    /**
+     * Opens link in the full body of the window.
+     */
+    public final static 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/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/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..b1312db
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/Resources.java
@@ -0,0 +1,110 @@
+// 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("removeReviewer.png")
+  public ImageResource removeReviewer();
+
+  @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-common/src/main/resources/com/google/gerrit/client/deleteHover.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/deleteHover.png
new file mode 100644
index 0000000..9fde3fa
--- /dev/null
+++ b/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/src/main/java/com/google/gerrit/client/change/remove_reviewer.png b/gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/removeReviewer.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/change/remove_reviewer.png
rename to gerrit-gwtui-common/src/main/resources/com/google/gerrit/client/removeReviewer.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..d50c6e7 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',
@@ -59,16 +59,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 +92,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 +100,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/DiffWebLinkInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffWebLinkInfo.java
new file mode 100644
index 0000000..bcf4256
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/DiffWebLinkInfo.java
@@ -0,0 +1,26 @@
+// 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;
+
+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..a9cdbb3 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
@@ -66,6 +66,7 @@
 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.FileTable;
 import com.google.gerrit.client.changes.AccountDashboardScreen;
 import com.google.gerrit.client.changes.ChangeScreen;
 import com.google.gerrit.client.changes.CustomDashboardScreen;
@@ -78,6 +79,7 @@
 import com.google.gerrit.client.diff.DisplaySide;
 import com.google.gerrit.client.diff.SideBySide2;
 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;
@@ -148,7 +150,7 @@
     if (diffBase != null) {
       p.append(diffBase.get()).append("..");
     }
-    p.append(revision.get()).append("/").append(KeyUtil.encode(fileName));
+    p.append(revision.getId()).append("/").append(KeyUtil.encode(fileName));
     if (type != null && !type.isEmpty()) {
       p.append(",").append(type);
     }
@@ -168,6 +170,15 @@
     }
   }
 
+  public static String toEditScreen(PatchSet.Id revision, String fileName) {
+    Change.Id c = revision.getParentKey();
+    StringBuilder p = new StringBuilder();
+    p.append("/c/").append(c).append("/");
+    p.append(revision.getId()).append("/").append(KeyUtil.encode(fileName));
+    p.append(",edit");
+    return p.toString();
+  }
+
   public static String toPublish(PatchSet.Id ps) {
     Change.Id c = ps.getParentKey();
     return "/c/" + c + "/" + ps.get() + ",publish";
@@ -239,7 +250,7 @@
       if (defaultScreenToken != null && !MINE.equals(defaultScreenToken)) {
         select(defaultScreenToken);
       } else {
-        Gerrit.display(token, mine(token));
+        Gerrit.display(token, mine());
       }
 
     } else if (matchPrefix("/dashboard/", token)) {
@@ -432,7 +443,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());
 
@@ -535,9 +546,15 @@
     }
 
     if (rest.isEmpty()) {
-      Gerrit.display(token, panel== null
+      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
           ? (isChangeScreen2()
-              ? new ChangeScreen2(id, null, null, false)
+              ? new ChangeScreen2(id, null, null, false, mode)
               : new ChangeScreen(id))
           : new NotFoundScreen());
       return;
@@ -553,16 +570,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;
@@ -587,7 +602,7 @@
                 base != null
                     ? String.valueOf(base.get())
                     : null,
-                String.valueOf(ps.get()), false)
+                String.valueOf(ps.get()), false, FileTable.Mode.REVIEW)
             : new ChangeScreen(id));
       } else if ("publish".equals(panel)) {
         publish(ps);
@@ -597,6 +612,12 @@
     }
   }
 
+  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()) {
@@ -633,6 +654,7 @@
   private static void publish(final PatchSet.Id ps) {
     String token = toPublish(ps);
     new AsyncSplit(token) {
+      @Override
       public void onSuccess() {
         Gerrit.display(token, select());
       }
@@ -650,88 +672,102 @@
         patchSetDetail, patchTable, topView, null);
   }
 
-  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;
+  public static void patch(String token, PatchSet.Id baseId,
+      Patch.Key id, DisplaySide side, int line,
+      int patchIndex, PatchSetDetail patchSetDetail,
+      PatchTable patchTable, PatchScreen.TopView topView,
+      String panelType) {
+    if (id == null) {
+      Gerrit.display(token, new NotFoundScreen());
+      return;
+    }
 
-    GWT.runAsync(new AsyncSplit(token) {
-      public void onSuccess() {
-        Gerrit.display(token, select());
-      }
+    String panel = panelType;
+    if (panel == null) {
+      int c = token.lastIndexOf(',');
+      panel = 0 <= c ? token.substring(c + 1) : "";
+    }
 
-      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);//
-          }
+    if ("".equals(panel)) {
+      if (isChangeScreen2()) {
+        if (Gerrit.isSignedIn()
+            && DiffView.UNIFIED_DIFF.equals(Gerrit.getUserAccount()
+                .getGeneralPreferences().getDiffView())) {
+          sbs1(token, baseId, id, patchIndex, patchSetDetail, patchTable,
+              topView, PatchScreen.Type.UNIFIED);
+          return;
         }
+        sbs2(token, baseId, id, side, line, false);
+        return;
+      }
+      sbs1(token, baseId, id, patchIndex, patchSetDetail, patchTable, topView,
+          PatchScreen.Type.SIDE_BY_SIDE);
+      return;
+    } else if ("unified".equals(panel)) {
+      sbs1(token, baseId, id, patchIndex, patchSetDetail, patchTable, topView,
+          PatchScreen.Type.UNIFIED);
+      return;
+    } else if ("cm".equals(panel) && Gerrit.getConfig().getNewFeatures()) {
+      if (Gerrit.isSignedIn()
+          && DiffView.UNIFIED_DIFF.equals(Gerrit.getUserAccount()
+              .getGeneralPreferences().getDiffView())) {
+        sbs1(token, baseId, id, patchIndex, patchSetDetail, patchTable,
+            topView, PatchScreen.Type.UNIFIED);
+        return;
+      }
+      sbs2(token, baseId, id, side, line, false);
+      return;
+    } else if ("".equals(panel) || "sidebyside".equals(panel)) {
+      sbs1(token, baseId, id, patchIndex, patchSetDetail, patchTable, topView,
+          PatchScreen.Type.SIDE_BY_SIDE);
+      return;
+    } else if (panel.equals("edit")) {
+      sbs2(token, null, id, null, 0, true);
+      return;
+    }
+    Gerrit.display(token, new NotFoundScreen());
+  }
 
-        return new NotFoundScreen();
+  private static void sbs1(final String token, final PatchSet.Id baseId,
+      final Patch.Key id, final int patchIndex,
+      final PatchSetDetail patchSetDetail, final PatchTable patchTable,
+      final PatchScreen.TopView topView, final PatchScreen.Type type) {
+    GWT.runAsync(new AsyncSplit(token) {
+      @Override
+      public void onSuccess() {
+        PatchScreen.TopView top =  topView == null
+            ? Gerrit.getPatchScreenTopView()
+            : topView;
+        switch (type) {
+          case SIDE_BY_SIDE:
+            Gerrit.display(token, new PatchScreen.SideBySide(id, patchIndex,
+                patchSetDetail, patchTable, top, baseId));
+            break;
+          case UNIFIED:
+            Gerrit.display(token, new PatchScreen.Unified(id, patchIndex,
+                patchSetDetail, patchTable, top, baseId));
+            break;
+        }
+      }
+    });
+  }
+
+  private static void sbs2(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(id)
+            : new SideBySide2(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());
       }
@@ -799,6 +835,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)) {
@@ -972,6 +1009,7 @@
       this.token = token;
     }
 
+    @Override
     public final void onFailure(Throwable reason) {
       if (!isReloadUi
           && "HTTP download failed with status 404".equals(reason.getMessage())) {
@@ -988,6 +1026,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 d81827c..1f33551 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
@@ -51,6 +51,7 @@
 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 +103,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 +112,7 @@
   private static String defaultScreenToken;
   private static AccountDiffPreference myAccountDiffPref;
   private static String xGerritAuth;
+  private static boolean isNoteDbEnabled;
 
   private static Map<String, LinkMenuBar> menuBars;
 
@@ -330,6 +333,10 @@
     Location.assign(loginRedirect(token));
   }
 
+  public static boolean isNoteDbEnabled() {
+    return isNoteDbEnabled;
+  }
+
   public static String loginRedirect(String token) {
     if (token == null) {
       token = "";
@@ -427,6 +434,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 +472,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()));
   }
@@ -642,6 +653,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 +715,7 @@
 
         case OPENID:
           menuRight.addItem(C.menuRegister(), new Command() {
+            @Override
             public void execute() {
               String t = History.getToken();
               if (t == null) {
@@ -712,6 +725,7 @@
             }
           });
           menuRight.addItem(C.menuSignIn(), new Command() {
+            @Override
             public void execute() {
               doSignIn(History.getToken());
             }
@@ -720,6 +734,7 @@
 
         case OPENID_SSO:
           menuRight.addItem(C.menuSignIn(), new Command() {
+            @Override
             public void execute() {
               doSignIn(History.getToken());
             }
@@ -742,6 +757,7 @@
             menuRight.add(anchor(registerText, cfg.getRegisterUrl()));
           }
           menuRight.addItem(C.menuSignIn(), new Command() {
+            @Override
             public void execute() {
               doSignIn(History.getToken());
             }
@@ -754,17 +770,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);
           }
         }
       }
@@ -883,6 +902,47 @@
       });
   }
 
+  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() {
+          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 addDiffLink(final LinkMenuBar m, final String text,
       final PatchScreen.Type type) {
     m.addItem(new LinkMenuItem(text, "") {
@@ -907,6 +967,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..64025db 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();
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..a335d6b 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
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..6891b68 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
@@ -161,6 +161,7 @@
   String link();
   String linkMenuBar();
   String linkMenuItemNotLast();
+  String linkPanel();
   String maxObjectSizeLimitEffectiveLabel();
   String menuBarUserName();
   String menuBarUserNameAvatar();
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/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/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..5aa2119 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.common.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 a84ca99..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);
@@ -243,9 +245,12 @@
         //
         return "";
 
-      } else if (k.isScheme(OpenIdUrls.URL_GOOGLE)) {
+      } else if (k.isScheme("https://www.google.com/accounts/o8/id")) {
         return OpenIdUtil.C.nameGoogle();
 
+      } else if (k.isScheme(OpenIdUrls.URL_LAUNCHPAD)) {
+        return OpenIdUtil.C.nameLaunchpad();
+
       } else if (k.isScheme(OpenIdUrls.URL_YAHOO)) {
         return OpenIdUtil.C.nameYahoo();
 
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..6fb2fe0 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
@@ -409,7 +409,7 @@
     }
 
     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);
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..9adcaf7 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
@@ -18,11 +18,11 @@
 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;
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..20ff993 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,11 @@
   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();
 }
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..26b8123 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,8 @@
 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.
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..c0689cb
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.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.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.user.client.ui.Button;
+
+class CreateChangeAction {
+  static void call(Button b, final String project) {
+    // TODO Replace CreateChangeDialog with a nicer looking display.
+    b.setEnabled(false);
+    new CreateChangeDialog(b, 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);
+            }
+        });
+      }
+    }.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/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/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..66d9d67 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
@@ -20,6 +20,7 @@
 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;
@@ -239,9 +240,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,6 +316,7 @@
     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);
@@ -396,6 +396,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 +421,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);
     }
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..9aae30c 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
@@ -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,30 @@
     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());
+    }
+  }
+
+  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 void doSave() {
@@ -569,7 +601,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/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..7bcac5f 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;
@@ -137,6 +138,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..490edee 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: {},
@@ -65,12 +67,14 @@
       refresh: @com.google.gerrit.client.api.ApiGlue::refresh(),
       refreshMenuBar: @com.google.gerrit.client.api.ApiGlue::refreshMenuBar(),
       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,6 +196,10 @@
     Gerrit.display(History.getToken());
   }
 
+  private static final AccountInfo getCurrentUser() {
+    return Gerrit.getUserAccountInfo();
+  }
+
   private static final void refreshMenuBar() {
     Gerrit.refreshMenuBar();
   }
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..8312cc3 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,6 +50,7 @@
     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(),
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/auth/openid/OpenIdConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.java
index 5d756a1..3bfb297 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.java
@@ -18,5 +18,6 @@
 
 public interface OpenIdConstants extends Constants {
   String nameGoogle();
+  String nameLaunchpad();
   String nameYahoo();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.properties
index 1a8acc1..08ddf38 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.properties
@@ -1,2 +1,3 @@
 nameGoogle = Google Account
+nameLaunchpad = Launchpad ID
 nameYahoo = Yahoo! ID
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..b5090ca 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
@@ -19,6 +19,7 @@
 import com.google.gerrit.client.actions.ActionInfo;
 import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.changes.ChangeInfo.EditInfo;
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.reviewdb.client.Change;
@@ -38,7 +39,7 @@
   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);
@@ -46,6 +47,9 @@
   @UiField Button cherrypick;
   @UiField Button deleteChange;
   @UiField Button deleteRevision;
+  @UiField Button deleteEdit;
+  @UiField Button publishEdit;
+  @UiField Button rebaseEdit;
   @UiField Button publish;
   @UiField Button rebase;
   @UiField Button revert;
@@ -57,12 +61,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,10 +89,13 @@
     project = info.project();
     subject = commit.subject();
     message = commit.message();
+    branch = info.branch();
+    key = info.change_id();
     changeInfo = info;
 
     initChangeActions(info, hasUser);
     initRevisionActions(info, revInfo, hasUser);
+    initEditActions(info, info.edit(), hasUser);
   }
 
   private void initChangeActions(ChangeInfo info, boolean hasUser) {
@@ -97,12 +109,33 @@
       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)));
       }
     }
   }
 
+  private void initEditActions(ChangeInfo info, EditInfo editInfo,
+      boolean hasUser) {
+    if (!info.has_edit() || !info.current_revision().equals(editInfo.name())) {
+      return;
+    }
+    NativeMap<ActionInfo> actions = editInfo.has_actions()
+        ? editInfo.actions()
+        : NativeMap.<ActionInfo> create();
+    actions.copyKeysIntoChildren("id");
+
+    if (hasUser) {
+      a2b(actions, "/", deleteEdit);
+      a2b(actions, "publish", publishEdit);
+      a2b(actions, "rebase", rebaseEdit);
+      for (String id : filterNonCore(actions)) {
+        add(new ActionButton(info, editInfo, actions.get(id)));
+      }
+    }
+  }
+
   private void initRevisionActions(ChangeInfo info, RevisionInfo revInfo,
       boolean hasUser) {
     NativeMap<ActionInfo> actions = revInfo.has_actions()
@@ -151,8 +184,17 @@
     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);
     }
@@ -160,22 +202,37 @@
   }
 
   @UiHandler("publish")
-  void onPublish(ClickEvent e) {
+  void onPublish(@SuppressWarnings("unused") ClickEvent e) {
     DraftActions.publish(changeId, revision);
   }
 
+  @UiHandler("deleteEdit")
+  void onDeleteEdit(@SuppressWarnings("unused") ClickEvent e) {
+    EditActions.deleteEdit(changeId);
+  }
+
+  @UiHandler("publishEdit")
+  void onPublishEdit(@SuppressWarnings("unused") ClickEvent e) {
+    EditActions.publishEdit(changeId);
+  }
+
+  @UiHandler("rebaseEdit")
+  void onRebaseEdit(@SuppressWarnings("unused") ClickEvent e) {
+    EditActions.rebaseEdit(changeId);
+  }
+
   @UiHandler("deleteRevision")
-  void onDeleteRevision(ClickEvent e) {
+  void onDeleteRevision(@SuppressWarnings("unused") ClickEvent e) {
     DraftActions.delete(changeId, revision);
   }
 
   @UiHandler("deleteChange")
-  void onDeleteChange(ClickEvent e) {
+  void onDeleteChange(@SuppressWarnings("unused") 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,23 +240,23 @@
   }
 
   @UiHandler("rebase")
-  void onRebase(ClickEvent e) {
+  void onRebase(@SuppressWarnings("unused") ClickEvent e) {
     RebaseAction.call(changeId, revision);
   }
 
   @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) {
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..496e048 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,37 +27,27 @@
 
     #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;}
 
@@ -92,6 +84,15 @@
     <g:Button ui:field='publish' styleName='' visible='false'>
       <div><ui:msg>Publish</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='publishEdit' styleName='' visible='false'>
+      <div><ui:msg>Publish Edit</ui:msg></div>
+    </g:Button>
+    <g:Button ui:field='rebaseEdit' styleName='' visible='false'>
+      <div><ui:msg>Rebase Edit</ui:msg></div>
+    </g:Button>
 
     <g:Button ui:field='abandon' styleName='{style.red}' visible='false'>
       <div><ui:msg>Abandon</ui:msg></div>
@@ -99,6 +100,9 @@
     <g:Button ui:field='restore' styleName='{style.red}' 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/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeConstants.java
index 11a1824..a7d4945 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,6 @@
   String sameTopicTooltip();
   String noChanges();
   String indirectAncestor();
+  String merged();
+  String abandoned();
 }
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..8852d4e 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,5 @@
 sameTopicTooltip = Changes with the same topic
 noChanges = No Changes
 indirectAncestor = Indirect ancestor
+merged = Merged
+abandoned = Abandoned
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/ChangeScreen2.java
index 69cd3fc..5d6fa38 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
@@ -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;
@@ -86,8 +88,11 @@
 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;
@@ -107,6 +112,7 @@
     String label_need();
     String replyBox();
     String selected();
+    String hashtagName();
   }
 
   static ChangeScreen2 get(NativeEvent in) {
@@ -125,6 +131,7 @@
   private String revision;
   private ChangeInfo changeInfo;
   private CommentLinkProcessor commentLinkProcessor;
+  private EditInfo edit;
 
   private KeyCommandSet keysNavigation;
   private KeyCommandSet keysAction;
@@ -134,6 +141,7 @@
   private UpdateAvailableBar updateAvailable;
   private boolean openReplyBox;
   private boolean loaded;
+  private FileTable.Mode fileTableMode;
 
   @UiField HTMLPanel headerLine;
   @UiField Style style;
@@ -142,6 +150,8 @@
 
   @UiField Element ccText;
   @UiField Reviewers reviewers;
+  @UiField Hashtags hashtags;
+  @UiField Element hashtagTableRow;
   @UiField FlowPanel ownerPanel;
   @UiField InlineHyperlink ownerLink;
   @UiField Element statusText;
@@ -170,6 +180,9 @@
   @UiField Button download;
   @UiField Button reply;
   @UiField Button openAll;
+  @UiField Button editMode;
+  @UiField Button reviewMode;
+  @UiField Button addFile;
   @UiField Button expandAll;
   @UiField Button collapseAll;
   @UiField Button editMessage;
@@ -180,12 +193,15 @@
   private IncludedInAction includedInAction;
   private PatchSetsAction patchSetsAction;
   private DownloadAction downloadAction;
+  private EditFileAction editFileAction;
 
-  public ChangeScreen2(Change.Id changeId, String base, String revision, boolean openReplyBox) {
+  public ChangeScreen2(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 +212,31 @@
   @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 GerritCallback<EditInfo>() {
+            @Override
+            public void onSuccess(EditInfo result) {
+              edit = result;
+            }
+          }));
+    }
+    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 +265,11 @@
     setHeaderVisible(false);
     Resources.I.style().ensureInjected();
     star.setVisible(Gerrit.isSignedIn());
-    labels.init(style, statusText);
+    labels.init(style);
     reviewers.init(style, ccText);
+    hashtags.init(style);
+
+    initReplyButton();
 
     keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
     keysNavigation.add(new KeyCommand(0, 'u', Util.C.upToChangeList()) {
@@ -306,6 +335,14 @@
     }
   }
 
+  private void initReplyButton() {
+    reply.setTitle(Gerrit.getConfig().getReplyTitle());
+    reply.setHTML(new SafeHtmlBuilder()
+      .openDiv()
+      .append(Gerrit.getConfig().getReplyLabel())
+      .closeDiv());
+  }
+
   private void gotoSibling(final int offset) {
     if (offset > 0 && changeInfo.current_revision().equals(revision)) {
       return;
@@ -350,7 +387,15 @@
       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(
@@ -392,14 +437,44 @@
                 null)));
   }
 
+  private void initEditMode(ChangeInfo info) {
+    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());
+        reviewMode.setVisible(!editMode.isVisible());
+        editFileAction = new EditFileAction(
+            new PatchSet.Id(changeId, edit == null ? rev._number() : 0),
+            "", "", style, editMessage, reply);
+      } else {
+        editMode.setVisible(false);
+        addFile.setVisible(false);
+        reviewMode.setVisible(false);
+      }
+    }
+  }
+
+  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());
+  }
+
   private void initEditMessageAction(ChangeInfo info, String revision) {
-    NativeMap<ActionInfo> actions = info.revision(revision).actions();
-    if (actions != null && actions.containsKey("message")) {
+    RevisionInfo revisionInfo = info.revision(revision);
+    NativeMap<ActionInfo> actions = revisionInfo.actions();
+    if ((actions != null && actions.containsKey("message"))
+        || revisionInfo.is_edit()) {
       editMessage.setVisible(true);
       editMessageAction = new EditMessageAction(
           info.legacy_id(),
-          revision,
-          info.revision(revision).commit().message(),
+          revisionInfo.commit().message(),
           style,
           editMessage,
           reply);
@@ -438,6 +513,7 @@
     }
 
     ChangeGlue.fireShowChange(changeInfo, changeInfo.revision(revision));
+    CodeMirror.preload();
     startPoller();
     if (NewChangeScreenBar.show()) {
       add(new NewChangeScreenBar(changeId));
@@ -447,7 +523,12 @@
   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 +558,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();
   }
 
@@ -511,17 +592,48 @@
   }
 
   @UiHandler("editMessage")
-  void onEditMessage(ClickEvent e) {
+  void onEditMessage(@SuppressWarnings("unused") 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);
+    reviewMode.setVisible(true);
+  }
+
+  @UiHandler("reviewMode")
+  void onReviewMode(@SuppressWarnings("unused") ClickEvent e) {
+    fileTableMode = FileTable.Mode.REVIEW;
+    refreshFileTable();
+    editMode.setVisible(true);
+    addFile.setVisible(false);
+    reviewMode.setVisible(false);
+  }
+
+  @UiHandler("addFile")
+  void onAddFile(@SuppressWarnings("unused") ClickEvent e) {
+    editFileAction.onEdit();
+  }
+
+  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 +643,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 +653,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 +663,55 @@
 
   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);
+    if (rev.is_edit()) {
+      NativeMap<JsArray<CommentInfo>> emptyComment = NativeMap.create();
+      files.set(
+          b != null ? new PatchSet.Id(changeId, b._number()) : null,
+          new PatchSet.Id(changeId, rev._number()),
+          style, editMessage, reply, fileTableMode, edit != null);
+      files.setValue(info.edit().files(), myLastReply(info), emptyComment,
+          emptyComment);
+    } else {
+      loadDiff(b, rev, myLastReply(info), group);
+    }
     loadCommit(rev, group);
 
     if (loaded) {
@@ -600,9 +756,10 @@
       group.add(new AsyncCallback<NativeMap<FileInfo>>() {
         @Override
         public void onSuccess(NativeMap<FileInfo> m) {
-          files.setRevisions(
+          files.set(
               base != null ? new PatchSet.Id(changeId, base._number()) : null,
-              new PatchSet.Id(changeId, rev._number()));
+              new PatchSet.Id(changeId, rev._number()),
+              style, editMessage, reply, fileTableMode, edit != null);
           files.setValue(m, myLastReply, comments.get(0), drafts.get(0));
         }
 
@@ -611,7 +768,7 @@
         }
       }));
 
-    if (Gerrit.isSignedIn()) {
+    if (Gerrit.isSignedIn() && fileTableMode == FileTable.Mode.REVIEW) {
       ChangeApi.revision(changeId.get(), rev.name())
         .view("files")
         .addParameterTrue("reviewed")
@@ -671,18 +828,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,19 +929,48 @@
     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);
@@ -791,6 +980,7 @@
     initDownloadAction(info, revision);
     initProjectLinks(info);
     initBranchLink(info);
+    initEditMode(info);
     actions.display(info, revision);
 
     star.setValue(info.starred());
@@ -800,6 +990,11 @@
     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);
@@ -818,7 +1013,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 +1077,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 +1119,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/ChangeScreen2.ui.xml
index 2b09ef9..85c93a1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml
@@ -26,7 +26,9 @@
     @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;
@@ -61,7 +63,6 @@
       top: 0;
       right: 3px;
       height: HEADER_HEIGHT;
-      line-height: HEADER_HEIGHT;
     }
 
     .infoLine {
@@ -75,8 +76,6 @@
     .infoLineHeaderButtons {
       display: inline-block;
       height: HEADER_HEIGHT;
-      line-height: HEADER_HEIGHT;
-      vertical-align: top;
     }
     .statusRight {
       position: absolute;
@@ -119,8 +118,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,30 +233,47 @@
     .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 {
       background-color: #4d90fe;
-      background-image: -webkit-linear-gradient(top, #4d90fe, #4d90fe);
     }
     button.quickApprove div { color: #fff; }
 
@@ -268,7 +282,8 @@
       background-color: trimColor;
       font-weight: bold;
       color: textColor;
-      height: 18px;
+      height: 20px;
+      line-height: 20px;
       margin: 0 -5px;
       padding: 5px 5px;
     }
@@ -295,8 +310,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 {
@@ -336,9 +355,8 @@
         <div class='{style.headerButtons} {style.infoLineHeaderButtons}'>
           <g:Button ui:field='reply'
               styleName=''
-              title='Reply and score (Shortcut: a)'>
+              title=''>
             <ui:attribute name='title'/>
-            <div><ui:msg>Reply&#8230;</ui:msg></div>
           </g:Button>
           <c:QuickApprove ui:field='quickApprove'
               styleName='{style.quickApprove}'
@@ -438,6 +456,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 +473,15 @@
       </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>
       <div class='{style.headerButtons}'>
         <g:Button ui:field='openAll'
             styleName=''
@@ -462,6 +492,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/CommitBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
index 4572cf8..c412587d 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
@@ -30,15 +30,12 @@
 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.event.dom.client.ClickEvent;
 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.Anchor;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Composite;
@@ -66,7 +63,7 @@
   @UiField FlowPanel committerPanel;
   @UiField Image mergeCommit;
   @UiField CopyableLabel commitName;
-  @UiField TableCellElement webLinkCell;
+  @UiField FlowPanel webLinkPanel;
   @UiField Element parents;
   @UiField FlowPanel parentCommits;
   @UiField FlowPanel parentWebLinks;
@@ -90,7 +87,7 @@
   }
 
   @UiHandler("more")
-  void onMore(ClickEvent e) {
+  void onMore(@SuppressWarnings("unused") ClickEvent e) {
     if (expanded) {
       removeStyleName(style.expanded());
       addStyleName(style.collapsed());
@@ -129,28 +126,30 @@
       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);
     for (CommitInfo c : Natives.asList(commits)) {
       CopyableLabel copyLabel = new CopyableLabel(c.commit());
+      copyLabel.setTitle(c.subject());
       copyLabel.setStyleName(style.clippy());
       parentCommits.add(copyLabel);
 
@@ -161,6 +160,12 @@
         a.setStyleName(style.parentWebLink());
         parentWebLinks.add(a);
       }
+      JsArray<WebLinkInfo> links = c.web_links();
+      if (links != null) {
+        for (WebLinkInfo link : Natives.asList(links)) {
+          parentWebLinks.add(link.toAnchor());
+        }
+      }
     }
   }
 
@@ -186,14 +191,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..90632c8 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;
 
@@ -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 {
@@ -152,7 +158,9 @@
           </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>
@@ -160,7 +168,7 @@
           <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/DownloadAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java
index 891cc10..5b273d3 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
@@ -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..d7a44ed 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,6 +18,7 @@
 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;
@@ -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..8654a62 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
@@ -40,10 +40,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 +59,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..c93c4e3
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java
@@ -0,0 +1,58 @@
+// 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.SubmitFailureDialog;
+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/EditFileAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileAction.java
new file mode 100644
index 0000000..951c84d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileAction.java
@@ -0,0 +1,80 @@
+//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.PatchSet;
+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 EditFileAction {
+ private final PatchSet.Id id;
+ private final String content;
+ private final String file;
+ private final ChangeScreen2.Style style;
+ private final Widget editMessageButton;
+ private final Widget relativeTo;
+
+ private EditFileBox editBox;
+ private PopupPanel popup;
+
+ EditFileAction(
+     PatchSet.Id id,
+     String content,
+     String file,
+     ChangeScreen2.Style style,
+     Widget editButton,
+     Widget relativeTo) {
+   this.id = id;
+   this.content = content;
+   this.file = file;
+   this.style = style;
+   this.editMessageButton = editButton;
+   this.relativeTo = relativeTo;
+ }
+
+ public void onEdit() {
+   if (popup != null) {
+     popup.hide();
+     return;
+   }
+
+   if (editBox == null) {
+     editBox = new EditFileBox(
+         id,
+         content,
+         file);
+   }
+
+   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(relativeTo);
+   GlobalKey.dialog(p);
+   popup = p;
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileBox.java
new file mode 100644
index 0000000..67f9265
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileBox.java
@@ -0,0 +1,119 @@
+//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.VoidResult;
+import com.google.gerrit.client.changes.ChangeFileApi;
+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.PatchSet;
+import com.google.gwt.core.client.GWT;
+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.TextBoxBase;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+
+class EditFileBox extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, EditFileBox> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  private final PatchSet.Id id;
+  private final String fileName;
+  private final String fileContent;
+
+  @UiField FileTextBox file;
+  @UiField NpTextArea content;
+  @UiField Button save;
+  @UiField Button cancel;
+
+  EditFileBox(
+      PatchSet.Id id,
+      String fileC,
+      String fileName) {
+    this.id = id;
+    this.fileName = fileName;
+    this.fileContent = fileC;
+    initWidget(uiBinder.createAndBindUi(this));
+    new EditFileBoxListener(content);
+    new EditFileBoxListener(file);
+  }
+
+  @Override
+  protected void onLoad() {
+    file.set(id, content);
+    file.setText(fileName);
+    file.setEnabled(fileName.isEmpty());
+    content.setText(fileContent);
+    save.setEnabled(false);
+    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+      @Override
+      public void execute() {
+        if (fileName.isEmpty()) {
+          file.setFocus(true);
+        } else {
+          content.setFocus(true);
+        }
+      }});
+  }
+
+  @UiHandler("save")
+  void onSave(@SuppressWarnings("unused") ClickEvent e) {
+    ChangeFileApi.putContent(id, file.getText(), content.getText(),
+        new GerritCallback<VoidResult>() {
+          @Override
+          public void onSuccess(VoidResult result) {
+            Gerrit.display(PageLinks.toChangeInEditMode(id.getParentKey()));
+            hide();
+          }
+        });
+  }
+
+  @UiHandler("cancel")
+  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
+    hide();
+  }
+
+  protected void hide() {
+    for (Widget w = getParent(); w != null; w = w.getParent()) {
+      if (w instanceof PopupPanel) {
+        ((PopupPanel) w).hide();
+        break;
+      }
+    }
+  }
+
+  private class EditFileBoxListener extends TextBoxChangeListener {
+    public EditFileBoxListener(TextBoxBase base) {
+      super(base);
+    }
+
+    @Override
+    public void onTextChanged(String newText) {
+      save.setEnabled(!file.getText().trim().isEmpty()
+          && !newText.trim().equals(fileContent));
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileBox.ui.xml
new file mode 100644
index 0000000..3a0e0be
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileBox.ui.xml
@@ -0,0 +1,61 @@
+<?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:f='urn:import:com.google.gerrit.client.change'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
+  <ui:style>
+    .fileContent {
+      background-color: white;
+      font-family: monospace;
+    }
+    .cancel { float: right; }
+  </ui:style>
+  <g:HTMLPanel>
+    <div class='{res.style.section}'>
+      <div>
+         <ui:msg>Path:</ui:msg>
+      </div>
+      <div>
+        <f:FileTextBox ui:field='file' visibleLength='79'/>
+      </div>
+      <div>
+        <ui:msg>Content:</ui:msg>
+      </div>
+      <c:NpTextArea
+         visibleLines='30'
+         characterWidth='78'
+         styleName='{style.fileContent}'
+         ui:field='content'/>
+    </div>
+    <div class='{res.style.section}'>
+      <g:Button ui:field='save'
+          title='Create new revision edit'
+          styleName='{res.style.button}'>
+        <ui:attribute name='title'/>
+        <div><ui:msg>Save</ui:msg></div>
+      </g:Button>
+      <g:Button ui:field='cancel'
+          styleName='{res.style.button}'
+          addStyleNames='{style.cancel}'>
+          <div>Cancel</div>
+      </g:Button>
+    </div>
+  </g:HTMLPanel>
+</ui:UiBinder>
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
index a0f7c9d..61a6805 100644
--- 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
@@ -24,7 +24,6 @@
 
 class EditMessageAction {
   private final Change.Id changeId;
-  private final String revision;
   private final String originalMessage;
   private final ChangeScreen2.Style style;
   private final Widget editMessageButton;
@@ -35,13 +34,11 @@
 
   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;
@@ -57,7 +54,6 @@
     if (editBox == null) {
       editBox = new EditMessageBox(
           changeId,
-          revision,
           originalMessage);
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java
index 0fe91ca..74600ab 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java
@@ -15,13 +15,13 @@
 package com.google.gerrit.client.change;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.changes.ChangeFileApi;
 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;
@@ -40,7 +40,6 @@
   private static final Binder uiBinder = GWT.create(Binder.class);
 
   private final Change.Id changeId;
-  private final String revision;
   private String originalMessage;
 
   @UiField NpTextArea message;
@@ -49,15 +48,14 @@
 
   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) {
+      @Override
       public void onTextChanged(String newText) {
         save.setEnabled(!newText.trim()
             .equals(originalMessage));
@@ -79,25 +77,25 @@
   }
 
   @UiHandler("save")
-  void onSave(ClickEvent e) {
+  void onSave(@SuppressWarnings("unused") ClickEvent e) {
     save.setEnabled(false);
-    ChangeApi.message(changeId.get(), revision, message.getText().trim(),
-        new GerritCallback<JavaScriptObject>() {
+    ChangeFileApi.putMessage(changeId, message.getText().trim(),
+        new GerritCallback<VoidResult>() {
           @Override
-          public void onSuccess(JavaScriptObject msg) {
-            Gerrit.display(PageLinks.toChange(changeId));
+          public void onSuccess(VoidResult result) {
+            Gerrit.display(PageLinks.toChangeInEditMode(changeId));
             hide();
           }
         });
   }
 
   @UiHandler("cancel")
-  void onCancel(ClickEvent e) {
+  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
     message.setText("");
     hide();
   }
 
-  private void hide() {
+  protected void hide() {
     for (Widget w = getParent(); w != null; w = w.getParent()) {
       if (w instanceof PopupPanel) {
         ((PopupPanel) w).hide();
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..429dd55 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.ChangeFileApi;
 import com.google.gerrit.client.changes.CommentInfo;
 import com.google.gerrit.client.changes.ReviewInfo;
 import com.google.gerrit.client.changes.Util;
@@ -27,6 +29,7 @@
 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.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 +49,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 +61,7 @@
 
 import java.sql.Timestamp;
 
-class FileTable extends FlowPanel {
+public class FileTable extends FlowPanel {
   static final FileTableResources R = GWT
       .create(FileTableResources.class);
 
@@ -80,20 +86,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 +124,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 +179,11 @@
   private boolean register;
   private JsArrayString reviewed;
   private String scrollToPath;
+  private ChangeScreen2.Style style;
+  private Widget editButton;
+  private Widget replyButton;
+  private boolean editExists;
+  private Mode mode;
 
   @Override
   protected void onLoad() {
@@ -146,9 +191,15 @@
     R.css().ensureInjected();
   }
 
-  void setRevisions(PatchSet.Id base, PatchSet.Id curr) {
+  public void set(PatchSet.Id base, PatchSet.Id curr, ChangeScreen2.Style style,
+      Widget editButton, Widget replyButton, Mode mode, boolean editExists) {
     this.base = base;
     this.curr = curr;
+    this.style = style;
+    this.editButton = editButton;
+    this.replyButton = replyButton;
+    this.mode = mode;
+    this.editExists = editExists;
   }
 
   void setValue(NativeMap<FileInfo> fileMap,
@@ -220,7 +271,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 +315,38 @@
           + curr.toString());
     }
 
+    void onDelete(int idx) {
+      String path = list.get(idx).path();
+      ChangeFileApi.deleteContent(curr, 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();
+      ChangeFileApi.restoreContent(curr, 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,7 +437,7 @@
 
   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;
@@ -372,15 +457,16 @@
         Timestamp myLastReply,
         NativeMap<JsArray<CommentInfo>> comments,
         NativeMap<JsArray<CommentInfo>> drafts) {
-      this.table = new MyTable(map, list);
+      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());
+      myTable.addStyleName(R.css().table());
     }
 
+    @Override
     public boolean execute() {
       boolean attachedNow = isAttached();
       if (!attached && attachedNow) {
@@ -408,9 +494,9 @@
         }
       }
       footer(sb);
-      table.resetHtml(sb);
-      table.finishDisplay();
-      setTable(table);
+      myTable.resetHtml(sb);
+      myTable.finishDisplay();
+      setTable(myTable);
       return false;
     }
 
@@ -449,7 +535,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 +556,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 +581,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,11 +620,16 @@
     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 {
@@ -629,7 +754,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
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTextBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTextBox.java
new file mode 100644
index 0000000..52d2a25
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTextBox.java
@@ -0,0 +1,70 @@
+// 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.changes.ChangeFileApi;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+
+class FileTextBox extends NpTextBox {
+  private HandlerRegistration blurHandler;
+  private NpTextArea textArea;
+  private PatchSet.Id id;
+
+  @Override
+  protected void onLoad() {
+    blurHandler = addBlurHandler(new BlurHandler() {
+      @Override
+      public void onBlur(BlurEvent event) {
+        loadFileContent();
+      }
+    });
+  }
+
+  @Override
+  protected void onUnload() {
+    super.onUnload();
+    blurHandler.removeHandler();
+  }
+
+  void set(PatchSet.Id id, NpTextArea content) {
+    this.id = id;
+    this.textArea = content;
+  }
+
+  private void loadFileContent() {
+    ChangeFileApi.getContent(id, getText(), new GerritCallback<String>() {
+      @Override
+      public void onSuccess(String result) {
+        textArea.setText(result);
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+        if (RestApi.isNotFound(caught)) {
+          // that means that the file doesn't exist in the repository
+        } else {
+          super.onFailure(caught);
+        }
+      }
+    });
+  }
+}
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..fd58e65
--- /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 ChangeScreen2 screen = ChangeScreen2.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 ChangeScreen2.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(ChangeScreen2.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/IncludedInAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInAction.java
index 58b286c..d06ebaf 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
@@ -30,6 +30,7 @@
     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..b0b2239 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;
@@ -92,18 +90,15 @@
   }
 
   private ChangeScreen2.Style style;
-  private Element statusText;
 
-  void init(ChangeScreen2.Style style, Element statusText) {
+  void init(ChangeScreen2.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) {
@@ -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/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..d9dd96e 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 {
     }
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
index fa65514..de6eefe 100644
--- 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
@@ -65,13 +65,13 @@
   }
 
   @UiHandler("keepOld")
-  void onKeepOld(ClickEvent e) {
+  void onKeepOld(@SuppressWarnings("unused") ClickEvent e) {
     save(ChangeScreen.OLD_UI);
     Gerrit.display(PageLinks.toChange(id));
   }
 
   @UiHandler("keepNew")
-  void onKeepNew(ClickEvent e) {
+  void onKeepNew(@SuppressWarnings("unused") ClickEvent e) {
     save(ChangeScreen.CHANGE_SCREEN2);
   }
 
@@ -84,7 +84,7 @@
 
       Prefs in = Prefs.createObject().cast();
       in.change_screen(sel.name());
-      AccountApi.self().view("preferences").background().post(in,
+      AccountApi.self().view("preferences").background().put(in,
         new AsyncCallback<JavaScriptObject>() {
           @Override public void onFailure(Throwable caught) {}
           @Override public void onSuccess(JavaScriptObject result) {}
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..e5b1f99 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
@@ -31,6 +31,7 @@
     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..70eeb73 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,8 +19,11 @@
 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;
@@ -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/RebaseAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java
index 2b1c250..b1ad583 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
@@ -25,6 +25,7 @@
   static void call(final Change.Id id, String revision) {
     ChangeApi.rebase(id.get(), revision,
       new GerritCallback<ChangeInfo>() {
+        @Override
         public void onSuccess(ChangeInfo result) {
           Gerrit.display(PageLinks.toChange(id));
         }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
index 5a0ba15..5f6c6de 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
@@ -55,6 +55,8 @@
     String current();
     String gitweb();
     String indirect();
+    String abandoned();
+    String merged();
     String notCurrent();
     String pointer();
     String row();
@@ -62,7 +64,7 @@
     String tabPanel();
   }
 
-  private enum Tab {
+  enum Tab {
     RELATED_CHANGES(Resources.C.relatedChanges(),
         Resources.C.relatedChangesTooltip()) {
       @Override
@@ -127,6 +129,8 @@
     }
   }
 
+  private static Tab savedTab;
+
   private final List<RelatedChangesTab> tabs;
   private int maxHeightWithHeader;
   private int selectedTab;
@@ -155,7 +159,7 @@
     });
 
     for (Tab tabInfo : Tab.values()) {
-      RelatedChangesTab panel = new RelatedChangesTab();
+      RelatedChangesTab panel = new RelatedChangesTab(tabInfo);
       add(panel, tabInfo.defaultTitle);
       tabs.add(panel);
 
@@ -222,6 +226,10 @@
     R.css().ensureInjected();
   }
 
+  static void setSavedTab(Tab subject) {
+    savedTab = subject;
+  }
+
   private RelatedChangesTab getTab(Tab tabInfo) {
     return tabs.get(tabInfo.ordinal());
   }
@@ -293,6 +301,10 @@
           }
         }
       }
+
+      if (tabInfo == savedTab && enabled) {
+        selectTab(savedTab.ordinal());
+      }
     }
   }
 
@@ -332,6 +344,13 @@
     }
 
     public final native String id() /*-{ return this.change_id }-*/;
+
+    public final Change.Status status() {
+      String s = statusRaw();
+      return s != null ? Change.Status.valueOf(s) : null;
+    }
+    private final native String statusRaw() /*-{ return this.status; }-*/;
+
     public final native CommitInfo commit() /*-{ return this.commit }-*/;
     final native String branch() /*-{ return this.branch }-*/;
 
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..2dfd53e 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
@@ -20,6 +20,7 @@
 import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
 import com.google.gerrit.client.changes.Util;
 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.JsArray;
@@ -82,6 +83,7 @@
   }
 
   private final SimplePanel panel;
+  private final RelatedChanges.Tab subject;
 
   private boolean showBranches;
   private boolean showIndirectAncestors;
@@ -91,8 +93,9 @@
   private String project;
   private NavigationList view;
 
-  RelatedChangesTab() {
+  RelatedChangesTab(RelatedChanges.Tab subject) {
     panel = new SimplePanel();
+    this.subject = subject;
   }
 
   @Override
@@ -200,6 +203,7 @@
       return false;
     }
 
+    @Override
     public boolean execute() {
       if (navList != view || !panel.isAttached()) {
         // If the user navigated away, we aren't in the DOM anymore.
@@ -298,6 +302,14 @@
         sb.setStyleName(RelatedChanges.R.css().notCurrent());
         sb.setAttribute("title", Util.C.notCurrent());
         sb.append('\u25CF');
+      } else if (Change.Status.MERGED == info.status()) {
+        sb.setStyleName(RelatedChanges.R.css().merged());
+        sb.setAttribute("title", Resources.C.merged());
+        sb.append('\u25CF');
+      } else if (Change.Status.ABANDONED == info.status()) {
+        sb.setStyleName(RelatedChanges.R.css().abandoned());
+        sb.setAttribute("title", Resources.C.abandoned());
+        sb.append('\u25CF');
       } else {
         sb.setStyleName(RelatedChanges.R.css().current());
       }
@@ -311,7 +323,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 +498,7 @@
         movePointerTo(startRow + DOM.getChildIndex(body, row), false);
         event.stopPropagation();
       }
+      saveSelectedTab();
     }
 
     @Override
@@ -536,6 +549,12 @@
           }
         }
       }
+
+      saveSelectedTab();
+    }
+
+    private void saveSelectedTab() {
+      RelatedChanges.setSavedTab(subject);
     }
 
     @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
index ddd650e..5eb936c 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
@@ -188,7 +188,7 @@
   }
 
   @UiHandler("post")
-  void onPost(ClickEvent e) {
+  void onPost(@SuppressWarnings("unused") ClickEvent e) {
     postReview();
   }
 
@@ -214,7 +214,7 @@
   }
 
   @UiHandler("cancel")
-  void onCancel(ClickEvent e) {
+  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
     message.setText("");
     hide();
   }
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..dd97d1c 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
@@ -26,7 +26,7 @@
 
 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,
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..e66ab4e 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,50 +65,33 @@
   @UiField Element form;
   @UiField Element error;
   @UiField(provided = true)
-  SuggestBox suggestBox;
+  RemoteSuggestBox suggestBox;
 
   private ChangeScreen2.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) {
@@ -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/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..31e43fe 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
@@ -32,10 +32,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/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/common.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/common.css
index ae00dc7..612ffed 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
@@ -33,7 +33,7 @@
   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..53729e7 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,11 +13,17 @@
  * 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 tr {
   vertical-align: top;
@@ -40,6 +46,7 @@
 }
 .pathColumn a {
   color: #000;
+  cursor: pointer;
 }
 .commonPrefix {
   color: #888;
@@ -85,3 +92,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/related_changes.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/related_changes.css
index 2e62b98..4d103ae 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/related_changes.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/related_changes.css
@@ -68,6 +68,8 @@
 .current,
 .gitweb,
 .indirect,
+.abandoned,
+.merged,
 .notCurrent {
   display: inline-block;
   text-align: center;
@@ -75,6 +77,7 @@
   width: 12px;
 }
 
+.merged,
 .gitweb {
   color: #000;
 }
@@ -84,6 +87,10 @@
   font-weight: bold;
 }
 
+.abandoned {
+  color: #900;      /* dark red */
+}
+
 .notCurrent {
   color: #FFA62F;   /* orange */
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
index b41a082..fc2af89 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
@@ -21,6 +21,7 @@
 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.ReviewerSuggestOracle;
 import com.google.gerrit.client.change.Reviewers.PostInput;
 import com.google.gerrit.client.change.Reviewers.PostResult;
 import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
@@ -29,7 +30,6 @@
 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;
@@ -254,6 +254,7 @@
     ChangeApi.reviewers(lastChange.legacy_id().get()).post(
         PostInput.create(reviewer, confirmed),
         new GerritCallback<PostResult>() {
+          @Override
           public void onSuccess(PostResult result) {
             addMemberBox.setEnabled(true);
             addMemberBox.setText("");
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..c3bbe18 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,7 +14,10 @@
 
 package com.google.gerrit.client.changes;
 
+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.PatchSet;
@@ -33,6 +36,18 @@
     call(id, "abandon").post(input, cb);
   }
 
+  /** Create a new change. */
+  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));
+
+    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 +83,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 +134,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,6 +180,23 @@
     revision(id, commit).delete(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();
+    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. */
   public static void rebase(int id, String commit, AsyncCallback<ChangeInfo> cb) {
     JavaScriptObject in = JavaScriptObject.createObject();
@@ -160,6 +215,20 @@
     }
   }
 
+  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; }-*/;
+
+    protected CreateChangeInput() {
+    }
+  }
+
   private static class CherryPickInput extends JavaScriptObject {
     static CherryPickInput create() {
       return (CherryPickInput) createObject();
@@ -198,4 +267,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/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index 03c58d2..2bbb429 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();
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..df1e2c5 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
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
index f2c97c1..3ad90d9 100644
--- 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
@@ -91,6 +91,7 @@
     r.setChange(toChange(info));
     r.setStarred(info.starred());
     r.setPatchSets(toPatchSets(info));
+    r.setMergeable(info.mergeable());
     r.setMessages(toMessages(info));
     r.setAccounts(users(info));
     r.setCurrentPatchSetId(new PatchSet.Id(info.legacy_id(), rev._number()));
@@ -224,7 +225,6 @@
     c.setStatus(info.status());
     c.setCurrentPatchSet(p);
     c.setLastUpdatedOn(info.updated());
-    c.setMergeable(info.mergeable());
     return c;
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeFileApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeFileApi.java
new file mode 100644
index 0000000..c25f0cf
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeFileApi.java
@@ -0,0 +1,166 @@
+// 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.rpc.GerritCallback;
+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.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+/**
+ * A collection of static methods which work on the Gerrit REST API for specific
+ * files in a change.
+ */
+public class ChangeFileApi {
+  static abstract class CallbackWrapper<I, O> implements AsyncCallback<I> {
+    protected AsyncCallback<O> wrapped;
+
+    public CallbackWrapper(AsyncCallback<O> callback) {
+      wrapped = callback;
+    }
+
+    @Override
+    public abstract void onSuccess(I result);
+
+    @Override
+    public void onFailure(Throwable caught) {
+      wrapped.onFailure(caught);
+    }
+  }
+
+  private static CallbackWrapper<NativeString, String> wrapper(
+      AsyncCallback<String> cb) {
+    return new CallbackWrapper<NativeString, String>(cb) {
+      @Override
+      public void onSuccess(NativeString b64) {
+        if (b64 != null) {
+          wrapped.onSuccess(b64decode(b64.asString()));
+        }
+      }
+    };
+  }
+
+  /** Get the contents of a File in a PatchSet or change edit. */
+  public static void getContent(PatchSet.Id id, String filename,
+      AsyncCallback<String> cb) {
+    contentEditOrPs(id, filename).get(wrapper(cb));
+  }
+
+  /** Get the content type of a File in a PatchSet or change edit. */
+  public static void getContentType(PatchSet.Id id, String filename,
+      AsyncCallback<String> cb) {
+    contentTypeEditOrPs(id, filename).get(
+        new CallbackWrapper<NativeString, String>(cb) {
+          @Override
+          public void onSuccess(NativeString str) {
+            if (str != null) {
+              wrapped.onSuccess(str.asString());
+            }
+          }
+        });
+  }
+
+  /**
+   * Get the contents of a file or commit message in a PatchSet or change
+   * edit.
+   **/
+  public static void getContentOrMessage(PatchSet.Id id, String path,
+      AsyncCallback<String> cb) {
+    RestApi api = (Patch.COMMIT_MSG.equals(path) && id.get() == 0)
+        ? messageEdit(id)
+        : contentEditOrPs(id, path);
+    api.get(wrapper(cb));
+  }
+
+  /** Put contents into a File in a change edit. */
+  public static void putContent(PatchSet.Id id, String filename,
+      String content, GerritCallback<VoidResult> result) {
+    contentEdit(id.getParentKey(), filename).put(content, result);
+  }
+
+  /** Put contents into a File or commit message in a change edit. */
+  public static void putContentOrMessage(PatchSet.Id id, String path,
+      String content, GerritCallback<VoidResult> result) {
+    if (Patch.COMMIT_MSG.equals(path)) {
+      putMessage(id, content, result);
+    } else {
+      contentEdit(id.getParentKey(), path).put(content, result);
+    }
+  }
+
+  /** Put message into a change edit. */
+  private static void putMessage(PatchSet.Id id, String m,
+      GerritCallback<VoidResult> r) {
+    putMessage(id.getParentKey(), m, r);
+  }
+
+  /** Put message into a change edit. */
+  public static void putMessage(Change.Id id, String m,
+      GerritCallback<VoidResult> r) {
+    ChangeApi.change(id.get()).view("edit:message").put(m, r);
+  }
+
+  /** Restore contents of a File in a change edit. */
+  public static void restoreContent(PatchSet.Id id, String filename,
+      AsyncCallback<VoidResult> result) {
+    Input in = Input.create();
+    in.restore_path(filename);
+    ChangeApi.edit(id.getParentKey().get()).post(in, result);
+  }
+
+  /** Delete a file from a change edit. */
+  public static void deleteContent(PatchSet.Id id, String filename,
+      AsyncCallback<VoidResult> result) {
+    contentEdit(id.getParentKey(), filename).delete(result);
+  }
+
+  private static RestApi contentEditOrPs(PatchSet.Id id, String filename) {
+    return id.get() == 0
+        ? contentEdit(id.getParentKey(), filename)
+        : ChangeApi.revision(id).view("files").id(filename).view("content");
+  }
+
+  private static RestApi messageEdit(PatchSet.Id id) {
+    return ChangeApi.change(id.getParentKey().get()).view("edit:message");
+  }
+
+  private static RestApi contentTypeEditOrPs(PatchSet.Id id, String filename) {
+    return id.get() == 0
+        ? contentEdit(id.getParentKey(), filename).view("type")
+        : ChangeApi.revision(id).view("files").id(filename).view("type");
+  }
+
+  private static RestApi contentEdit(Change.Id id, String filename) {
+    return ChangeApi.edit(id.get()).id(filename);
+  }
+
+  private static native String b64decode(String a) /*-{ return window.atob(a); }-*/;
+
+  private static class Input extends JavaScriptObject {
+    final native void restore_path(String p) /*-{ if(p)this.restore_path=p; }-*/;
+
+    static Input create() {
+      return (Input) createObject();
+    }
+
+    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..a9dec39 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;
@@ -85,7 +86,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 +96,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 +209,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 +257,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 +317,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
index 8fe7d56..5f5cf8f 100644
--- 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
@@ -125,7 +125,7 @@
     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
+      table.setText(R_MERGE_TEST, 1, changeDetail.isMergeable() ? Util.C
           .changeInfoBlockCanMergeYes() : Util.C.changeInfoBlockCanMergeNo());
     } else {
       table.getRowFormatter().setVisible(R_MERGE_TEST, false);
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/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
index 5a6e96c..e950ad4 100644
--- 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
@@ -313,6 +313,7 @@
               event.getValue().setSubmitTypeRecord(SubmitTypeRecord.OK(
                   SubmitType.valueOf(result.asString())));
             }
+            @Override
             public void onFailure(Throwable caught) {}
           }));
       }
@@ -388,7 +389,9 @@
                   ListChangesOption.CURRENT_REVISION));
                 call.get(cbs2.add(new AsyncCallback<
                     com.google.gerrit.client.changes.ChangeInfo>() {
+                  @Override
                   public void onFailure(Throwable caught) {}
+                  @Override
                   public void onSuccess(
                       com.google.gerrit.client.changes.ChangeInfo result) {
                     i.set(ChangeDetailCache.toChange(result),
@@ -398,6 +401,7 @@
                         .merge(ChangeDetailCache.users(result));
                   }}));
               }
+              @Override
               public void onFailure(Throwable caught) {}
             }));
         ChangeApi.revision(changeId.get(), revId)
@@ -416,6 +420,7 @@
                   p.setReviewedByCurrentUser(true);
                 }
               }
+              @Override
               public void onFailure(Throwable caught) {}
             }));
         final Set<PatchSet.Id> withDrafts = new HashSet<>();
@@ -432,6 +437,7 @@
                     withDrafts.add(id);
                   }
                 }
+                @Override
                 public void onFailure(Throwable caught) {}
               }));
           }
@@ -453,6 +459,7 @@
                 withDrafts.add(psId);
               }
             }
+            @Override
             public void onFailure(Throwable caught) {}
           }));
       }
@@ -470,6 +477,7 @@
               p.setCommentCount(result.get(path).length());
             }
           }
+          @Override
           public void onFailure(Throwable caught) {}
         }));
       DiffApi.list(changeId.get(), null, revId,
@@ -497,6 +505,7 @@
               }
               event.getValue().getCurrentPatchSetDetail().setPatches(list);
             }
+            @Override
             public void onFailure(Throwable caught) {}
       });
       ConfigInfoCache.get(
@@ -524,6 +533,7 @@
                 public void onSuccess(Void result) {
                   display(event.getValue());
                 }
+                @Override
                 public void onFailure(Throwable caught) {}
               }).onSuccess(null);
             }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
index 270b9f5..ca7157a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
@@ -18,8 +18,8 @@
 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.changes.ChangeInfo.LabelInfo;
 import com.google.gerrit.client.ui.AccountLinkPanel;
 import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.ChangeLink;
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
index 1f66b72..f8febb7 100644
--- 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
@@ -87,6 +87,7 @@
       sendButton.setEnabled(false);
 
       new TextBoxChangeListener(message) {
+        @Override
         public void onTextChanged(String newText) {
           // Trim the new text so we don't consider trailing
           // newlines as changes
@@ -95,6 +96,7 @@
       };
     }
 
+    @Override
     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
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..3b9cfc0 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
@@ -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/PatchSetComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
index b7a0ec8..3cddab7 100644
--- 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
@@ -319,10 +319,12 @@
               patchSet.getId().getParentKey().get(),
               patchSet.getRevision().get(),
               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();
@@ -480,10 +482,12 @@
           b.setEnabled(false);
           ChangeApi.deleteChange(patchSet.getId().getParentKey().get(),
               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();
@@ -545,6 +549,7 @@
           final Change.Id id = patchSet.getId().getParentKey();
           ChangeApi.rebase(id.get(), patchSet.getRevision().get(),
               new GerritCallback<ChangeInfo>() {
+                @Override
                 public void onSuccess(ChangeInfo result) {
                   Gerrit.display(PageLinks.toChange(id));
                 }
@@ -691,6 +696,7 @@
 
       Util.DETAIL_SVC.patchSetDetail2(diffBaseId, patchSet.getId(), diffPrefs,
           new GerritCallback<PatchSetDetail>() {
+            @Override
             public void onSuccess(final PatchSetDetail result) {
               loadInfoTable(result);
               loadActionPanel(result);
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/changes/PatchTable.java
index 638ec13..96c567d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java
@@ -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;
@@ -103,7 +102,7 @@
     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++) {
@@ -247,7 +246,7 @@
       SafeHtml before, SafeHtml after) {
     Patch patch = patchList.get(index);
 
-    Key thisKey = patch.getKey();
+    Patch.Key thisKey = patch.getKey();
     PatchLink link;
 
     if (isUnifiedPatchLink(patch, screenType)) {
@@ -457,6 +456,7 @@
       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()));
@@ -744,6 +744,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();
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
index f718b5d..e839717 100644
--- 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
@@ -171,6 +171,7 @@
           submitTypeRecord = SubmitTypeRecord.OK(
               SubmitType.valueOf(result.asString()));
         }
+        @Override
         public void onFailure(Throwable caught) {}
       }));
     ChangeApi.revision(patchSetId.getParentKey().get(), "" + patchSetId.get())
@@ -180,6 +181,7 @@
         public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
           drafts = result;
         }
+        @Override
         public void onFailure(Throwable caught) {}
       }));
     ChangeApi.revision(patchSetId).view("review")
@@ -445,6 +447,7 @@
       patchSetId.getParentKey().get(),
       "" + patchSetId.get(),
       new GerritCallback<SubmitInfo>() {
+          @Override
           public void onSuccess(SubmitInfo result) {
             saveStateOnUnload = false;
             goChange();
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..91582b2 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,7 +32,7 @@
 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.Pos;
 import net.codemirror.lib.LineWidget;
 import net.codemirror.lib.TextMarker;
 
@@ -67,7 +64,7 @@
         event.stopPropagation();
       }
     }
-  };
+  }
 
   static void focusOnClick(Element e, DisplaySide side) {
     onClick(e, side == A ? focusA : focusB);
@@ -79,7 +76,7 @@
   private final SideBySide2 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;
@@ -91,11 +88,11 @@
   ChunkManager(SideBySide2 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();
   }
 
@@ -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,7 @@
     return new Runnable() {
       @Override
       public void run() {
-        int line = cm.hasActiveLine() ? cm.getLineNumber(cm.getActiveLine()) : 0;
+        int line = cm.hasActiveLine() ? cm.getLineNumber(cm.activeLine()) : 0;
         int res = Collections.binarySearch(
                 chunks,
                 new DiffChunkInfo(cm.side(), line, 0, false),
@@ -318,11 +315,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..8d8f117 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
@@ -26,7 +26,7 @@
 
 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;
@@ -92,7 +92,7 @@
         // on either side of the editor pair.
         SortedMap<Integer, CommentGroup> map = map(src.side());
         int line = src.hasActiveLine()
-            ? src.getLineNumber(src.getActiveLine()) + 1
+            ? src.getLineNumber(src.activeLine()) + 1
             : 0;
         if (dir == Direction.NEXT) {
           map = map.tailMap(line + 1);
@@ -115,8 +115,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();
       }
     };
@@ -159,10 +159,9 @@
             getPatchSetIdFromSide(side),
             info);
         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 +222,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 +293,11 @@
 
   Runnable toggleOpenBox(final CodeMirror cm) {
     return new Runnable() {
+      @Override
       public void run() {
         if (cm.hasActiveLine()) {
           CommentGroup w = map(cm.side()).get(
-              cm.getLineNumber(cm.getActiveLine()) + 1);
+              cm.getLineNumber(cm.activeLine()) + 1);
           if (w != null) {
             w.openCloseLast();
           }
@@ -313,7 +312,7 @@
       public void run() {
         if (cm.hasActiveLine()) {
           CommentGroup w = map(cm.side()).get(
-              cm.getLineNumber(cm.getActiveLine()) + 1);
+              cm.getLineNumber(cm.activeLine()) + 1);
           if (w != null) {
             w.openCloseAll();
           }
@@ -329,7 +328,7 @@
         public void run() {
           String token = host.getToken();
           if (cm.hasActiveLine()) {
-            LineHandle handle = cm.getActiveLine();
+            LineHandle handle = cm.activeLine();
             int line = cm.getLineNumber(handle) + 1;
             token += "@" + (cm.side() == DisplaySide.A ? "a" : "") + line;
           }
@@ -339,6 +338,7 @@
     }
 
     return new Runnable() {
+      @Override
       public void run() {
         if (cm.hasActiveLine()) {
           newDraft(cm);
@@ -348,13 +348,13 @@
   }
 
   private void newDraft(CodeMirror cm) {
-    int line = cm.getLineNumber(cm.getActiveLine()) + 1;
+    int line = cm.getLineNumber(cm.activeLine()) + 1;
     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/DiffApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
index 5e88ac9..321883d 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.addParameter("weblinks-only", true);
+    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..8d94438 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,33 @@
   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 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 +134,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..8bdc747 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
@@ -61,7 +61,7 @@
 
   @UiField Element cmA;
   @UiField Element cmB;
-  @UiField OverviewBar overview;
+  Scrollbar scrollbar;
   @UiField Element patchSetNavRow;
   @UiField Element patchSetNavCellA;
   @UiField Element patchSetNavCellB;
@@ -79,6 +79,7 @@
   private SideBySide2 parent;
   private boolean header;
   private boolean headerVisible;
+  private boolean autoHideHeader;
   private boolean visibleA;
   private ChangeType changeType;
 
@@ -91,6 +92,7 @@
     PatchSetSelectBox2.link(patchSetSelectBoxA, patchSetSelectBoxB);
 
     initWidget(uiBinder.createAndBindUi(this));
+    this.scrollbar = new Scrollbar(this);
     this.parent = parent;
     this.headerVisible = true;
     this.visibleA = true;
@@ -133,7 +135,11 @@
   }
 
   void setHeaderVisible(boolean show) {
-    headerVisible = show;
+    headerVisible = !autoHideHeader || show;
+    showHeader(headerVisible);
+  }
+
+  private void showHeader(boolean show) {
     UIObject.setVisible(patchSetNavRow, show);
     UIObject.setVisible(diffHeaderRow, show && header);
     if (show) {
@@ -144,6 +150,13 @@
     parent.resizeCodeMirror();
   }
 
+  void setAutoHideDiffHeader(boolean hide) {
+    autoHideHeader = hide;
+    if (!hide) {
+      showHeader(true);
+    }
+  }
+
   int getHeaderHeight() {
     int h = patchSetSelectBoxA.getOffsetHeight();
     if (header) {
@@ -156,10 +169,14 @@
     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) {
     this.changeType = info.change_type();
-    patchSetSelectBoxA.setUpPatchSetNav(list, info.meta_a());
-    patchSetSelectBoxB.setUpPatchSetNav(list, info.meta_b());
+    this.autoHideHeader = prefs.autoHideDiffTableHeader();
+    patchSetSelectBoxA.setUpPatchSetNav(list, info.meta_a(), editExists,
+        currentPatchSet, open);
+    patchSetSelectBoxB.setUpPatchSetNav(list, info.meta_b(), editExists,
+        currentPatchSet, open);
 
     JsArrayString hdr = info.diff_header();
     if (hdr != null) {
@@ -195,7 +212,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..1420d3b 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
@@ -19,9 +19,10 @@
     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-linenumber;
+    @external .CodeMirror-overlayscroll-vertical, .CodeMirror-scroll;
     @external .CodeMirror-dialog-bottom;
-    @external .cm-keymap-fat-cursor, CodeMirror-cursor;
+    @external .cm-animate-fat-cursor, .CodeMirror-cursor;
     @external .cm-searching, .cm-trailingspace, .cm-tab;
 
     .fullscreen {
@@ -75,14 +76,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; }
@@ -134,11 +130,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;
@@ -186,16 +191,13 @@
         <td ui:field='patchSetNavCellB' class='{style.psNavB}'>
           <d:PatchSetSelectBox2 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..a3ccb6f 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
@@ -236,14 +236,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 +253,7 @@
   }
 
   @UiHandler("message")
-  void onMessageDoubleClick(DoubleClickEvent e) {
+  void onMessageDoubleClick(@SuppressWarnings("unused") DoubleClickEvent e) {
     setEdit(true);
   }
 
@@ -389,7 +389,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/Header.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
index 2b3e2be..7753cb3 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();
   }
 
@@ -282,6 +294,7 @@
 
   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/PatchSetSelectBox2.java
index 7154c9f..0ad6ad5 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/PatchSetSelectBox2.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,8 +36,11 @@
 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> {}
@@ -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) {
     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,9 +103,34 @@
     } 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 (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());
+      }
+    }
+  }
+
+  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(PatchSetSelectBox2 a, PatchSetSelectBox2 b) {
@@ -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/PatchSetSelectBox2.ui.xml
index dca0cd5..ba7a0a7 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/PatchSetSelectBox2.ui.xml
@@ -22,6 +22,7 @@
       type='com.google.gerrit.client.patches.PatchConstants'/>
   <ui:style type='com.google.gerrit.client.diff.PatchSetSelectBox2.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/PreferencesBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
index ac11cd5..4eec4ba 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,13 @@
 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.common.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.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 +54,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 {
@@ -90,6 +89,7 @@
   @UiField ToggleButton leftSide;
   @UiField ToggleButton emptyPane;
   @UiField ToggleButton topMenu;
+  @UiField ToggleButton autoHideDiffTableHeader;
   @UiField ToggleButton manualReview;
   @UiField ToggleButton expandAllComments;
   @UiField ToggleButton renderEntireFile;
@@ -157,6 +157,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 +193,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 +323,13 @@
     view.resizeCodeMirror();
   }
 
+  @UiHandler("autoHideDiffTableHeader")
+  void onAutoHideDiffTableHeader(ValueChangeEvent<Boolean> e) {
+    prefs.autoHideDiffTableHeader(!e.getValue());
+    view.setAutoHideDiffHeader(!e.getValue());
+    view.resizeCodeMirror();
+  }
+
   @UiHandler("manualReview")
   void onManualReview(ValueChangeEvent<Boolean> e) {
     prefs.manualReview(e.getValue());
@@ -338,26 +346,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 +393,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 +479,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 +513,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..2f22cdd 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
@@ -250,6 +250,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..8c5f2a8 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
@@ -109,6 +109,7 @@
     return UIObject.isVisible(message);
   }
 
+  @Override
   void setOpen(boolean open) {
     UIObject.setVisible(summary, !open);
     UIObject.setVisible(message, open);
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..61e00c3 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,7 +22,6 @@
 class ScrollSynchronizer {
   private DiffTable diffTable;
   private LineMapper mapper;
-  private OverviewBar overview;
   private ScrollCallback active;
   private ScrollCallback callbackA;
   private ScrollCallback callbackB;
@@ -32,7 +31,6 @@
       LineMapper mapper) {
     this.diffTable = diffTable;
     this.mapper = mapper;
-    this.overview = diffTable.overview;
 
     callbackA = new ScrollCallback(cmA, cmB, DisplaySide.A);
     callbackB = new ScrollCallback(cmB, cmA, DisplaySide.B);
@@ -45,9 +43,9 @@
   }
 
   private void updateScreenHeader(ScrollInfo si) {
-    if (si.getTop() == 0 && !diffTable.isHeaderVisible()) {
+    if (si.top() == 0 && !diffTable.isHeaderVisible()) {
       diffTable.setHeaderVisible(true);
-    } else if (si.getTop() > 0.5 * si.getClientHeight()
+    } else if (si.top() > 0.5 * si.clientHeight()
         && diffTable.isHeaderVisible()) {
       diffTable.setHeaderVisible(false);
     }
@@ -75,7 +73,7 @@
     }
 
     void sync() {
-      dst.scrollToY(align(src.getScrollInfo().getTop()));
+      dst.scrollToY(align(src.getScrollInfo().top()));
     }
 
     @Override
@@ -87,8 +85,7 @@
       if (active == this) {
         ScrollInfo si = src.getScrollInfo();
         updateScreenHeader(si);
-        overview.update(si);
-        dst.scrollTo(si.getLeft(), align(si.getTop()));
+        dst.scrollTo(si.left(), align(si.top()));
         state = 0;
       }
     }
@@ -97,7 +94,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..a535115
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Scrollbar.css
@@ -0,0 +1,58 @@
+/* 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.
+ */
+
+@external .CodeMirror;
+@external .CodeMirror-overlayscroll-horizontal;
+@external .CodeMirror-overlayscroll-vertical;
+
+.CodeMirror-overlayscroll-horizontal div {
+  min-width: 25px;
+}
+.CodeMirror-overlayscroll-vertical div {
+  min-height: 25px;
+}
+
+.CodeMirror .CodeMirror-overlayscroll-vertical {
+  z-index: inherit;
+}
+.CodeMirror .CodeMirror-overlayscroll-horizontal div,
+.CodeMirror .CodeMirror-overlayscroll-vertical div {
+  background-color: rgba(128, 128, 128, 0.50);
+  z-index: 8;
+}
+
+.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/SideBySide2.java
index 30306f2..42b49d3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
@@ -21,8 +21,10 @@
 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.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,6 +35,7 @@
 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;
@@ -59,6 +62,7 @@
 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;
@@ -71,10 +75,13 @@
 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;
 
@@ -122,6 +129,7 @@
   private ScrollSynchronizer scrollSynchronizer;
   private DiffInfo diff;
   private FileSize fileSize;
+  private EditInfo edit;
   private ChunkManager chunkManager;
   private CommentManager commentManager;
   private SkipManager skipManager;
@@ -167,10 +175,11 @@
     super.onLoad();
 
     CallbackGroup cmGroup = new CallbackGroup();
-    CodeMirror.initLibrary(cmGroup.add(CallbackGroup.<Void> emptyCallback()));
+    CodeMirror.initLibrary(cmGroup.<Void> addEmpty());
+
     final CallbackGroup group = new CallbackGroup();
-    final AsyncCallback<Void> modeInjectorCb =
-        group.add(CallbackGroup.<Void> emptyCallback());
+    final AsyncCallback<Void> themeCallback = group.addEmpty();
+    final AsyncCallback<Void> modeInjectorCb = group.addEmpty();
 
     DiffApi.diff(revision, path)
       .base(base)
@@ -182,6 +191,9 @@
         public void onSuccess(DiffInfo diffInfo) {
           diff = diffInfo;
           fileSize = bucketFileSize(diffInfo);
+
+          // Load theme after CM library to ensure theme can override CSS.
+          ThemeLoader.loadTheme(prefs.theme(), themeCallback);
           if (prefs.syntaxHighlighting()) {
             if (fileSize.compareTo(FileSize.SMALL) > 0) {
               modeInjectorCb.onSuccess(null);
@@ -194,6 +206,16 @@
         }
       }));
 
+    if (Gerrit.isSignedIn()) {
+      ChangeApi.edit(changeId.get(), group.add(
+          new GerritCallback<EditInfo>() {
+            @Override
+            public void onSuccess(EditInfo result) {
+              edit = result;
+            }
+          }));
+    }
+
     final CommentsCollections comments = new CommentsCollections();
     comments.load(base, revision, path, group);
 
@@ -204,9 +226,16 @@
       @Override
       public void onSuccess(ChangeInfo info) {
         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,
+            info.status().isOpen());
         header.setChangeInfo(info);
       }}));
 
@@ -271,10 +300,10 @@
       if (cm.lineAtHeight(height - 20) < line) {
         cm.scrollToY(cm.heightAtLine(line, "local") - 0.5 * height);
       }
-      cm.setCursor(LineCharacter.create(line));
+      cm.setCursor(Pos.create(line));
       cm.focus();
     } else {
-      cmA.setCursor(LineCharacter.create(0));
+      cmA.setCursor(Pos.create(0));
       cmA.focus();
     }
     if (Gerrit.isSignedIn() && prefs.autoReview()) {
@@ -343,6 +372,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 +399,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() {
@@ -400,10 +430,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 +444,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());
       }
     };
   }
@@ -527,18 +555,17 @@
       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);
+    cmA = newCM(diff.meta_a(), diff.text_a(), diffTable.cmA).side(DisplaySide.A);
+    cmB = newCM(diff.meta_b(), diff.text_b(), diffTable.cmB).side(DisplaySide.B);
+    chunkManager = new ChunkManager(this, cmA, cmB, diffTable.scrollbar);
     skipManager = new SkipManager(this, commentManager);
 
     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.mover().appendChild(columnMarginA);
+    cmB.mover().appendChild(columnMarginB);
 
     if (prefs.renderEntireFile() && !canEnableRenderEntireFile(prefs)) {
       // CodeMirror is too slow to layout an entire huge file.
@@ -546,6 +573,7 @@
     }
 
     operation(new Runnable() {
+      @Override
       public void run() {
         // Estimate initial CM3 height, fixed up in onShowView.
         int height = Window.getClientHeight()
@@ -565,7 +593,7 @@
             chunkManager.getLineMapper());
 
     prefsAction = new PreferencesAction(this, prefs);
-    header.init(prefsAction);
+    header.init(prefsAction, getLinks(), diff.side_by_side_web_links());
 
     if (prefs.syntaxHighlighting() && fileSize.compareTo(FileSize.SMALL) > 0) {
       Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
@@ -580,22 +608,48 @@
     }
   }
 
+  private List<InlineHyperlink> getLinks() {
+    // skip change edits
+    if (revision.get() > 0) {
+      InlineHyperlink toUnifiedDiffLink = new InlineHyperlink();
+      toUnifiedDiffLink.setHTML(new ImageResourceRenderer().render(Gerrit.RESOURCES.unifiedDiff()));
+      toUnifiedDiffLink.setTargetHistoryToken(getUnifiedDiffUrl());
+      toUnifiedDiffLink.setTitle(PatchUtil.C.unifiedDiff());
+      return Collections.singletonList(toUnifiedDiffLink);
+    } else {
+      return Collections.emptyList();
+    }
+  }
+
+  private String getUnifiedDiffUrl() {
+    StringBuilder url = new StringBuilder();
+    url.append("/c/");
+    url.append(changeId.get());
+    url.append("/");
+    if (base != null) {
+      url.append(base.get());
+      url.append("..");
+    }
+    url.append(revision.get());
+    url.append("/");
+    url.append(path);
+    url.append(",unified");
+    return url.toString();
+  }
+
   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")
@@ -652,7 +706,7 @@
         e.appendChild(pre);
       }
 
-      cmB.getMeasureElement().appendChild(p);
+      cmB.measure().appendChild(p);
       lineHeightPx = ((double) p.getOffsetHeight()) / lines;
       p.removeFromParent();
     }
@@ -670,11 +724,11 @@
       e.getStyle().setDisplay(Style.Display.INLINE_BLOCK);
       e.setInnerText(s.toString());
 
-      cmA.getMeasureElement().appendChild(e);
+      cmA.measure().appendChild(e);
       double a = ((double) e.getOffsetWidth()) / len;
       e.removeFromParent();
 
-      cmB.getMeasureElement().appendChild(e);
+      cmB.measure().appendChild(e);
       double b = ((double) e.getOffsetWidth()) / len;
       e.removeFromParent();
       charWidthPx = Math.max(a, b);
@@ -736,11 +790,14 @@
       public void run() {
         skipManager.removeAll();
         skipManager.render(context, diff);
-        diffTable.overview.refresh();
       }
     });
   }
 
+  void setAutoHideDiffHeader(boolean hide) {
+    diffTable.setAutoHideDiffHeader(hide);
+  }
+
   private void render(DiffInfo diff) {
     header.setNoDiff(diff);
     chunkManager.render(diff);
@@ -760,16 +817,17 @@
 
   private void clearActiveLine(CodeMirror cm) {
     if (cm.hasActiveLine()) {
-      LineHandle activeLine = cm.getActiveLine();
+      LineHandle activeLine = cm.activeLine();
       cm.removeLineClass(activeLine,
           LineClassWhere.WRAP, DiffTable.style.activeLine());
-      cm.setActiveLine(null);
+      cm.activeLine(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,23 +838,24 @@
           @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.hasActiveLine() && cm.activeLine().equals(handle)) {
                   return;
                 }
 
                 clearActiveLine(cm);
                 clearActiveLine(other);
-                cm.setActiveLine(handle);
+                cm.activeLine(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.activeLine(oLineHandle);
                   other.addLineClass(oLineHandle, LineClassWhere.WRAP,
                       DiffTable.style.activeLine());
                 }
@@ -819,8 +878,8 @@
             && !clickEvent.getCtrlKey()
             && !clickEvent.getShiftKey()) {
           if (!(cm.hasActiveLine() &&
-              cm.getLineNumber(cm.getActiveLine()) == line)) {
-            cm.setCursor(LineCharacter.create(line));
+              cm.getLineNumber(cm.activeLine()) == line)) {
+            cm.setCursor(Pos.create(line));
           }
           Scheduler.get().scheduleDeferred(new ScheduledCommand() {
             @Override
@@ -835,6 +894,7 @@
 
   private Runnable upToChange(final boolean openReplyBox) {
     return new Runnable() {
+      @Override
       public void run() {
         CallbackGroup group = new CallbackGroup();
         commentManager.saveAllDrafts(group);
@@ -842,11 +902,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 ChangeScreen2(changeId, b, rev, openReplyBox,
+                  FileTable.Mode.REVIEW));
           }
         });
       }
@@ -865,11 +926,12 @@
 
     final DisplaySide sideSrc = cmSrc.side();
     return new Runnable() {
+      @Override
       public void run() {
         if (cmSrc.hasActiveLine()) {
-          cmDst.setCursor(LineCharacter.create(lineOnOther(
+          cmDst.setCursor(Pos.create(lineOnOther(
               sideSrc,
-              cmSrc.getLineNumber(cmSrc.getActiveLine())).getLine()));
+              cmSrc.getLineNumber(cmSrc.activeLine())).getLine()));
         }
         cmDst.focus();
       }
@@ -880,8 +942,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 +955,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();
         }
@@ -918,7 +980,6 @@
     int height = getCodeMirrorHeight();
     cmA.setHeight(height);
     cmB.setHeight(height);
-    diffTable.overview.refresh();
   }
 
   private int getCodeMirrorHeight() {
@@ -936,11 +997,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) {
@@ -1021,7 +1083,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/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..0baada5 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
@@ -38,10 +38,6 @@
     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/UpToChangeCommand2.java
index 7071e7f..7a3ec45 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand2.java
@@ -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/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/EditScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
new file mode 100644
index 0000000..5c2e5ef
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
@@ -0,0 +1,180 @@
+// 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 com.google.gerrit.client.Gerrit;
+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.ChangeFileApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.diff.FileInfo;
+import com.google.gerrit.client.diff.Header;
+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.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.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+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;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.Configuration;
+import net.codemirror.mode.ModeInfo;
+import net.codemirror.mode.ModeInjector;
+
+public class EditScreen extends Screen {
+  interface Binder extends UiBinder<HTMLPanel, EditScreen> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  private final PatchSet.Id revision;
+  private final String path;
+  private DiffPreferences prefs;
+  private CodeMirror cm;
+  private String type;
+
+  @UiField Element project;
+  @UiField Element filePath;
+  @UiField Button cancel;
+  @UiField Button save;
+  @UiField Element editor;
+
+  public EditScreen(Patch.Key patch) {
+    this.revision = patch.getParentKey();
+    this.path = patch.get();
+    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 cmGroup = new CallbackGroup();
+    CodeMirror.initLibrary(cmGroup.<Void> addEmpty());
+    CallbackGroup group = new CallbackGroup();
+    if (!Patch.COMMIT_MSG.equals(path)) {
+      final AsyncCallback<Void> modeInjectorCb = group.addEmpty();
+      ChangeFileApi.getContentType(revision, path,
+          cmGroup.add(new GerritCallback<String>() {
+            @Override
+            public void onSuccess(String result) {
+              ModeInfo mode = ModeInfo.findMode(result, path);
+              type = mode != null ? mode.mime() : null;
+              injectMode(result, modeInjectorCb);
+            }
+          }));
+    }
+    cmGroup.done();
+
+    ChangeApi.detail(revision.getParentKey().get(),
+        group.add(new GerritCallback<ChangeInfo>() {
+          @Override
+          public void onSuccess(ChangeInfo c) {
+            project.setInnerText(c.project());
+            SafeHtml.setInnerHTML(filePath, Header.formatPath(path, null, null));
+          }
+        }));
+
+    ChangeFileApi.getContentOrMessage(revision, path,
+        group.addFinal(new ScreenLoadCallback<String>(this) {
+          @Override
+          protected void preDisplay(String content) {
+            initEditor(content);
+          }
+        }));
+  }
+
+  @Override
+  public void onShowView() {
+    super.onShowView();
+    if (prefs.hideTopMenu()) {
+      Gerrit.setHeaderVisible(false);
+    }
+    int rest = Gerrit.getHeaderFooterHeight()
+        + 30; // Estimate
+    cm.setHeight(Window.getClientHeight() - rest);
+    cm.refresh();
+    cm.focus();
+  }
+
+  @Override
+  protected void onUnload() {
+    super.onUnload();
+    Gerrit.setHeaderVisible(true);
+  }
+
+  @UiHandler("save")
+  void onSave(@SuppressWarnings("unused") ClickEvent e) {
+    ChangeFileApi.putContentOrMessage(revision, path, cm.getValue(),
+        new GerritCallback<VoidResult>() {
+          @Override
+          public void onSuccess(VoidResult result) {
+            Gerrit.display(PageLinks.toChangeInEditMode(
+                revision.getParentKey()));
+          }
+        });
+  }
+
+  @UiHandler("cancel")
+  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
+    Gerrit.display(PageLinks.toChangeInEditMode(revision.getParentKey()));
+  }
+
+  private void initEditor(String content) {
+    cm = CodeMirror.create(editor, getConfig());
+    cm.setValue(content);
+  }
+
+  private void injectMode(String type, AsyncCallback<Void> cb) {
+    new ModeInjector().add(type).inject(cb);
+  }
+
+  private Configuration getConfig() {
+    // TODO(davido): Retrieve user preferences from AllUsers repository
+    return Configuration.create()
+        .set("readOnly", false)
+        .set("cursorBlinkRate", 0)
+        .set("cursorHeight", 0.85)
+        .set("lineNumbers", true)
+        .set("tabSize", 4)
+        .set("lineWrapping", false)
+        .set("styleSelectedText", true)
+        .set("showTrailingSpace", true)
+        .set("keyMap", "default")
+        .set("mode", type);
+  }
+}
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..8033bbf
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml
@@ -0,0 +1,84 @@
+<?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>
+    .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: #999;
+    }
+
+    .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;
+    }
+  </ui:style>
+  <g:HTMLPanel>
+    <div class='{style.headerLine}'>
+       <div class='{style.headerButtons}'>
+         <g:Button ui:field='cancel'
+             styleName=''
+             title='Cancel'>
+           <ui:attribute name='title'/>
+           <div><ui:msg>Cancel</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>
+    <div ui:field='editor' />
+  </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..5fd8fa2 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
@@ -232,6 +232,7 @@
   margin-bottom: 0px;
   padding-top: 0.5em;
   padding-bottom: 0.5em;
+  max-height: 100000px;
 }
 .commentPanelButtons {
   margin-left: 0.5em;
@@ -683,6 +684,10 @@
   font-size: small;
 }
 
+.linkPanel img {
+  padding-right: 3px;
+}
+
 
 /** PatchContentTable **/
 .patchContentTable {
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..cc37075 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
@@ -541,6 +541,11 @@
     }
   }
 
+  /**
+   * Update cursor after selecting a comment.
+   *
+   * @param newComment comment that was selected.
+   */
   protected void updateCursor(final PatchLineComment newComment) {
   }
 
@@ -724,7 +729,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);
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..e9832c3 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
@@ -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/NavLinks.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
index 9f36342..f908c79 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,6 +15,7 @@
 package com.google.gerrit.client.patches;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.WebLinkInfo;
 import com.google.gerrit.client.changes.PatchTable;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.ui.ChangeLink;
@@ -22,6 +23,7 @@
 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 +31,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 +60,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,13 +68,15 @@
     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, PatchScreen.Type type, 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));
@@ -78,6 +84,16 @@
       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/PatchConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
index 9ff9893..0a99a98 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
@@ -77,6 +77,7 @@
   String reviewedAnd();
   String next();
   String download();
+  String edit();
   String addFileCommentToolTip();
   String addFileCommentByDoubleClick();
 
@@ -88,4 +89,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..0a9ce83 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
@@ -59,6 +59,7 @@
 reviewedAnd = Reviewed &
 next = next
 download = Download
+edit = Edit
 addFileCommentToolTip = Click to add file comment
 addFileCommentByDoubleClick = Double click to add file comment
 
@@ -67,3 +68,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/PatchScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
index ad96f24..e333c26 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/PatchScreen.java
@@ -18,14 +18,18 @@
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.RpcStatus;
+import com.google.gerrit.client.WebLinkInfo;
 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.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,10 +47,14 @@
 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;
 
+import java.util.Collections;
+import java.util.List;
+
 public abstract class PatchScreen extends Screen implements
     CommentEditorContainer {
   static final PrettyFactory PRETTY = ClientSideFormatter.FACTORY;
@@ -271,11 +279,74 @@
     }
 
     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, getPatchScreenType(), fileList,
+              getLinks(), getWebLinks(diffInfo));
+          bottomNav.display(patchIndex, getPatchScreenType(), fileList,
+              getLinks(), getWebLinks(diffInfo));
+        }
+      });
+  }
+
+  private List<InlineHyperlink> getLinks() {
+    if (contentTable instanceof SideBySideTable) {
+      InlineHyperlink toUnifiedDiffLink = new InlineHyperlink();
+      toUnifiedDiffLink.setHTML(new ImageResourceRenderer().render(Gerrit.RESOURCES.unifiedDiff()));
+      toUnifiedDiffLink.setTargetHistoryToken(getUnifiedDiffUrl());
+      toUnifiedDiffLink.setTitle(PatchUtil.C.unifiedDiff());
+      return Collections.singletonList(toUnifiedDiffLink);
+    } else if (contentTable instanceof UnifiedDiffTable) {
+      InlineHyperlink toSideBySideDiffLink = new InlineHyperlink();
+      toSideBySideDiffLink.setHTML(new ImageResourceRenderer().render(Gerrit.RESOURCES.sideBySideDiff()));
+      toSideBySideDiffLink.setTargetHistoryToken(getSideBySideDiffUrl());
+      toSideBySideDiffLink.setTitle(PatchUtil.C.sideBySideDiff());
+      return Collections.singletonList(toSideBySideDiffLink);
+    } else {
+      throw new IllegalStateException("unknown table type: "
+          + contentTable.getClass().getSimpleName());
+    }
+  }
+
+  private List<WebLinkInfo> getWebLinks(DiffInfo diffInfo) {
+    if (contentTable instanceof SideBySideTable) {
+      return diffInfo.side_by_side_web_links();
+    } else if (contentTable instanceof UnifiedDiffTable) {
+      return diffInfo.unified_web_links();
+    } else {
+      throw new IllegalStateException("unknown table type: "
+          + contentTable.getClass().getSimpleName());
+    }
+  }
+
+  private String getSideBySideDiffUrl() {
+    StringBuilder url = new StringBuilder();
+    url.append("/c/");
+    url.append(patchKey.getParentKey().getParentKey().get());
+    url.append("/");
+    if (idSideA != null) {
+      url.append(idSideA.get());
+      url.append("..");
+    }
+    url.append(idSideB.get());
+    url.append("/");
+    url.append(patchKey.getFileName());
+    return url.toString();
+  }
+
+  private String getUnifiedDiffUrl() {
+    return getSideBySideDiffUrl() + ",unified";
+  }
+
   @Override
   protected void onLoad() {
     super.onLoad();
@@ -485,8 +556,7 @@
     lastScript = script;
 
     if (fileList != null) {
-      topNav.display(patchIndex, getPatchScreenType(), fileList);
-      bottomNav.display(patchIndex, getPatchScreenType(), fileList);
+      displayNav();
     }
 
     if (Gerrit.isSignedIn()) {
@@ -564,6 +634,7 @@
         fileList.setSavePointerId("PatchTable " + psid);
         Util.DETAIL_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/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/ReviewedPanels.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/ReviewedPanels.java
index 68df45d..b445fca 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
@@ -135,6 +135,7 @@
   private InlineHyperlink createReviewedLink(final int patchIndex,
       final PatchScreen.Type patchScreenType) {
     final PatchValidator unreviewedValidator = new PatchValidator() {
+      @Override
       public boolean isValid(Patch patch) {
         return !patch.isReviewedByCurrentUser();
       }
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
index 2d58816..8835904 100644
--- 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
@@ -57,10 +57,12 @@
   private boolean isHugeFile;
   protected boolean isFileCommentBorderRowExist;
 
+  @Override
   protected void createFileCommentEditorOnSideA() {
     createCommentEditor(R_HEAD + 1, A, R_HEAD, FILE_SIDE_A);
   }
 
+  @Override
   protected void createFileCommentEditorOnSideB() {
     createCommentEditor(R_HEAD + 1, B, R_HEAD, FILE_SIDE_B);
   }
@@ -96,7 +98,7 @@
     final ArrayList<Object> lines = new ArrayList<>();
     final SafeHtmlBuilder nc = new SafeHtmlBuilder();
     isHugeFile = script.isHugeFile();
-    allocateTableHeader(script, nc);
+    allocateTableHeader(nc);
     lines.add(null);
     if (!isDisplayBinary) {
       if (script.getFileModeA() != FileMode.FILE
@@ -119,7 +121,7 @@
                 && script.hasIntralineDifference();
         for (final EditList.Hunk hunk : script.getHunks()) {
           if (!hunk.isStartOfFile()) {
-            appendSkipLine(nc, hunk.getCurB() - lastB);
+            appendSkipLine(nc);
             lines.add(new SkippedLine(lastA, lastB, hunk.getCurB() - lastB));
           }
 
@@ -185,7 +187,7 @@
           lastB = hunk.getCurB();
         }
         if (lastB != b.size()) {
-          appendSkipLine(nc, b.size() - lastB);
+          appendSkipLine(nc);
           lines.add(new SkippedLine(lastA, lastB, b.size() - lastB));
         }
       }
@@ -329,13 +331,13 @@
         } else {
           insertRow(row);
         }
-        bindComment(row, A, ac, !ai.hasNext(), expandComments);
-        bindComment(row, B, bc, !bi.hasNext(), expandComments);
+        bindComment(row, A, ac, !ai.hasNext());
+        bindComment(row, B, bc, !bi.hasNext());
         row++;
       }
 
-      row = finish(ai, row, A, expandComments);
-      row = finish(bi, row, B, expandComments);
+      row = finish(ai, row, A);
+      row = finish(bi, row, B);
     }
   }
 
@@ -390,7 +392,7 @@
     }
   }
 
-  private int finish(final Iterator<PatchLineComment> i, int row, final int col, boolean expandComment) {
+  private int finish(final Iterator<PatchLineComment> i, int row, final int col) {
     while (i.hasNext()) {
       final PatchLineComment c = i.next();
       if (c.getLine() == R_HEAD) {
@@ -398,13 +400,13 @@
       } else {
         insertRow(row);
       }
-      bindComment(row, col, c, !i.hasNext(), expandComment);
+      bindComment(row, col, c, !i.hasNext());
       row++;
     }
     return row;
   }
 
-  private void allocateTableHeader(PatchScript script, final SafeHtmlBuilder m) {
+  private void allocateTableHeader(final SafeHtmlBuilder m) {
     m.openTr();
 
     m.openTd();
@@ -457,7 +459,7 @@
     m.closeTr();
   }
 
-  private void appendSkipLine(final SafeHtmlBuilder m, final int skipCnt) {
+  private void appendSkipLine(final SafeHtmlBuilder m) {
     m.openTr();
 
     m.openTd();
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..2562ce1 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();
@@ -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++;
@@ -422,7 +425,7 @@
     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 +433,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/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..58d8f00 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
@@ -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..45c26de 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
@@ -85,6 +85,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 +96,7 @@
     in.setUseContentMerge(useContentMerge);
     in.setUseSignedOffBy(useSignedOffBy);
     in.setRequireChangeId(requireChangeId);
+    in.setCreateNewChangeForAllNotInTarget(createNewChangeForAllNotInTarget);
     in.setMaxObjectSizeLimit(maxObjectSizeLimit);
     in.setSubmitType(submitType);
     in.setState(state);
@@ -209,6 +211,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; }-*/;
 
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..6a9ddb5 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
@@ -66,6 +66,11 @@
     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);
@@ -91,7 +96,7 @@
   }
 
   public void addListener(CallbackGroup group) {
-    addListener(group.add(CallbackGroup.<Void> emptyCallback()));
+    addListener(group.<Void> addEmpty());
   }
 
   private void applyAllSuccess() {
@@ -103,6 +108,15 @@
     }
   }
 
+  private void applyAllFailed() {
+    if (failed && finalAdded && remaining.isEmpty()) {
+      for (CallbackImpl<?> cb : callbacks) {
+        cb.applyFailed();
+      }
+      callbacks.clear();
+    }
+  }
+
   private <T> Callback<T> handleAdd(AsyncCallback<T> cb) {
     if (failed) {
       cb.onFailure(failedThrowable);
@@ -135,10 +149,6 @@
 
     @Override
     public void onSuccess(T value) {
-      if (failed) {
-        return;
-      }
-
       this.result = value;
       remaining.remove(this);
       CallbackGroup.this.applyAllSuccess();
@@ -146,19 +156,12 @@
 
     @Override
     public void onFailure(Throwable caught) {
-      if (failed) {
-        return;
+      if (!failed) {
+        failed = true;
+        failedThrowable = caught;
       }
-
-      failed = true;
-      failedThrowable = caught;
-      for (CallbackImpl<?> cb : callbacks) {
-        cb.delegate.onFailure(failedThrowable);
-        cb.delegate = null;
-        cb.result = null;
-      }
-      callbacks.clear();
-      remaining.clear();
+      remaining.remove(this);
+      CallbackGroup.this.applyAllFailed();
     }
 
     void applySuccess() {
@@ -169,5 +172,14 @@
         result = null;
       }
     }
+
+    void applyFailed() {
+      AsyncCallback<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..08ff7d9 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
@@ -33,6 +33,7 @@
 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) {
     if (isNotSignedIn(caught) || isInvalidXSRF(caught)) {
       new NotSignedInDialog().center();
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..b677276 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
@@ -325,6 +325,11 @@
     send(DELETE, cb);
   }
 
+  public <T extends JavaScriptObject> void delete(JavaScriptObject content,
+      AsyncCallback<T> cb) {
+    sendJSON(DELETE, content, cb);
+  }
+
   private <T extends JavaScriptObject> void send(
       Method method, AsyncCallback<T> cb) {
     HttpCallback<T> httpCallback = new HttpCallback<>(background, cb);
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/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/CommentPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
index bb50b19c..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..23e1109 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
@@ -16,12 +16,10 @@
 
 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;
@@ -29,6 +27,7 @@
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.NpTextArea;
 import com.google.gwtexpui.user.client.AutoCenterDialogBox;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 
 public abstract class CommentedActionDialog<T> extends AutoCenterDialogBox
     implements CloseHandler<PopupPanel> {
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..a515568
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
@@ -0,0 +1,106 @@
+// 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.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;
+import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class CreateChangeDialog extends ActionDialog {
+  private SuggestBox newChange;
+  private List<BranchInfo> branches;
+
+  public CreateChangeDialog(final FocusWidget enableOnFailure, Project.NameKey project) {
+    super(enableOnFailure, true, 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/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/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/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..c6e1063 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
@@ -15,6 +15,7 @@
 package net.codemirror.lib;
 
 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;
@@ -28,274 +29,275 @@
  * @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;
+  public static native CodeMirror create(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 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 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 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 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;
+    return !!this.state.activeLine
   }-*/;
 
-  public final native LineHandle getActiveLine() /*-{
-    return this.state.activeLine;
+  public final native LineHandle activeLine() /*-{
+    return this.state.activeLine
   }-*/;
 
-  public final native void setActiveLine(LineHandle line) /*-{
-    this.state.activeLine = line;
+  public final native void activeLine(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 final native Element scrollbarV() /*-{
+    return this.display.scrollbarV
   }-*/;
 
   public static final native KeyMap cloneKeyMap(String name) /*-{
@@ -308,36 +310,31 @@
   }-*/;
 
   public final native void execCommand(String cmd) /*-{
-    this.execCommand(cmd);
+    this.execCommand(cmd)
   }-*/;
 
   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 static final native void mapVimKey(String alias, String actual) /*-{
-    $wnd.CodeMirror.Vim.map(alias, actual);
-  }-*/;
-
-  public final native boolean hasVimSearchHighlight() /*-{
-    return this.state.vim && this.state.vim.searchState_ &&
-        !!this.state.vim.searchState_.getOverlay();
+  public final native Vim vim() /*-{
+    return this;
   }-*/;
 
   public final native DisplaySide side() /*-{ return this._sbs2_side }-*/;
+  public final native CodeMirror side(DisplaySide side) /*-{
+    this._sbs2_side = side;
+    return 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 +346,11 @@
     }
   }
 
+  public static class RegisteredHandler extends JavaScriptObject {
+    protected RegisteredHandler() {
+    }
+  }
+
   public interface EventHandler {
     public void handle(CodeMirror instance, NativeEvent event);
   }
@@ -363,6 +365,6 @@
   }
 
   public interface BeforeSelectionChangeHandler {
-    public void handle(CodeMirror instance, LineCharacter anchor, LineCharacter head);
+    public void handle(CodeMirror instance, Pos anchor, Pos head);
   }
 }
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/Lib.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Lib.java
index bb60fe9..874d186 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,10 @@
 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();
 }
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..2643f84 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java
@@ -14,6 +14,7 @@
 
 package net.codemirror.lib;
 
+import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gwt.core.client.Callback;
 import com.google.gwt.core.client.ScriptInjector;
@@ -26,47 +27,49 @@
 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 GerritCallback<Void>() {
+      @Override
+      public void onSuccess(Void result) {
+        Vim.initKeyMap();
+      }
+    }));
+    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());
+          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 +82,6 @@
 
         @Override
         public void onFailure(Exception reason) {
-          error(reason);
           callback.onFailure(reason);
         }
        })
@@ -87,33 +89,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..e7da469
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Vim.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 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() {
+    // 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);
+
+    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/mode/ModeInfo.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
new file mode 100644
index 0000000..2dc034b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
@@ -0,0 +1,197 @@
+// 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.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..2bd5b00 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,49 @@
 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("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..53a2a02
--- /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.common.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..97d28f0 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',
@@ -53,6 +54,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..22c862f 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
@@ -31,6 +31,7 @@
     return account.hashCode();
   }
 
+  @Override
   public boolean equals(Object other) {
     if (other instanceof AdvertisedObjectsCacheKey) {
       AdvertisedObjectsCacheKey o = (AdvertisedObjectsCacheKey) other;
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 bbe6972..c4ca15d 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,6 +15,7 @@
 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;
@@ -147,9 +148,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<>();
@@ -173,6 +172,17 @@
       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);
+
     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..1f26aa3 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
@@ -98,12 +98,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 +127,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);
 
@@ -308,6 +317,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;
 
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 7e1aa28..9ccdcfc 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 @@
   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-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProxyProperties.java
similarity index 69%
copy from gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
copy to gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProxyProperties.java
index cd07320..67b97c4 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProxyProperties.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 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.
@@ -14,14 +14,10 @@
 
 package com.google.gerrit.httpd;
 
-public class GerritUiOptions {
-  private final boolean headless;
+import java.net.URL;
 
-  public GerritUiOptions(boolean headless) {
-    this.headless = headless;
-  }
-
-  public boolean enableDefaultUi() {
-    return !headless;
-  }
+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 3c4dfc5..4651959 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
@@ -63,11 +63,11 @@
   }
 
   private final UrlConfig cfg;
-  private GerritUiOptions uiOptions;
+  private GerritOptions options;
 
-  UrlModule(UrlConfig cfg, GerritUiOptions uiOptions) {
+  UrlModule(UrlConfig cfg, GerritOptions options) {
     this.cfg = cfg;
-    this.uiOptions = uiOptions;
+    this.options = options;
   }
 
   @Override
@@ -75,7 +75,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());
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 3443968..4c52a5e 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;
@@ -50,18 +47,18 @@
   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 +79,34 @@
     }
     install(new RunAsFilter.Module());
 
+    if (options.enableMasterFeatures()) {
+      installAuthModule();
+      install(new UrlModule(urlConfig, options));
+      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:
@@ -109,28 +134,5 @@
       default:
         throw new ProvisionException("Unsupported loginType: " + authConfig.getAuthType());
     }
-
-    install(new UrlModule(urlConfig, uiOptions));
-    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..5f93a56 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;
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 f593caca..9a7cab0 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
@@ -402,7 +402,7 @@
     }
     try {
       CacheHeaders.setNotCacheable(rsp);
-      exec(req, rsp, project, repo);
+      exec(req, rsp, project);
     } finally {
       repo.close();
     }
@@ -440,8 +440,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),
@@ -600,6 +599,7 @@
     final int contentLength = req.getContentLength();
     final InputStream src = req.getInputStream();
     new Thread(new Runnable() {
+      @Override
       public void run() {
         try {
           try {
@@ -626,6 +626,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 dd4a899..f5c6c0c 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,10 +26,12 @@
 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.plugins.Plugin;
 import com.google.gerrit.server.plugins.Plugin.ApiType;
@@ -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 = new PluginResourceKey(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();
@@ -605,7 +606,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/ResourceKey.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginResourceKey.java
similarity index 79%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceKey.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginResourceKey.java
index 068d6b4..afe3b79 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceKey.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginResourceKey.java
@@ -14,18 +14,20 @@
 
 package com.google.gerrit.httpd.plugins;
 
+import com.google.gerrit.httpd.resources.ResourceKey;
 import com.google.gerrit.server.plugins.Plugin;
 
-final class ResourceKey {
+final class PluginResourceKey implements ResourceKey {
   private final Plugin.CacheKey plugin;
   private final String resource;
 
-  ResourceKey(Plugin p, String r) {
+  PluginResourceKey(Plugin p, String r) {
     this.plugin = p.getCacheKey();
     this.resource = r;
   }
 
-  int weigh() {
+  @Override
+  public int weigh() {
     return resource.length() * 2;
   }
 
@@ -36,8 +38,8 @@
 
   @Override
   public boolean equals(Object other) {
-    if (other instanceof ResourceKey) {
-      ResourceKey rk = (ResourceKey) other;
+    if (other instanceof PluginResourceKey) {
+      PluginResourceKey rk = (PluginResourceKey) 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..ab89d98 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,6 +14,7 @@
 
 package com.google.gerrit.httpd.raw;
 
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
@@ -24,7 +25,6 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 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;
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-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/resources/ResourceKey.java
similarity index 65%
copy from gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
copy to gerrit-httpd/src/main/java/com/google/gerrit/httpd/resources/ResourceKey.java
index cd07320..ef5a1df 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/resources/ResourceKey.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 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,16 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd;
+package com.google.gerrit.httpd.resources;
 
-public class GerritUiOptions {
-  private final boolean headless;
-
-  public GerritUiOptions(boolean headless) {
-    this.headless = headless;
-  }
-
-  public boolean enableDefaultUi() {
-    return !headless;
-  }
+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..5055d47 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
@@ -26,12 +26,12 @@
 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_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,9 +47,11 @@
 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;
@@ -78,7 +80,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;
@@ -221,7 +223,7 @@
         try {
           rsrc = rc.parse(rsrc, id);
           if (path.isEmpty()) {
-            checkPreconditions(req, rsrc);
+            checkPreconditions(req);
           }
         } catch (ResourceNotFoundException e) {
           if (rc instanceof AcceptsCreate
@@ -254,6 +256,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 +268,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 +279,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;
             }
@@ -330,28 +343,35 @@
           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 (Exception e) {
       status = SC_INTERNAL_SERVER_ERROR;
       handleException(e, req, res);
@@ -363,6 +383,13 @@
     }
   }
 
+  private static String messageOr(Throwable t, String defaultMessage) {
+    if (!Strings.isNullOrEmpty(t.getMessage())) {
+      return t.getMessage();
+    }
+    return defaultMessage;
+  }
+
   private static boolean notModified(HttpServletRequest req, RestResource rsrc) {
     if (!isGetOrHead(req)) {
       return false;
@@ -425,7 +452,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");
@@ -858,7 +885,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 +941,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
index da21c51..d4cbde0 100644
--- 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
@@ -14,8 +14,8 @@
 
 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.gerrit.server.query.change.OutputStreamQuery;
+import com.google.gerrit.server.query.change.OutputStreamQuery.OutputFormat;
 import com.google.gson.Gson;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -31,11 +31,11 @@
 @Singleton
 public class DeprecatedChangeQueryServlet extends HttpServlet {
   private static final long serialVersionUID = 1L;
-  private final Provider<QueryProcessor> processor;
+  private final Provider<OutputStreamQuery> queryProvider;
 
   @Inject
-  DeprecatedChangeQueryServlet(Provider<QueryProcessor> processor) {
-    this.processor = processor;
+  DeprecatedChangeQueryServlet(Provider<OutputStreamQuery> queryProvider) {
+    this.queryProvider = queryProvider;
   }
 
   @Override
@@ -44,7 +44,7 @@
     rsp.setContentType("text/json");
     rsp.setCharacterEncoding("UTF-8");
 
-    QueryProcessor p = processor.get();
+    OutputStreamQuery p = queryProvider.get();
     OutputFormat format = OutputFormat.JSON;
     try {
       format = OutputFormat.valueOf(get(req, "format", format.toString()));
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..4a673cb 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
@@ -170,7 +170,8 @@
       // 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..2df0ac8 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
@@ -47,6 +47,7 @@
     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) {
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..88c67c1 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(
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..63ec4ae 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
@@ -16,6 +16,7 @@
 
 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;
@@ -35,18 +36,17 @@
 import com.google.gerrit.server.account.GroupBackend;
 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.index.ChangeIndexer;
 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;
@@ -79,7 +79,7 @@
   private final IdentifiedUser user;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final Provider<PostReviewers> reviewersProvider;
-  private final MergeabilityChecker mergeabilityChecker;
+  private final ChangeIndexer indexer;
   private final ChangeHooks hooks;
   private final CreateChangeSender.Factory createChangeSenderFactory;
   private final ProjectCache projectCache;
@@ -91,7 +91,8 @@
       MetaDataUpdate.User metaDataUpdateFactory, ReviewDb db,
       IdentifiedUser user, PatchSetInfoFactory patchSetInfoFactory,
       Provider<PostReviewers> reviewersProvider,
-      MergeabilityChecker mergeabilityChecker, ChangeHooks hooks,
+      ChangeIndexer indexer,
+      ChangeHooks hooks,
       CreateChangeSender.Factory createChangeSenderFactory,
       ProjectCache projectCache,
       AllProjectsNameProvider allProjects,
@@ -110,7 +111,7 @@
     this.user = user;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.reviewersProvider = reviewersProvider;
-    this.mergeabilityChecker = mergeabilityChecker;
+    this.indexer = indexer;
     this.hooks = hooks;
     this.createChangeSenderFactory = createChangeSenderFactory;
     this.projectCache = projectCache;
@@ -155,7 +156,7 @@
     } finally {
       db.rollback();
     }
-    mergeabilityChecker.newCheck().addChange(change).reindex().run();
+    indexer.index(db, change);
     hooks.doPatchsetCreatedHook(change, ps, db);
     try {
       CreateChangeSender cm =
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-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..a25ce2c 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,7 +16,6 @@
 
 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.IndexModule;
@@ -69,8 +68,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 d3dc963..99bf7e0 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", version(index));
       return;
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-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 92ee908..435bfa7 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;
@@ -56,7 +56,7 @@
 class LoginForm extends HttpServlet {
   private static final Logger log = LoggerFactory.getLogger(LoginForm.class);
   private static final ImmutableMap<String, String> ALL_PROVIDERS = ImmutableMap.of(
-      "google", OpenIdUrls.URL_GOOGLE,
+      "launchpad", OpenIdUrls.URL_LAUNCHPAD,
       "yahoo", OpenIdUrls.URL_YAHOO);
 
   private final ImmutableSet<String> suggestProviders;
@@ -220,7 +220,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/OpenIdServiceImpl.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
index 1681bc2..ba806f0 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-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html b/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
index f5734ffe..1e2c510 100644
--- a/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
+++ b/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
@@ -57,10 +57,11 @@
           <a href="../" id="cancel_link">Cancel</a>
         </div>
 
-        <div id="provider_google">
-          <img height="16" width="16" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABsUlEQVR42rWSy0sbYRRH88fYhW5KQXThIkUQN6ISJFBBW6FFBN2UqlC78LVKFlHIoqWGakF8gUZ8gajVzKSKZpQmAWtK0pBmhkxmSgPuiuaYmRKhSEwU/ODyDZfzO8ydOxbLQx1NB0H4y8FBlmMpy9LyOavrKUoKB4IXvBsIIopRPJ441dY/2Fr8fAtFSxO0dkRy4eA1PDp+TkVlmGhMKS5IJOFpbYgj6fQalhV4UhVm0esvLojELimv1JiYOPwPflS2gM93WNoIXZ1nPC4/IRDQzYDTqTA0PM2dtuCZ8vGmV6SvP5srL/da5c+4TnNLFlvTMYmEVrpkazuM2x1naOQ7Dc1fqLOmaayXkaSz4hLHewm7fY+Z2R0iP+JmYGpym/q6DDb76u0CwR+g9+0FHz4u3wAdLh9VNWu5UX4VlqysfaXRDoODNz+aw7nDs9ZPpNPpwgJV1WhvF6m1/qb79T6fpwXm5kVGRnd5/mIdQSzwH2iahq7/27nxiq5xLy9fbdDWsUtPzyausRVC4VMMzmAMNv9syTdkWTZLVVUymYx5J5NJs5dKpVAUxaw8Z/SM7BV+fz9Uc4yYRAAAAABJRU5ErkJggg==" />
-          <a href="?id=https://www.google.com/accounts/o8/id" id="id_google">Sign in with a Google Account</a>
+        <div id="provider_launchpad">
+          <img height="16" width="16" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAHKSURBVCiRjZI9SFtRGIafc5tiVFCkhUxKBAcLYoPWwE0GEUPBwc0f1MHFoWLBiKABobWtBh2kacFYwUUQRXBoB3EJCVUvyBVrIB1UskTawUGFQOoP5h6H3ki8deg7vuc853zvxyuklORLDUc9QH9nZeAL0AGM+1v0cywSOVANR8uBaaALOOysDLwCYsAp8AaY97fo2RyoqOFooRqOvgUOTMiqJ8AsEA9tuH13IFkcQD1QZHpbQE+bPXEADAK5MSsA1/p+1SMAoTbF9oAh2WYUAcWbz5tThiQE1EsI7VxVzMWvHe2lGWP1RaqgWyKe1g5vjyhAHfBdrCmuWG1zqSHRAS9gFxBQC451/+PdhbqUPSYR00JSAmDLy+IE7ICwZHRkbBQKqMo3lQeW8V/KB5PAMSAtd06Kb7iQf8/vgXuA9/2V9nN7dCKjCNyABlxKmPqx7HQvDjX0HX29bBLIUSlIA4gNT9Bpw/gMtJqPaYD/2evJX5FgTQcwDpQBaeCD8+XZx8aZZFaxYZyYv/4xQS+wFAnWVAOfTAgzRrxxJpkFUHza2IVPG3sHVAMrD+zhFBgAXL0JPZIzhbXkEe+kB+j/nf52V/LexL8lvwVTCpkwGXEEfAAAAABJRU5ErkJggg=="/>
+          <a href="?id=https://login.launchpad.net/%2Bopenid" id="id_launchpad">Sign in with a Launchpad ID</a>
         </div>
+
         <div id="provider_yahoo">
           <img height="16" width="16" src="data:image/gif;base64,R0lGODlhEAAQAPECAAAAAP8AAP///8zMzCH5BAEAAAMALAAAAAAQABAAAAIqnI+py30BY3AgAjCkfJDjiIAQlgUkNxqWkqrm0honKk7KhZOzw/f+fygAADs=" />
           <a href="?id=https://me.yahoo.com" id="id_yahoo">Sign in with a Yahoo! ID</a>
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 3a89cd0..e9b40cc 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,100 +34,83 @@
   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-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-lucene:lucene',
-    '//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/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',
-    '//lib/jetty:jmx',
-    '//lib/jgit:jgit',
-    '//lib/log:api',
-    '//lib/log:log4j',
-    '//lib/lucene:core',
+  ],
+  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-openid:openid',
+    '//gerrit-solr:solr',
+    '//lib:args4j',
+    '//lib:gwtorm',
+    '//lib:servlet-api-3_1',
     '//lib/prolog:prolog-cafe',
   ],
   provided_deps = ['//gerrit-launcher:launcher'],
   visibility = [
     '//:',
     '//gerrit-acceptance-tests/...',
+    '//gerrit-gwtdebug:gwtdebug',
     '//tools/eclipse:classpath',
     '//Documentation:licenses.txt',
   ],
@@ -136,8 +120,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 88a16fb..2044e33 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;
@@ -31,11 +32,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;
@@ -44,7 +43,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;
@@ -53,6 +51,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;
@@ -67,6 +66,8 @@
 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.SecureStore;
+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;
@@ -105,7 +106,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;
   }
 
@@ -113,11 +114,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)")
@@ -189,9 +190,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) {
@@ -284,7 +282,7 @@
       initSshd();
     }
 
-    if (Objects.firstNonNull(httpd, true)) {
+    if (MoreObjects.firstNonNull(httpd, true)) {
       initHttpd();
     }
 
@@ -318,7 +316,6 @@
     modules.add(new WorkQueue.Module());
     modules.add(new ChangeHookRunner.Module());
     modules.add(new ReceiveCommitsExecutorModule());
-    modules.add(new MergeabilityChecksExecutorModule());
     modules.add(new IntraLineWorkerPool.Module());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
     modules.add(new InternalAccountDirectory.Module());
@@ -328,7 +325,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() {
@@ -354,7 +351,10 @@
     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(SecureStore.class).toProvider(SecureStoreProvider.class);
+        }
       }
     });
     modules.add(GarbageCollectionRunner.module());
@@ -420,6 +420,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..710aabd 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,15 +17,18 @@
 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.SecureStore;
+import com.google.gerrit.server.securestore.SecureStoreProvider;
 import com.google.gerrit.server.util.HostPlatform;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
@@ -101,6 +104,7 @@
       protected void configure() {
         bind(File.class).annotatedWith(SitePath.class).toInstance(getSitePath());
         bind(Browser.class);
+        bind(SecureStore.class).toProvider(SecureStoreProvider.class);
       }
     });
     modules.add(new GerritServerConfigModule());
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..5532abe
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNotedb.java
@@ -0,0 +1,297 @@
+// 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.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 {
+    RevWalk rw = new RevWalk(repo);
+    try {
+      bru.execute(rw, NullProgressMonitor.INSTANCE);
+    } finally {
+      rw.release();
+    }
+  }
+
+  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(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 c0cf8f9..1fccd02 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,102 +15,41 @@
 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.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.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();
 
@@ -121,9 +60,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;
 
@@ -131,7 +67,6 @@
   private boolean dryRun;
 
   private Injector dbInjector;
-  private Config cfg;
   private Injector sysInjector;
   private ChangeIndex index;
 
@@ -139,10 +74,8 @@
   public int run() throws Exception {
     mustHaveValidSite();
     dbInjector = createDbInjector(MULTI_USER);
-    cfg = dbInjector.getInstance(
-        Key.get(Config.class, GerritServerConfig.class));
+    threads = ThreadLimiter.limitThreads(dbInjector, threads);
     checkNotSlaveMode();
-    limitThreads();
     disableLuceneAutomaticCommit();
     if (version == null) {
       version = ChangeSchemas.getLatest().getVersion();
@@ -172,27 +105,16 @@
   }
 
   private void checkNotSlaveMode() throws Die {
+    Config cfg = dbInjector.getInstance(
+        Key.get(Config.class, GerritServerConfig.class));
     if (cfg.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(PatchListCacheImpl.module());
-    AbstractModule changeIndexModule;
+    Module changeIndexModule;
     switch (IndexModule.getIndexType(dbInjector)) {
       case LUCENE:
         changeIndexModule = new LuceneIndexModule(version, threads, outputBase);
@@ -204,56 +126,7 @@
         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));
-        }
-      }
-    });
+    modules.add(dbInjector.getInstance(BatchProgramModule.class));
 
     return dbInjector.createChildInjector(modules);
   }
@@ -267,81 +140,6 @@
     }
   }
 
-  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 int indexAll() throws Exception {
     ReviewDb db = sysInjector.getInstance(ReviewDb.class);
     ProgressMonitor pm = new TextProgressMonitor();
@@ -361,11 +159,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/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 92%
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..8eff603 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,20 +12,17 @@
 // 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.Stage.PRODUCTION;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 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.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;
@@ -135,10 +132,22 @@
     return false;
   }
 
+  /**
+   * 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 +177,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;
@@ -196,7 +205,8 @@
         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);
@@ -230,10 +240,10 @@
     return ConsoleUI.getInstance(false);
   }
 
-  static class SiteRun {
-    final ConsoleUI ui;
-    final SitePaths site;
-    final InitFlags flags;
+  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;
@@ -319,7 +329,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 a2037b5..91d1a41 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..2bca8ec 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");
 
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..893f00d 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;
@@ -98,7 +99,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..cc076b5 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;
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-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-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..bff810f 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,16 @@
 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.saveSecure;
+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.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.mail.OutgoingEmail;
 import com.google.inject.Binding;
@@ -85,7 +86,7 @@
     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);
 
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..2b4bd2d 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,11 +14,14 @@
 
 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 static com.google.gerrit.pgm.init.api.InitUtil.saveSecure;
 
-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.util.SocketUtil;
 import com.google.inject.Inject;
@@ -86,6 +89,7 @@
     return false;
   }
 
+  @Override
   public void run() throws IOException, ConfigInvalidException {
     if (!isNeedUpgrade()) {
       return;
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 77%
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..dda536d 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);
   }
 
@@ -118,6 +146,7 @@
           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.
@@ -151,6 +180,14 @@
     } 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 91%
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..6ef7071 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,8 +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.annotations.VisibleForTesting;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -41,9 +42,9 @@
   public final FileBasedConfig sec;
   public final List<String> installPlugins;
 
-
+  @VisibleForTesting
   @Inject
-  InitFlags(final SitePaths site,
+  public InitFlags(final SitePaths site,
       final @InstallPlugins List<String> installPlugins) throws IOException,
       ConfigInvalidException {
     this.installPlugins = installPlugins;
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 85%
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..c51420e 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,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 static com.google.gerrit.common.FileUtil.chmod;
 import static com.google.gerrit.common.FileUtil.modified;
@@ -38,22 +38,22 @@
 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 {
+  public static void saveSecure(final FileBasedConfig sec) throws IOException {
     if (modified(sec)) {
       final byte[] out = Constants.encode(sec.toText());
       final File path = sec.getFile();
@@ -73,25 +73,25 @@
     }
   }
 
-  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 +99,7 @@
     }
   }
 
-  static String dnOf(String name) {
+  public static String dnOf(String name) {
     if (name != null) {
       int p = name.indexOf("://");
       if (0 < p) {
@@ -117,7 +117,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 +131,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 +158,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 +196,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 +208,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 95%
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..e17a032 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,11 +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.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.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -50,7 +48,7 @@
     this.subsection = subsection;
   }
 
-  String get(String name) {
+  public String get(String name) {
     return flags.cfg.getString(section, subsection, name);
   }
 
@@ -116,7 +114,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;
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..77acb10
--- /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.ChangeCache;
+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.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(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(ChangeCache.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-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..1d39b8a 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
@@ -30,6 +30,8 @@
 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.SecureStore;
+import com.google.gerrit.server.securestore.SecureStoreProvider;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.AbstractModule;
 import com.google.inject.Binding;
@@ -96,6 +98,7 @@
       @Override
       protected void configure() {
         bind(File.class).annotatedWith(SitePath.class).toInstance(sitePath);
+        bind(SecureStore.class).toProvider(SecureStoreProvider.class);
       }
     };
     modules.add(sitePathModule);
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 98%
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..57411ef 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
@@ -326,6 +326,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
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..c6da0f0 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,7 +24,9 @@
 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 org.eclipse.jgit.errors.ConfigInvalidException;
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 8439532..40767e0 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-SNAPSHOT</version>
+  <version>2.11-SNAPSHOT</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 404f0d9..b117b29 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-SNAPSHOT</version>
+  <version>2.11-SNAPSHOT</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 38b223a..a7f2bbf 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-SNAPSHOT</version>
+  <version>2.11-SNAPSHOT</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..07e475b 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'])
 
@@ -26,7 +27,13 @@
   name = 'gwtui-api-lib2',
   srcs = SRCS,
   resources = glob(['src/main/**/*']),
-  exported_deps = ['//gerrit-gwtui-common:client-lib2'],
+  exported_deps = [
+    '//gerrit-gwtexpui:Clippy',
+    '//gerrit-gwtexpui:GlobalKey',
+    '//gerrit-gwtexpui:SafeHtml',
+    '//gerrit-gwtexpui:UserAgent',
+    '//gerrit-gwtui-common:client-lib2',
+  ],
   provided_deps = DEPS,
   visibility = ['PUBLIC'],
 )
@@ -35,6 +42,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 +58,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 444b792..fd6fc9b 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-SNAPSHOT</version>
+  <version>2.11-SNAPSHOT</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/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/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 9da4e89..796df19 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-SNAPSHOT</version>
+  <version>2.11-SNAPSHOT</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..668ebed5 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
@@ -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..9b1991b 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,15 @@
   ],
   visibility = ['PUBLIC'],
 )
+
+java_test(
+  name = 'client_tests',
+  srcs = glob([TESTS + 'client/**/*.java']),
+  deps = [
+    ':client',
+    '//lib:gwtorm',
+    '//lib:junit',
+  ],
+  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..1f1e6cb 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.common.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/AccountGeneralPreferences.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
index c6b3089..8947083 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
@@ -154,6 +154,8 @@
   @Column(id = 10)
   protected boolean reversePatchSetOrder;
 
+  // DELETED: id = 11 (showUserInReview)
+
   @Column(id = 12)
   protected boolean relativeDateInChangeTable;
 
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..0e7b2ef 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.common.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;
@@ -362,6 +450,8 @@
   @Column(id = 10)
   protected char status;
 
+  // DELETED: id = 11 (nbrPatchSets)
+
   /** The current patch set. */
   @Column(id = 12)
   protected int currentPatchSetId;
@@ -374,16 +464,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 +488,6 @@
     owner = ownedBy;
     dest = forBranch;
     setStatus(Status.NEW);
-    setLastSha1MergeTested(null);
   }
 
   public Change(Change other) {
@@ -406,16 +496,14 @@
     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 +541,6 @@
     return rowVersion;
   }
 
-  public String getSortKey() {
-    return sortKey;
-  }
-
-  public void setSortKey(final String newSortKey) {
-    sortKey = newSortKey;
-  }
-
   public Account.Id getOwner() {
     return owner;
   }
@@ -477,6 +557,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,8 +570,20 @@
   }
 
   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() {
@@ -506,20 +602,4 @@
   public void setTopic(String topic) {
     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;
-  }
 }
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..d8fb115 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
@@ -249,6 +249,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..ce4387c 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
@@ -130,6 +130,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..a17f908 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
@@ -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..9fdb560 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,22 @@
     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();
+  }
+
   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..ec46638 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
@@ -24,6 +24,7 @@
 import com.google.gwtorm.server.ResultSet;
 
 public interface ChangeAccess extends Access<Change, Change.Id> {
+  @Override
   @PrimaryKey("changeId")
   Change get(Change.Id id) throws OrmException;
 
@@ -54,11 +55,6 @@
   @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..59e6934 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
@@ -75,7 +75,7 @@
 
 --    covers:             byProjectOpenAll
 CREATE INDEX changes_byProjectOpen
-ON changes (open, dest_project_name, sort_key);
+ON changes (open, dest_project_name, last_updated_on);
 
 --    covers:             byProject
 CREATE INDEX changes_byProject
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..de86ff6 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
@@ -83,7 +83,7 @@
 
 --    covers:             byProjectOpenPrev, byProjectOpenNext
 CREATE INDEX changes_byProjectOpen
-ON changes (open, dest_project_name, sort_key)
+ON changes (open, dest_project_name, last_updated_on);
 #
 
 --    covers:             byProject
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..ef50f24 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
@@ -124,7 +124,7 @@
 
 --    covers:             byProjectOpenAll
 CREATE INDEX changes_byProjectOpen
-ON changes (dest_project_name, sort_key)
+ON changes (dest_project_name, last_updated_on)
 WHERE open = 'Y';
 
 --    covers:             byProject
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/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..f3eb0a0 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,12 @@
 
 package com.google.gerrit.audit;
 
-import com.google.common.base.Objects;
+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 {
 
@@ -87,12 +87,12 @@
       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.params = MoreObjects.firstNonNull(params, EMPTY_PARAMS);
     this.uuid = new UUID();
     this.result = result;
     this.elapsed = TimeUtil.nowMs() - timeAtStart;
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..179497e 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,7 @@
 
 package com.google.gerrit.common;
 
+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 +28,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;
@@ -41,6 +42,7 @@
 import com.google.gerrit.server.events.CommentAddedEvent;
 import com.google.gerrit.server.events.DraftPublishedEvent;
 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 +75,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;
@@ -98,9 +101,9 @@
 
     private static class ChangeListenerHolder {
         final ChangeListener listener;
-        final IdentifiedUser user;
+        final CurrentUser user;
 
-        ChangeListenerHolder(ChangeListener l, IdentifiedUser u) {
+        ChangeListenerHolder(ChangeListener l, CurrentUser u) {
             listener = l;
             user = u;
         }
@@ -134,6 +137,7 @@
         return output;
       }
 
+      @Override
       public String toString() {
         StringBuilder sb = new StringBuilder();
 
@@ -197,6 +201,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,7 +244,6 @@
       final ProjectCache projectCache,
       final AccountCache accountCache,
       final EventFactory eventFactory,
-      final SitePaths sitePaths,
       final DynamicSet<ChangeListener> unrestrictedListeners) {
         this.anonymousCowardName = anonymousCowardName;
         this.repoManager = repoManager;
@@ -262,6 +268,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,10 +276,12 @@
               .build());
     }
 
-    public void addChangeListener(ChangeListener listener, IdentifiedUser user) {
+    @Override
+    public void addChangeListener(ChangeListener listener, CurrentUser user) {
         listeners.put(listener, new ChangeListenerHolder(listener, user));
     }
 
+    @Override
     public void removeChangeListener(ChangeListener listener) {
         listeners.remove(listener);
     }
@@ -317,6 +326,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 +337,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 +347,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 +375,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 +401,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,6 +445,7 @@
         runHook(change.getProject(), commentAddedHook, args);
     }
 
+    @Override
     public void doChangeMergedHook(final Change change, final Account account,
           final PatchSet patchSet, final ReviewDb db) throws OrmException {
         final ChangeMergedEvent event = new ChangeMergedEvent();
@@ -463,6 +469,7 @@
         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 +496,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 +523,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 +550,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 +577,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 +599,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 +623,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<>();
@@ -659,7 +720,7 @@
       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 +729,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 +794,7 @@
   }
 
   private HookResult runSyncHook(Project.NameKey project,
-      File hook, List<String> args) throws TimeoutException {
+      File hook, List<String> args) {
 
     if (!hook.exists()) {
       return null;
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..550b5f0 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,7 +22,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.events.ChangeEvent;
 import com.google.gwtorm.server.OrmException;
 
@@ -30,10 +30,11 @@
 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 addChangeListener(ChangeListener listener, CurrentUser user);
 
   public void removeChangeListener(ChangeListener listener);
 
@@ -173,6 +174,20 @@
        Account uploader, ObjectId oldId, ObjectId newId);
 
   /**
+   * 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 doHashtagsChangedHook(Change change, Account account,
+      Set<String>added, Set<String> removed, Set<String> hashtags,
+      ReviewDb db) throws OrmException;
+
+  /**
    * Post a stream event that is related to a change
    *
    * @param change The change that the event is related to
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..27b54bc 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,24 @@
 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.CurrentUser;
 import com.google.gerrit.server.events.ChangeEvent;
 
 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 {
   @Override
-  public void addChangeListener(ChangeListener listener, IdentifiedUser user) {
+  public void addChangeListener(ChangeListener listener, CurrentUser user) {
   }
 
   @Override
@@ -78,13 +78,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,6 +98,11 @@
   }
 
   @Override
+  public void doHashtagsChangedHook(Change change, Account account, Set<String> added,
+      Set<String> removed, Set<String> hashtags, ReviewDb db) {
+  }
+
+  @Override
   public void removeChangeListener(ChangeListener 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..02ad0c4 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
@@ -150,8 +150,7 @@
   }
 
   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());
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..3e169f3 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
@@ -30,6 +30,7 @@
 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;
@@ -46,7 +47,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;
@@ -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..b3c4709 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,13 +15,12 @@
 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 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;
@@ -30,6 +29,7 @@
 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,7 +38,6 @@
 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;
@@ -46,7 +45,6 @@
 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 +80,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 +136,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 +151,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);
@@ -247,7 +212,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();
@@ -371,18 +336,14 @@
     }
   }
 
-  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");
     }
@@ -397,7 +358,7 @@
       RevWalk revWalk = new RevWalk(git);
       try {
         RevCommit commit =
-            revWalk.parseCommit(ObjectId.fromString(originalPS.getRevision()
+            revWalk.parseCommit(ObjectId.fromString(ps.getRevision()
                 .get()));
         if (commit.getFullMessage().equals(message)) {
           throw new InvalidChangeOperationException(
@@ -405,7 +366,6 @@
         }
 
         Date now = myIdent.getWhen();
-        Change change = db.get().changes().get(changeId);
         PersonIdent authorIdent =
             user().newCommitterIdent(now, myIdent.getTimeZone());
 
@@ -439,9 +399,8 @@
             .create(git, revWalk, ctl, newCommit)
             .setPatchSet(newPatchSet)
             .setMessage(msg)
-            .setCopyLabels(true)
             .setValidatePolicy(RECEIVE_COMMITS)
-            .setDraft(originalPS.isDraft())
+            .setDraft(ps.isDraft())
             .insert();
 
         return change.getId();
@@ -453,19 +412,44 @@
     }
   }
 
-  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);
     }
 
+    Repository git;
+    try {
+      git = gitManager.openRepository(change.getProject());
+    } catch (RepositoryNotFoundException e) {
+      throw new NoSuchChangeException(changeId, e);
+    }
+    try {
+      RevWalk revWalk = new RevWalk(git);
+      try {
+        RevCommit commit =
+            revWalk.parseCommit(ObjectId.fromString(ps.getRevision()
+                .get()));
+        return commit.getFullMessage();
+      } finally {
+        revWalk.release();
+      }
+    } finally {
+      git.close();
+    }
+  }
+
+  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 +458,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 +492,59 @@
     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("~")) {
+      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
+    Optional<ChangeTriplet> triplet = ChangeTriplet.parse(id);
+    if (triplet.isPresent()) {
+      return db.get().changes().byBranchKey(
+          triplet.get().branch(),
+          triplet.get().id()).toList();
+    }
+
+    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/MimeUtilFileTypeRegistry.java b/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java
index 5263c6b..d0cfea0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/MimeUtilFileTypeRegistry.java
@@ -92,6 +92,7 @@
     return mimeType.getSpecificity();
   }
 
+  @Override
   @SuppressWarnings("unchecked")
   public MimeType getMimeType(final String path, final byte[] content) {
     Set<MimeType> mimeTypes = new HashSet<>();
@@ -122,6 +123,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/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..173c4bd 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).getPathSetWebLink(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);
+  }
+}
\ No newline at end of file
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-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..c981755 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,11 +25,9 @@
 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.SchemaFactory;
 import com.google.inject.Inject;
@@ -53,6 +53,7 @@
   private final ChangeUserName.Factory changeUserNameFactory;
   private final ProjectCache projectCache;
   private final AtomicBoolean awaitsFirstAccountCheck;
+  private final AuditService auditService;
 
   @Inject
   AccountManager(final SchemaFactory<ReviewDb> schema,
@@ -60,7 +61,8 @@
       final Realm accountMapper,
       final IdentifiedUser.GenericFactory userFactory,
       final ChangeUserName.Factory changeUserNameFactory,
-      final ProjectCache projectCache) throws OrmException {
+      final ProjectCache projectCache,
+      final AuditService auditService) {
     this.schema = schema;
     this.byIdCache = byIdCache;
     this.byEmailCache = byEmailCache;
@@ -69,6 +71,7 @@
     this.changeUserNameFactory = changeUserNameFactory;
     this.projectCache = projectCache;
     this.awaitsFirstAccountCheck = new AtomicBoolean(true);
+    this.auditService = auditService;
   }
 
   /**
@@ -227,8 +230,7 @@
       final AccountGroup.Id adminId = g.getId();
       final 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));
     }
 
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..2a961c3 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;
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..903d388 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.common.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..914f159 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
@@ -23,11 +23,11 @@
 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;
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/GroupControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
index ede2431..a3a2809 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
@@ -150,11 +150,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 +166,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/GroupIncludeCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java
index d130243..6ba6bf0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java
@@ -21,14 +21,14 @@
 /** Tracks group inclusions in memory for efficient access. */
 public interface GroupIncludeCache {
   /** @return groups directly a member of the passed group. */
-  public Set<AccountGroup.UUID> membersOf(AccountGroup.UUID group);
+  public Set<AccountGroup.UUID> subgroupsOf(AccountGroup.UUID group);
 
   /** @return any groups the passed group belongs to. */
-  public Set<AccountGroup.UUID> memberIn(AccountGroup.UUID groupId);
+  public Set<AccountGroup.UUID> parentGroupsOf(AccountGroup.UUID groupId);
 
   /** @return set of any UUIDs that are not internal groups. */
   public Set<AccountGroup.UUID> allExternalMembers();
 
-  public void evictMembersOf(AccountGroup.UUID groupId);
-  public void evictMemberIn(AccountGroup.UUID groupId);
+  public void evictSubgroupsOf(AccountGroup.UUID groupId);
+  public void evictParentGroupsOf(AccountGroup.UUID groupId);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index 37d407c..9e7918d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -42,23 +42,23 @@
 public class GroupIncludeCacheImpl implements GroupIncludeCache {
   private static final Logger log = LoggerFactory
       .getLogger(GroupIncludeCacheImpl.class);
-  private static final String BYINCLUDE_NAME = "groups_byinclude";
-  private static final String MEMBERS_NAME = "groups_members";
+  private static final String PARENT_GROUPS_NAME = "groups_byinclude";
+  private static final String SUBGROUPS_NAME = "groups_members";
   private static final String EXTERNAL_NAME = "groups_external";
 
   public static Module module() {
     return new CacheModule() {
       @Override
       protected void configure() {
-        cache(BYINCLUDE_NAME,
+        cache(PARENT_GROUPS_NAME,
             AccountGroup.UUID.class,
             new TypeLiteral<Set<AccountGroup.UUID>>() {})
-          .loader(MemberInLoader.class);
+          .loader(ParentGroupsLoader.class);
 
-        cache(MEMBERS_NAME,
+        cache(SUBGROUPS_NAME,
             AccountGroup.UUID.class,
             new TypeLiteral<Set<AccountGroup.UUID>>() {})
-          .loader(MembersOfLoader.class);
+          .loader(SubgroupsLoader.class);
 
         cache(EXTERNAL_NAME,
             String.class,
@@ -71,24 +71,24 @@
     };
   }
 
-  private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> membersOf;
-  private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> memberIn;
+  private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> subgroups;
+  private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> parentGroups;
   private final LoadingCache<String, Set<AccountGroup.UUID>> external;
 
   @Inject
   GroupIncludeCacheImpl(
-      @Named(MEMBERS_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> membersOf,
-      @Named(BYINCLUDE_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> memberIn,
+      @Named(SUBGROUPS_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> subgroups,
+      @Named(PARENT_GROUPS_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> parentGroups,
       @Named(EXTERNAL_NAME) LoadingCache<String, Set<AccountGroup.UUID>> external) {
-    this.membersOf = membersOf;
-    this.memberIn = memberIn;
+    this.subgroups = subgroups;
+    this.parentGroups = parentGroups;
     this.external = external;
   }
 
   @Override
-  public Set<AccountGroup.UUID> membersOf(AccountGroup.UUID groupId) {
+  public Set<AccountGroup.UUID> subgroupsOf(AccountGroup.UUID groupId) {
     try {
-      return membersOf.get(groupId);
+      return subgroups.get(groupId);
     } catch (ExecutionException e) {
       log.warn("Cannot load members of group", e);
       return Collections.emptySet();
@@ -96,9 +96,9 @@
   }
 
   @Override
-  public Set<AccountGroup.UUID> memberIn(AccountGroup.UUID groupId) {
+  public Set<AccountGroup.UUID> parentGroupsOf(AccountGroup.UUID groupId) {
     try {
-      return memberIn.get(groupId);
+      return parentGroups.get(groupId);
     } catch (ExecutionException e) {
       log.warn("Cannot load included groups", e);
       return Collections.emptySet();
@@ -106,16 +106,16 @@
   }
 
   @Override
-  public void evictMembersOf(AccountGroup.UUID groupId) {
+  public void evictSubgroupsOf(AccountGroup.UUID groupId) {
     if (groupId != null) {
-      membersOf.invalidate(groupId);
+      subgroups.invalidate(groupId);
     }
   }
 
   @Override
-  public void evictMemberIn(AccountGroup.UUID groupId) {
+  public void evictParentGroupsOf(AccountGroup.UUID groupId) {
     if (groupId != null) {
-      memberIn.invalidate(groupId);
+      parentGroups.invalidate(groupId);
 
       if (!AccountGroup.isInternalGroup(groupId)) {
         external.invalidate(EXTERNAL_NAME);
@@ -133,12 +133,12 @@
     }
   }
 
-  static class MembersOfLoader extends
+  static class SubgroupsLoader extends
       CacheLoader<AccountGroup.UUID, Set<AccountGroup.UUID>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
-    MembersOfLoader(final SchemaFactory<ReviewDb> sf) {
+    SubgroupsLoader(final SchemaFactory<ReviewDb> sf) {
       schema = sf;
     }
 
@@ -163,12 +163,12 @@
     }
   }
 
-  static class MemberInLoader extends
+  static class ParentGroupsLoader extends
       CacheLoader<AccountGroup.UUID, Set<AccountGroup.UUID>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
-    MemberInLoader(final SchemaFactory<ReviewDb> sf) {
+    ParentGroupsLoader(final SchemaFactory<ReviewDb> sf) {
       schema = sf;
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
index 2e278c3..b8a67ff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
@@ -90,7 +90,7 @@
         }
 
         memberOf.put(id, false);
-        if (search(includeCache.membersOf(id))) {
+        if (search(includeCache.subgroupsOf(id))) {
           memberOf.put(id, true);
           return true;
         }
@@ -131,7 +131,7 @@
 
     while (!q.isEmpty()) {
       AccountGroup.UUID id = q.remove(q.size() - 1);
-      for (AccountGroup.UUID g : includeCache.memberIn(id)) {
+      for (AccountGroup.UUID g : includeCache.parentGroupsOf(id)) {
         if (g != null && r.add(g)) {
           q.add(g);
           memberOf.put(g, true);
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..700fd76 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,13 +69,17 @@
       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()) {
@@ -88,6 +98,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/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 a54a97b..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;
   }
 
   /**
@@ -116,7 +116,7 @@
 
     if (createGroupArgs.initialGroups != null) {
       addGroups(groupId, createGroupArgs.initialGroups);
-      groupIncludeCache.evictMembersOf(uuid);
+      groupIncludeCache.evictSubgroupsOf(uuid);
     }
 
     groupCache.onCreateGroup(createGroupArgs.getGroup());
@@ -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,21 +143,16 @@
   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.evictMemberIn(uuid);
+      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..7972efa 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.common.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..a5e02d2 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
@@ -28,11 +28,11 @@
 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;
@@ -95,8 +95,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();
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/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..9dcf97b 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,6 +19,8 @@
 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;
@@ -29,7 +31,12 @@
 import com.google.gerrit.server.change.Abandon;
 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;
@@ -40,6 +47,7 @@
 
 import java.io.IOException;
 import java.util.EnumSet;
+import java.util.Set;
 
 class ChangeApiImpl extends ChangeApi.NotImplemented implements ChangeApi {
   interface Factory {
@@ -53,8 +61,13 @@
   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;
 
   @Inject
   ChangeApiImpl(Changes changeApi,
@@ -63,8 +76,13 @@
       Abandon abandon,
       Revert revert,
       Restore restore,
-      Provider<PostReviewers> postReviewers,
+      GetTopic getTopic,
+      PutTopic putTopic,
+      PostReviewers postReviewers,
       Provider<ChangeJson> changeJson,
+      PostHashtags postHashtags,
+      GetHashtags getHashtags,
+      Check check,
       @Assisted ChangeResource change) {
     this.changeApi = changeApi;
     this.revert = revert;
@@ -72,8 +90,13 @@
     this.revisionApi = revisionApi;
     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.change = change;
   }
 
@@ -145,6 +168,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,7 +193,7 @@
   @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);
     }
@@ -164,8 +203,7 @@
   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 +211,47 @@
 
   @Override
   public ChangeInfo get() throws RestApiException {
-    return get(EnumSet.allOf(ListChangesOption.class));
+    return get(EnumSet.complementOf(EnumSet.of(ListChangesOption.CHECK)));
   }
 
   @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..bf113a3 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,7 +19,6 @@
 
 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.common.ChangeInfo;
@@ -30,7 +29,6 @@
 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/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index c709b34..566b298 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
@@ -22,6 +22,7 @@
 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.MergeableInfo;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -29,6 +30,7 @@
 import com.google.gerrit.server.change.DeleteDraftPatchSet;
 import com.google.gerrit.server.change.FileResource;
 import com.google.gerrit.server.change.Files;
+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.Rebase;
@@ -62,6 +64,7 @@
   private final Provider<Files> files;
   private final Provider<Files.ListFiles> listFiles;
   private final Provider<PostReview> review;
+  private final Provider<Mergeable> mergeable;
 
   @Inject
   RevisionApiImpl(Changes changes,
@@ -76,6 +79,7 @@
       Provider<Files> files,
       Provider<Files.ListFiles> listFiles,
       Provider<PostReview> review,
+      Provider<Mergeable> mergeable,
       @Assisted RevisionResource r) {
     this.changes = changes;
     this.cherryPick = cherryPick;
@@ -89,6 +93,7 @@
     this.putReviewed = putReviewed;
     this.deleteReviewed = deleteReviewed;
     this.listFiles = listFiles;
+    this.mergeable = mergeable;
     this.revision = r;
   }
 
@@ -186,4 +191,24 @@
       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);
+    }
+  }
 }
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 5a19814..d62b1a5 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
@@ -17,6 +17,7 @@
 import com.google.common.base.Throwables;
 import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountException;
@@ -37,6 +38,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
@@ -58,7 +60,43 @@
 @Singleton class Helper {
   static final String LDAP_UUID = "ldap:";
 
-  private final Cache<String, ImmutableSet<String>> groupsByInclude;
+  static private Map<String, String> getPoolProperties(Config config) {
+    if (LdapRealm.optional(config, "useConnectionPooling", false)) {
+      Map<String, String> r = Maps.newHashMap();
+      r.put("com.sun.jndi.ldap.connect.pool", "true");
+
+      String connectTimeout = LdapRealm.optional(config, "connectTimeout");
+      String poolDebug = LdapRealm.optional(config, "poolDebug");
+      String poolTimeout = LdapRealm.optional(config, "poolTimeout");
+
+      if (connectTimeout != null) {
+        r.put("com.sun.jndi.ldap.connect.timeout", Long.toString(ConfigUtil
+            .getTimeUnit(connectTimeout, 0, TimeUnit.MILLISECONDS)));
+      }
+      r.put("com.sun.jndi.ldap.connect.pool.authentication",
+          LdapRealm.optional(config, "poolAuthentication", "none simple"));
+      if (poolDebug != null) {
+        r.put("com.sun.jndi.ldap.connect.pool.debug", poolDebug);
+      }
+      r.put("com.sun.jndi.ldap.connect.pool.initsize",
+          String.valueOf(LdapRealm.optional(config, "poolInitsize", 1)));
+      r.put("com.sun.jndi.ldap.connect.pool.maxsize",
+          String.valueOf(LdapRealm.optional(config, "poolMaxsize", 0)));
+      r.put("com.sun.jndi.ldap.connect.pool.prefsize",
+          String.valueOf(LdapRealm.optional(config, "poolPrefsize", 0)));
+      r.put("com.sun.jndi.ldap.connect.pool.protocol",
+          LdapRealm.optional(config, "poolProtocol", "plain"));
+      if (poolTimeout != null) {
+        r.put("com.sun.jndi.ldap.connect.pool.timeout", Long
+            .toString(ConfigUtil.getTimeUnit(poolTimeout, 0,
+                TimeUnit.MILLISECONDS)));
+      }
+      return r;
+    }
+    return null;
+  }
+
+  private final Cache<String, ImmutableSet<String>> parentGroups;
   private final Config config;
   private final String server;
   private final String username;
@@ -68,18 +106,20 @@
   private final String authentication;
   private volatile LdapSchema ldapSchema;
   private final String readTimeOutMillis;
+  private final Map<String, String> connectionPoolConfig;
 
   @Inject
   Helper(@GerritServerConfig final Config config,
-      @Named(LdapModule.GROUPS_BYINCLUDE_CACHE)
-      Cache<String, ImmutableSet<String>> groupsByInclude) {
+      @Named(LdapModule.PARENT_GROUPS_CACHE)
+      Cache<String, ImmutableSet<String>> parentGroups) {
     this.config = config;
     this.server = LdapRealm.optional(config, "server");
     this.username = LdapRealm.optional(config, "username");
-    this.password = LdapRealm.optional(config, "password");
-    this.referral = LdapRealm.optional(config, "referral");
+    this.password = LdapRealm.optional(config, "password", "");
+    this.referral = LdapRealm.optional(config, "referral", "ignore");
     this.sslVerify = config.getBoolean("ldap", "sslverify", true);
-    this.authentication = LdapRealm.optional(config, "authentication");
+    this.authentication =
+        LdapRealm.optional(config, "authentication", "simple");
     String timeout = LdapRealm.optional(config, "readTimeout");
     if (timeout != null) {
       readTimeOutMillis =
@@ -88,7 +128,8 @@
     } else {
       readTimeOutMillis = null;
     }
-    this.groupsByInclude = groupsByInclude;
+    this.parentGroups = parentGroups;
+    this.connectionPoolConfig = getPoolProperties(config);
   }
 
   private Properties createContextProperties() {
@@ -107,14 +148,17 @@
 
   DirContext open() throws NamingException, LoginException {
     final Properties env = createContextProperties();
-    env.put(Context.SECURITY_AUTHENTICATION, authentication != null ? authentication : "simple");
-    env.put(Context.REFERRAL, referral != null ? referral : "ignore");
+    if (connectionPoolConfig != null) {
+      env.putAll(connectionPoolConfig);
+    }
+    env.put(Context.SECURITY_AUTHENTICATION, authentication);
+    env.put(Context.REFERRAL, referral);
     if ("GSSAPI".equals(authentication)) {
       return kerberosOpen(env);
     } else {
       if (username != null) {
         env.put(Context.SECURITY_PRINCIPAL, username);
-        env.put(Context.SECURITY_CREDENTIALS, password != null ? password : "");
+        env.put(Context.SECURITY_CREDENTIALS, password);
       }
       return new InitialDirContext(env);
     }
@@ -146,8 +190,8 @@
     final Properties env = createContextProperties();
     env.put(Context.SECURITY_AUTHENTICATION, "simple");
     env.put(Context.SECURITY_PRINCIPAL, dn);
-    env.put(Context.SECURITY_CREDENTIALS, password != null ? password : "");
-    env.put(Context.REFERRAL, referral != null ? referral : "ignore");
+    env.put(Context.SECURITY_CREDENTIALS, password);
+    env.put(Context.REFERRAL, referral);
     try {
       return new InitialDirContext(env);
     } catch (NamingException e) {
@@ -191,7 +235,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<>();
 
@@ -259,8 +303,8 @@
   private void recursivelyExpandGroups(final Set<String> groupDNs,
       final LdapSchema schema, final DirContext ctx, final String groupDN) {
     if (groupDNs.add(groupDN) && schema.accountMemberField != null) {
-      ImmutableSet<String> cachedGroupDNs = groupsByInclude.getIfPresent(groupDN);
-      if (cachedGroupDNs == null) {
+      ImmutableSet<String> cachedParentsDNs = parentGroups.getIfPresent(groupDN);
+      if (cachedParentsDNs == null) {
         // Recursively identify the groups it is a member of.
         ImmutableSet.Builder<String> dns = ImmutableSet.builder();
         try {
@@ -279,10 +323,10 @@
         } catch (NamingException e) {
           LdapRealm.log.warn("Could not find group " + groupDN, e);
         }
-        cachedGroupDNs = dns.build();
-        groupsByInclude.put(groupDN, cachedGroupDNs);
+        cachedParentsDNs = dns.build();
+        parentGroups.put(groupDN, cachedParentsDNs);
       }
-      for (String dn : cachedGroupDNs) {
+      for (String dn : cachedParentsDNs) {
         recursivelyExpandGroups(groupDNs, schema, ctx, dn);
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
index 88cf45b..eaaafd6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
@@ -33,7 +33,7 @@
   static final String USERNAME_CACHE = "ldap_usernames";
   static final String GROUP_CACHE = "ldap_groups";
   static final String GROUP_EXIST_CACHE = "ldap_group_existence";
-  static final String GROUPS_BYINCLUDE_CACHE = "ldap_groups_byinclude";
+  static final String PARENT_GROUPS_CACHE = "ldap_groups_byinclude";
 
 
   @Override
@@ -55,7 +55,7 @@
       .expireAfterWrite(1, HOURS)
       .loader(LdapRealm.ExistenceLoader.class);
 
-    cache(GROUPS_BYINCLUDE_CACHE,
+    cache(PARENT_GROUPS_CACHE,
         String.class,
         new TypeLiteral<ImmutableSet<String>>() {})
       .expireAfterWrite(1, HOURS);
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 84b5277..66da633 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";
@@ -99,13 +98,29 @@
   }
 
   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) {
     return config.getString("ldap", null, name);
   }
 
+  static int optional(Config config, String name, int defaultValue) {
+    return config.getInt("ldap", name, defaultValue);
+  }
+
+  static String optional(Config config, String name, String defaultValue) {
+    final String v = optional(config, name);
+    if (Strings.isNullOrEmpty(v)) {
+      return defaultValue;
+    }
+    return v;
+  }
+
+  static boolean optional(Config config, String name, boolean defaultValue) {
+    return config.getBoolean("ldap", name, defaultValue);
+  }
+
   static String required(final Config config, final String name) {
     final String v = optional(config, name);
     if (v == null || "".equals(v)) {
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/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..a2e4604
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
@@ -0,0 +1,530 @@
+// 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.gerrit.common.Nullable;
+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.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.kohsuke.args4j.Option;
+
+import java.io.IOException;
+
+@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
+  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;
+    }
+
+    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 && !Strings.isNullOrEmpty(input.restorePath)) {
+        editModifier.restoreFile(edit.get(), input.restorePath);
+      }
+      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.getChangeEdit().getChange().getProject(),
+              rsrc.getChangeEdit().getRevision().get(),
+              rsrc.getPath()));
+      } catch (ResourceNotFoundException rnfe) {
+        return Response.none();
+      }
+    }
+  }
+
+  @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()) {
+        return BinaryResult.create(
+            edit.get().getEditCommit().getFullMessage()).base64();
+      }
+      throw new ResourceNotFoundException();
+    }
+  }
+
+  @Singleton
+  public static class GetType implements RestReadView<ChangeEditResource> {
+    private final FileContentUtil fileContentUtil;
+
+    @Inject
+    GetType(FileContentUtil fileContentUtil) {
+      this.fileContentUtil = fileContentUtil;
+    }
+
+    @Override
+    public String apply(ChangeEditResource rsrc)
+        throws ResourceNotFoundException, IOException {
+      return fileContentUtil.getContentType(
+          rsrc.getChangeEdit().getChange().getProject(),
+          rsrc.getChangeEdit().getRevision().get(),
+          rsrc.getPath());
+    }
+  }
+}
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..fc6da27 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,7 @@
 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.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -29,12 +30,18 @@
 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.auth.AuthException;
 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.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;
@@ -64,8 +71,11 @@
   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 Change change;
@@ -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,8 +100,11 @@
       ChangeHooks hooks,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
-      MergeabilityChecker mergeabilityChecker,
+      ChangeIndexer indexer,
       CreateChangeSender.Factory createChangeSenderFactory,
+      HashtagsUtil hashtagsUtil,
+      AccountCache accountCache,
+      WorkQueue workQueue,
       @Assisted RefControl refControl,
       @Assisted Change change,
       @Assisted RevCommit commit) {
@@ -99,14 +114,18 @@
     this.hooks = hooks;
     this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
-    this.mergeabilityChecker = mergeabilityChecker;
+    this.indexer = indexer;
     this.createChangeSenderFactory = createChangeSenderFactory;
+    this.hashtagsUtil = hashtagsUtil;
+    this.accountCache = accountCache;
+    this.workQueue = workQueue;
     this.refControl = refControl;
     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;
   }
@@ -183,7 +211,7 @@
       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;
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..e69f90c 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
@@ -17,6 +17,7 @@
 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.CHECK;
 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;
@@ -30,12 +31,16 @@
 import static com.google.gerrit.extensions.common.ListChangesOption.REVIEWED;
 import static com.google.gerrit.extensions.common.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,17 +49,25 @@
 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.api.changes.FixInput;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ActionInfo;
+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.LabelInfo;
 import com.google.gerrit.extensions.common.ListChangesOption;
+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;
@@ -62,6 +75,7 @@
 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.PrivateInternals_UiActionDescription;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -70,6 +84,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.PatchSetInfo.ParentInfo;
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -77,8 +92,9 @@
 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.account.AccountLoader;
 import com.google.gerrit.server.extensions.webui.UiActions;
 import com.google.gerrit.server.git.LabelNormalizer;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -86,9 +102,10 @@
 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 +125,7 @@
 
 public class ChangeJson {
   private static final Logger log = LoggerFactory.getLogger(ChangeJson.class);
+
   private static final List<ChangeMessage> NO_MESSAGES =
       ImmutableList.of();
 
@@ -119,16 +137,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 AccountInfo.Loader accountLoader;
+  private AccountLoader accountLoader;
+  private FixInput fix;
 
   @Inject
   ChangeJson(
@@ -137,17 +158,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) {
     this.db = db;
     this.labelNormalizer = ln;
     this.userProvider = user;
@@ -163,6 +185,8 @@
     this.revisions = revisions;
     this.webLinks = webLinks;
     this.cmUtil = cmUtil;
+    this.plcUtil = plcUtil;
+    this.checkerProvider = checkerProvider;
     options = EnumSet.noneOf(ListChangesOption.class);
   }
 
@@ -176,6 +200,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 +214,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 +232,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 +254,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 +278,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 +294,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 +317,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 +396,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) {
@@ -326,36 +427,36 @@
           userProvider)) {
         out.actions.put(d.getId(), new ActionInfo(d));
       }
+      if (userProvider.get().isIdentifiedUser()
+          && in.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.actions.put(descr.getId(), new ActionInfo(descr));
+      }
     }
+
     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 +466,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 +501,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 +527,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 +536,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 +548,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 +581,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 +600,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 +628,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 +644,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 +662,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 +684,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 +703,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 +768,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 +777,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 +838,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,16 +879,17 @@
   }
 
   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.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);
       }
@@ -821,24 +920,15 @@
         && 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 +937,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 +982,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 +1020,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/ChangeResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
index fb8397f..3232f77 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;
@@ -26,6 +26,7 @@
 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;
@@ -68,11 +69,24 @@
           : 0);
 
     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..1648a5d 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,7 +24,7 @@
 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.index.ChangeIndexer;
 import com.google.gerrit.server.project.ChangeControl;
@@ -37,35 +36,34 @@
 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;
 
   @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;
     this.user = user;
     this.changeControlFactory = changeControlFactory;
     this.queryFactory = queryFactory;
     this.views = views;
+    this.changeUtil = changeUtil;
     this.createChange = createChange;
     this.changeIndexer = changeIndexer;
   }
@@ -83,12 +81,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);
         }
@@ -113,43 +111,10 @@
         IdString.fromUrl(Integer.toString(id.get())));
   }
 
-  public ChangeResource parse(ChangeControl control) throws OrmException {
+  public ChangeResource parse(ChangeControl control) {
     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();
-  }
-
   @SuppressWarnings("unchecked")
   @Override
   public CreateChange post(TopLevelResource parent) throws RestApiException {
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..514d6c8
--- /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.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ListChangesOption;
+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..9cb1128 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,7 +14,8 @@
 
 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;
@@ -27,7 +28,9 @@
 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;
@@ -37,7 +40,6 @@
 import com.google.gerrit.server.project.ProjectState;
 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;
@@ -52,7 +54,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,8 +66,6 @@
 @Singleton
 public class CherryPickChange {
 
-  private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
-
   private final Provider<ReviewDb> db;
   private final GitRepositoryManager gitManager;
   private final TimeZone serverTimeZone;
@@ -95,30 +94,25 @@
     this.mergeUtilFactory = mergeUtilFactory;
   }
 
-  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);
+      throw new NoSuchChangeException(change.getId(), e);
     }
 
     try {
@@ -153,16 +147,15 @@
           cherryPickCommit =
               mergeUtilFactory.create(projectState).createCherryPickFromCommit(git, oi, mergeTip,
                   commitToCherryPick, committerIdent, commitMessage, revWalk);
+        } catch (MergeIdenticalTreeException | MergeConflictException e) {
+          throw new MergeException("Cherry pick failed: " + e.getMessage());
         } finally {
           oi.release();
         }
 
-        if (cherryPickCommit == null) {
-          throw new MergeException("Cherry pick failed");
-        }
-
         Change.Key changeKey;
-        final List<String> idList = cherryPickCommit.getFooterLines(CHANGE_ID);
+        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);
@@ -172,9 +165,8 @@
 
         List<Change> destChanges =
             db.get().changes()
-                .byBranchKey(
-                    new Branch.NameKey(db.get().changes().get(changeId).getProject(),
-                        destRef.getName()), changeKey).toList();
+                .byBranchKey(new Branch.NameKey(project, destRef.getName()),
+                    changeKey).toList();
 
         if (destChanges.size() > 1) {
           throw new InvalidChangeOperationException("Several changes with key "
@@ -183,13 +175,14 @@
         } 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,
+          return insertPatchSet(git, revWalk, destChanges.get(0),
               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);
+          return createNewChange(git, revWalk, changeKey, project,
+              patch.getId(), destRef, cherryPickCommit, refControl,
+              identifiedUser, change.getTopic());
         }
       } finally {
         revWalk.release();
@@ -200,8 +193,8 @@
   }
 
   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,7 +207,6 @@
       .setMessage("Uploaded patch set " + newPatchSetId.get() + ".")
       .setDraft(current.isDraft())
       .setUploader(identifiedUser.getAccountId())
-      .setCopyLabels(true)
       .insert();
     return change.getId();
   }
@@ -222,12 +214,13 @@
   private Change.Id createNewChange(Repository git, RevWalk revWalk,
       Change.Key changeKey, Project.NameKey project, PatchSet.Id patchSetId,
       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);
     PatchSet newPatchSet = ins.getPatchSet();
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
index adc1644..23da838 100644
--- 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
@@ -16,10 +16,11 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.changes.Side;
+import com.google.gerrit.extensions.common.AccountInfo;
 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 com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.server.account.AccountLoader;
 
 import java.sql.Timestamp;
 
@@ -34,7 +35,7 @@
   AccountInfo author;
   CommentRange range;
 
-  CommentInfo(PatchLineComment c, AccountInfo.Loader accountLoader) {
+  CommentInfo(PatchLineComment c, AccountLoader accountLoader) {
     id = Url.encode(c.getKey().get());
     path = c.getKey().getParentKey().getFileName();
     if (c.getSide() == 0) {
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..7197269
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -0,0 +1,334 @@
+// 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.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.git.GitRepositoryManager;
+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.Ref;
+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.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 static abstract 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 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) {
+    this.db = db;
+    this.repoManager = repoManager;
+    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.release();
+      }
+      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 boolean checkPatchSets() {
+    List<PatchSet> all;
+    try {
+      all = db.get().patchSets().byChange(change.getId()).toList();
+    } catch (OrmException e) {
+      return error("Failed to look up patch sets", e);
+    }
+    Function<PatchSet, Integer> toPsId = new Function<PatchSet, Integer>() {
+      @Override
+      public Integer apply(PatchSet in) {
+        return in.getId().get();
+      }
+    };
+    Multimap<ObjectId, PatchSet> bySha = MultimapBuilder.hashKeys(all.size())
+        .treeSetValues(Ordering.natural().onResultOf(toPsId))
+        .build();
+    for (PatchSet ps : all) {
+      ObjectId objId;
+      String rev = ps.getRevision().get();
+      int psNum = ps.getId().get();
+      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);
+
+      RevCommit psCommit = parseCommit(
+          objId, String.format("patch set %d", psNum));
+      if (psCommit == null) {
+        continue;
+      }
+      if (ps.getId().equals(change.currentPatchSetId())) {
+        currPsCommit = psCommit;
+      }
+    }
+
+    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(), toPsId)));
+      }
+    }
+
+    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 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 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..440865c 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,15 @@
 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.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.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
@@ -29,13 +33,12 @@
 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.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,7 +46,6 @@
 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;
@@ -80,6 +82,7 @@
   private final CommitValidators.Factory commitValidatorsFactory;
   private final ChangeInserter.Factory changeInserterFactory;
   private final ChangeJson json;
+  private final ChangeUtil changeUtil;
 
   @Inject
   CreateChange(Provider<ReviewDb> db,
@@ -89,7 +92,8 @@
       ProjectsCollection projectsCollection,
       CommitValidators.Factory commitValidatorsFactory,
       ChangeInserter.Factory changeInserterFactory,
-      ChangeJson json) {
+      ChangeJson json,
+      ChangeUtil changeUtil) {
     this.db = db;
     this.gitManager = gitManager;
     this.serverTimeZone = myIdent.getTimeZone();
@@ -98,13 +102,14 @@
     this.commitValidatorsFactory = commitValidatorsFactory;
     this.changeInserterFactory = changeInserterFactory;
     this.json = json;
+    this.changeUtil = changeUtil;
   }
 
   @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 {
 
     if (Strings.isNullOrEmpty(input.project)) {
       throw new BadRequestException("project must be non-empty");
@@ -148,17 +153,36 @@
     try {
       RevWalk rw = new RevWalk(git);
       try {
-        Ref destRef = git.getRef(refName);
-        if (destRef == null) {
-          throw new UnprocessableEntityException(String.format(
-              "Branch %s does not exist.", refName));
+        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));
+          }
+          parentCommit = destRef.getObjectId();
         }
+        RevCommit mergeTip = rw.parseCommit(parentCommit);
 
         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);
@@ -169,7 +193,7 @@
             getChangeId(id, c),
             new Change.Id(db.get().nextChangeId()),
             me.getAccountId(),
-            new Branch.NameKey(project, destRef.getName()),
+            new Branch.NameKey(project, refName),
             now);
 
         ChangeInserter ins =
@@ -179,7 +203,8 @@
         updateRef(git, rw, c, change, ins.getPatchSet());
 
         change.setTopic(input.topic);
-        change.setStatus(ChangeInfoMapper.changeStatus2Status(input.status));
+        change.setStatus(input.status != null
+            ? Change.Status.forChangeStatus(input.status) : Change.Status.NEW);
         ins.insert();
 
         return Response.created(json.format(change.getId()));
@@ -229,7 +254,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());
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
index 1d2fa406..1cd39be 100644
--- 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
@@ -14,7 +14,10 @@
 
 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.common.changes.Side;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -24,27 +27,40 @@
 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.change.PutDraft.Input;
-import com.google.gerrit.server.util.TimeUtil;
+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
 class CreateDraft implements RestModifyView<RevisionResource, Input> {
   private final Provider<ReviewDb> db;
+  private final ChangeUpdate.Factory updateFactory;
+  private final PatchLineCommentsUtil plcUtil;
+  private final PatchListCache patchListCache;
 
   @Inject
-  CreateDraft(Provider<ReviewDb> db) {
+  CreateDraft(Provider<ReviewDb> db,
+      ChangeUpdate.Factory updateFactory,
+      PatchLineCommentsUtil plcUtil,
+      PatchListCache patchListCache) {
     this.db = db;
+    this.updateFactory = updateFactory;
+    this.plcUtil = plcUtil;
+    this.patchListCache = patchListCache;
   }
 
   @Override
   public Response<CommentInfo> apply(RevisionResource rsrc, Input in)
-      throws BadRequestException, OrmException {
+      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()) {
@@ -59,15 +75,20 @@
         ? in.line
         : in.range != null ? in.range.getEndLine() : 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), TimeUtil.nowTs());
+        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);
-    db.get().patchComments().insert(Collections.singleton(c));
+    setCommentRevId(c, patchListCache, rsrc.getChange(), rsrc.getPatchSet());
+    plcUtil.insertComments(db.get(), update, Collections.singleton(c));
+    update.commit();
     return Response.created(new CommentInfo(c, null));
   }
 }
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
index 46ae834..f7fb300 100644
--- 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
@@ -14,15 +14,22 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
+
 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.DeleteDraft.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
@@ -31,16 +38,30 @@
   }
 
   private final Provider<ReviewDb> db;
+  private final PatchLineCommentsUtil plcUtil;
+  private final ChangeUpdate.Factory updateFactory;
+  private final PatchListCache patchListCache;
 
   @Inject
-  DeleteDraft(Provider<ReviewDb> db) {
+  DeleteDraft(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(DraftResource rsrc, Input input)
-      throws OrmException {
-    db.get().patchComments().delete(Collections.singleton(rsrc.getComment()));
+      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/DeleteDraftChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java
index 28885db..d1742cc 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
@@ -25,7 +25,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 +47,6 @@
 
   @Inject
   public DeleteDraftChange(Provider<ReviewDb> dbProvider,
-      PatchSetInfoFactory patchSetInfoFactory,
       ChangeUtil changeUtil,
       @GerritServerConfig Config cfg) {
     this.dbProvider = dbProvider;
@@ -69,11 +67,11 @@
     }
 
     if (!allowDrafts) {
-      throw new ResourceConflictException("Draft workflow is disabled.");
+      throw new ResourceConflictException("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/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
index 46d0ed9..589b11a 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
@@ -71,11 +71,11 @@
     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 ResourceConflictException("Draft workflow is disabled");
     }
 
     if (!rsrc.getControl().canDeleteDraft(dbProvider.get())) {
@@ -121,7 +121,7 @@
             .patchSets()
             .byChange(change.getId())
             .toList().size() == 0) {
-      deleteDraftChange(patchSetId);
+      deleteDraftChange(change);
     } else {
       if (change.currentPatchSetId().equals(patchSetId)) {
         updateChange(dbProvider.get(), change,
@@ -133,10 +133,10 @@
     }
   }
 
-  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());
     }
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/Drafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java
index 322faea..b0ed7d0 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/Drafts.java
@@ -23,6 +23,7 @@
 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;
@@ -34,16 +35,19 @@
   private final Provider<CurrentUser> user;
   private final ListDrafts list;
   private final Provider<ReviewDb> dbProvider;
+  private final PatchLineCommentsUtil plcUtil;
 
   @Inject
   Drafts(DynamicMap<RestView<DraftResource>> views,
       Provider<CurrentUser> user,
       ListDrafts list,
-      Provider<ReviewDb> dbProvider) {
+      Provider<ReviewDb> dbProvider,
+      PatchLineCommentsUtil plcUtil) {
     this.views = views;
     this.user = user;
     this.list = list;
     this.dbProvider = dbProvider;
+    this.plcUtil = plcUtil;
   }
 
   @Override
@@ -62,10 +66,8 @@
       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);
       }
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
index af81627..37b4ce4 100644
--- 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
@@ -15,7 +15,9 @@
 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.common.ChangeInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -25,12 +27,9 @@
 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;
@@ -77,15 +76,14 @@
     try {
       return json.format(changeUtil.editCommitMessage(
           rsrc.getControl(),
-          rsrc.getPatchSet().getId(),
+          rsrc.getPatchSet(),
           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) {
+    } catch (MissingObjectException | IncorrectObjectTypeException e) {
       throw new ResourceConflictException(e.getMessage());
     }
   }
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..5d07402
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -0,0 +1,106 @@
+// 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.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.FileTypeRegistry;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+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 {
+  private final GitRepositoryManager repoManager;
+  private final FileTypeRegistry registry;
+
+  @Inject
+  FileContentUtil(GitRepositoryManager repoManager,
+      FileTypeRegistry ftr) {
+    this.repoManager = repoManager;
+    this.registry = ftr;
+  }
+
+  public BinaryResult getContent(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();
+        }
+        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 {
+        rw.release();
+      }
+    } finally {
+      repo.close();
+    }
+  }
+
+  public String getContentType(Project.NameKey project, String revstr,
+      String path) throws ResourceNotFoundException, IOException {
+    Repository repo = repoManager.openRepository(project);
+    try {
+      RevWalk rw = new RevWalk(repo);
+      ObjectReader reader = repo.newObjectReader();
+      try {
+        RevCommit commit = rw.parseCommit(repo.resolve(revstr));
+        TreeWalk tw =
+            TreeWalk.forPath(rw.getObjectReader(), path,
+                commit.getTree().getId());
+        if (tw == null) {
+          throw new ResourceNotFoundException();
+        }
+        ObjectLoader blobLoader = reader.open(tw.getObjectId(0), OBJ_BLOB);
+        byte[] raw = blobLoader.isLarge()
+            ? null
+            : blobLoader.getCachedBytes();
+        return registry.getMimeType(path, raw).toString();
+      } finally {
+        reader.release();
+        rw.release();
+      }
+    } finally {
+      repo.close();
+    }
+  }
+}
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/Files.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
index 74659b8..3035ce1 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
@@ -141,7 +141,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));
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..a5a1614 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.ChangeInfo;
 import com.google.gerrit.extensions.common.ListChangesOption;
-import com.google.gerrit.extensions.restapi.CacheControl;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.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..96390d6 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
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.change;
 
 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;
@@ -23,16 +23,16 @@
 @Singleton
 class GetComment implements RestReadView<CommentResource> {
 
-  private final AccountInfo.Loader.Factory accountLoaderFactory;
+  private final AccountLoader.Factory accountLoaderFactory;
 
   @Inject
-  GetComment(AccountInfo.Loader.Factory accountLoaderFactory) {
+  GetComment(AccountLoader.Factory accountLoaderFactory) {
     this.accountLoaderFactory = accountLoaderFactory;
   }
 
   @Override
   public CommentInfo apply(CommentResource rsrc) throws OrmException {
-    AccountInfo.Loader accountLoader = accountLoaderFactory.create(true);
+    AccountLoader accountLoader = accountLoaderFactory.create(true);
     CommentInfo ci = new CommentInfo(rsrc.getComment(), accountLoader);
     accountLoader.fill();
     return ci;
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..6f41a86 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
@@ -22,14 +22,17 @@
 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;
@@ -40,7 +43,8 @@
       throws ResourceNotFoundException, 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));
       }
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..67c68fc 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,39 @@
 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 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)) {
+      return BinaryResult.create(
+          changeUtil.getMessage(rsrc.getRevision().getChange())).base64();
     }
+    return fileContentUtil.getContent(
+        rsrc.getRevision().getControl().getProject().getNameKey(),
+        rsrc.getRevision().getPatchSet().getRevision().get(),
+        path);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContentType.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContentType.java
new file mode 100644
index 0000000..2fa3126
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContentType.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.change;
+
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+
+@Singleton
+public class GetContentType implements RestReadView<FileResource> {
+  private final FileContentUtil fileContentUtil;
+
+  @Inject
+  GetContentType(FileContentUtil fileContentUtil) {
+    this.fileContentUtil = fileContentUtil;
+  }
+
+  @Override
+  public String apply(FileResource rsrc)
+      throws ResourceNotFoundException, IOException {
+    String path = rsrc.getPatchKey().get();
+    if (Patch.COMMIT_MSG.equals(path)) {
+      return "text/plain";
+    }
+    return fileContentUtil.getContentType(
+        rsrc.getRevision().getControl().getProject().getNameKey(),
+        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..f9ad6146 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.ChangeInfo;
 import com.google.gerrit.extensions.common.ListChangesOption;
 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..04b1386 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,23 @@
 
 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 +43,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 +66,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 final static 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 +99,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 +132,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 +146,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 +168,70 @@
       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.getDisplayMethodA() != DisplayMethod.NONE) {
+          result.metaA = new FileMeta();
+          result.metaA.name = MoreObjects.firstNonNull(ps.getOldName(),
+              ps.getNewName());
+          setContentType(result.metaA, state, 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();
+          setContentType(result.metaB, state, 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,19 +243,11 @@
     }
   }
 
-  static class Result {
-    FileMeta metaA;
-    FileMeta metaB;
-    IntraLineStatus intralineStatus;
-    ChangeType changeType;
-    List<String> diffHeader;
-    List<ContentEntry> content;
-  }
-
-  static class FileMeta {
-    String name;
-    String contentType;
-    Integer lines;
+  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();
   }
 
   private void setContentType(FileMeta meta, ProjectState project,
@@ -221,12 +278,6 @@
     }
   }
 
-  enum IntraLineStatus {
-    OK,
-    TIMEOUT,
-    FAILURE
-  }
-
   private static class Content {
     final List<ContentEntry> lines;
     final SparseFileContent fileA;
@@ -341,28 +392,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/GetDraft.java
index 12c50ae..8cff9f8 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/GetDraft.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.change;
 
 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;
@@ -23,16 +23,16 @@
 @Singleton
 class GetDraft implements RestReadView<DraftResource> {
 
-  private final AccountInfo.Loader.Factory accountLoaderFactory;
+  private final AccountLoader.Factory accountLoaderFactory;
 
   @Inject
-  GetDraft(AccountInfo.Loader.Factory accountLoaderFactory) {
+  GetDraft(AccountLoader.Factory accountLoaderFactory) {
     this.accountLoaderFactory = accountLoaderFactory;
   }
 
   @Override
   public CommentInfo apply(DraftResource rsrc) throws OrmException {
-    AccountInfo.Loader accountLoader = accountLoaderFactory.create(true);
+    AccountLoader accountLoader = accountLoaderFactory.create(true);
     CommentInfo ci = new CommentInfo(rsrc.getComment(), accountLoader);
     accountLoader.fill();
     return ci;
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/GetRelated.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
index 0ccb15c..6056e5f 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
@@ -20,13 +20,15 @@
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.common.ChangeStatus;
 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.client.RevId;
 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;
@@ -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;
@@ -93,7 +93,7 @@
   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<PatchSet.Id, PatchSet> patchSets = allPatchSets(rsrc, changes.keySet());
 
     Map<String, PatchSet> commits = Maps.newHashMap();
     for (PatchSet p : patchSets.values()) {
@@ -121,6 +121,18 @@
       if (p != null) {
         g = changes.get(p.getId().getParentKey());
         added.add(p.getId().getParentKey());
+      } else {
+        // check if there is a merged or abandoned change for this commit
+        ReviewDb db = dbProvider.get();
+        for (PatchSet ps : db.patchSets().byRevision(new RevId(c.name())).toList()) {
+          Change change = db.changes().get(ps.getId().getParentKey());
+          if (change != null && change.getDest().equals(rsrc.getChange().getDest())) {
+            p = ps;
+            g = change;
+            added.add(g.getId());
+            break;
+          }
+        }
       }
       parents.add(new ChangeAndCommit(g, p, c));
     }
@@ -129,9 +141,7 @@
 
     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();
       }
     }
@@ -145,8 +155,8 @@
         db.changes().byBranchOpenAll(rsrc.getChange().getDest()));
   }
 
-  private Map<PatchSet.Id, PatchSet> allPatchSets(Collection<Change.Id> ids)
-      throws OrmException {
+  private Map<PatchSet.Id, PatchSet> allPatchSets(RevisionResource rsrc,
+      Collection<Change.Id> ids) throws OrmException {
     int n = ids.size();
     ReviewDb db = dbProvider.get();
     List<ResultSet<PatchSet>> t = Lists.newArrayListWithCapacity(n);
@@ -160,6 +170,10 @@
         r.put(p.getId(), p);
       }
     }
+
+    if (rsrc.getEdit().isPresent()) {
+      r.put(rsrc.getPatchSet().getId(), rsrc.getPatchSet());
+    }
     return r;
   }
 
@@ -272,21 +286,13 @@
     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;
   }
 
   public static class ChangeAndCommit {
     public String changeId;
+    public ChangeStatus status;
     public CommitInfo commit;
     public Integer _changeNumber;
     public Integer _revisionNumber;
@@ -295,6 +301,7 @@
     ChangeAndCommit(@Nullable Change change, @Nullable PatchSet ps, RevCommit c) {
       if (change != null) {
         changeId = change.getKey().get();
+        status = change.getStatus().asChangeStatus();
         _changeNumber = change.getChangeId();
         _revisionNumber = ps != null ? ps.getPatchSetId() : null;
         PatchSet.Id curr = change.currentPatchSetId();
@@ -309,7 +316,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..f734302 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.ChangeInfo;
 import com.google.gerrit.extensions.common.ListChangesOption;
 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/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..84fa783
--- /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.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.auth.AuthException;
+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/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..6222d82 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,7 @@
 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.account.AccountLoader;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -26,13 +26,10 @@
 
 @Singleton
 class ListComments extends ListDrafts {
-  private final PatchLineCommentsUtil plcUtil;
-
   @Inject
-  ListComments(Provider<ReviewDb> db, AccountInfo.Loader.Factory alf,
+  ListComments(Provider<ReviewDb> db, AccountLoader.Factory alf,
       PatchLineCommentsUtil plcUtil) {
-    super(db, alf);
-    this.plcUtil = plcUtil;
+    super(db, alf, 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/ListDrafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
index bd3aa04..be047db 100644
--- 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
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.change;
 
-import static com.google.common.base.Objects.firstNonNull;
+import static com.google.common.base.MoreObjects.firstNonNull;
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -22,7 +22,8 @@
 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.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.account.AccountLoader;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -36,20 +37,21 @@
 @Singleton
 class ListDrafts implements RestReadView<RevisionResource> {
   protected final Provider<ReviewDb> db;
-  private final AccountInfo.Loader.Factory accountLoaderFactory;
+  protected final PatchLineCommentsUtil plcUtil;
+  private final AccountLoader.Factory accountLoaderFactory;
 
   @Inject
-  ListDrafts(Provider<ReviewDb> db, AccountInfo.Loader.Factory alf) {
+  ListDrafts(Provider<ReviewDb> db, AccountLoader.Factory alf,
+      PatchLineCommentsUtil plcUtil) {
     this.db = db;
     this.accountLoaderFactory = alf;
+    this.plcUtil = plcUtil;
   }
 
   protected Iterable<PatchLineComment> listComments(RevisionResource rsrc)
       throws OrmException {
-    return db.get().patchComments()
-        .draftByPatchSetAuthor(
-            rsrc.getPatchSet().getId(),
-            rsrc.getAccountId());
+    return plcUtil.draftByPatchSetAuthor(db.get(), rsrc.getPatchSet().getId(),
+        rsrc.getAccountId(), rsrc.getNotes());
   }
 
   protected boolean includeAuthorInfo() {
@@ -60,7 +62,7 @@
   public Map<String, List<CommentInfo>> apply(RevisionResource rsrc)
       throws OrmException {
     Map<String, List<CommentInfo>> out = Maps.newTreeMap();
-    AccountInfo.Loader accountLoader =
+    AccountLoader accountLoader =
         includeAuthorInfo() ? accountLoaderFactory.create(true) : null;
     for (PatchLineComment c : listComments(rsrc)) {
       CommentInfo o = new CommentInfo(c, accountLoader);
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..584a81b
--- /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.common.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..f9192bc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -0,0 +1,315 @@
+// 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.Sets;
+import com.google.gerrit.extensions.common.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.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.Collection;
+import java.util.Map;
+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 {
+        Map<String, Ref> refs = key.load.repo.getAllRefs();
+        RevWalk rw = CodeReviewCommit.newRevWalk(key.load.repo);
+        try {
+          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.values());
+          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 {
+          rw.release();
+        }
+      } finally {
+        key.load = null;
+      }
+    }
+
+    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);
+    }
+  }
+
+  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 481e535..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityChecker.java
+++ /dev/null
@@ -1,367 +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 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.getId(), 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..710f0f84 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,7 +14,8 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.extensions.common.MergeableInfo;
 import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -22,85 +23,66 @@
 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..f89c981 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,6 +14,7 @@
 
 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;
@@ -23,7 +24,7 @@
 
 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;
@@ -44,15 +45,20 @@
     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, "hashtags").to(PostHashtags.class);
     post(CHANGE_KIND, "publish").to(Publish.CurrentRevision.class);
     post(CHANGE_KIND, "restore").to(Restore.class);
     post(CHANGE_KIND, "revert").to(Revert.class);
@@ -97,16 +103,30 @@
     put(FILE_KIND, "reviewed").to(PutReviewed.class);
     delete(FILE_KIND, "reviewed").to(DeleteReviewed.class);
     get(FILE_KIND, "content").to(GetContent.class);
+    get(FILE_KIND, "type").to(GetContentType.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, "type").to(ChangeEdits.GetType.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..98d99a3 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,8 +34,10 @@
 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;
@@ -44,7 +47,6 @@
 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 +55,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 +90,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 +104,6 @@
 
   private PatchSet patchSet;
   private ChangeMessage changeMessage;
-  private boolean copyLabels;
   private SshInfo sshInfo;
   private ValidatePolicy validatePolicy = ValidatePolicy.GERRIT;
   private boolean draft;
@@ -120,7 +122,7 @@
       PatchSetInfoFactory patchSetInfoFactory,
       GitReferenceUpdated gitRefUpdated,
       CommitValidators.Factory commitValidatorsFactory,
-      MergeabilityChecker mergeabilityChecker,
+      ChangeIndexer indexer,
       ReplacePatchSetSender.Factory replacePatchSetFactory,
       @Assisted Repository git,
       @Assisted RevWalk revWalk,
@@ -139,7 +141,7 @@
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.gitRefUpdated = gitRefUpdated;
     this.commitValidatorsFactory = commitValidatorsFactory;
-    this.mergeabilityChecker = mergeabilityChecker;
+    this.indexer = indexer;
     this.replacePatchSetFactory = replacePatchSetFactory;
 
     this.git = git;
@@ -177,17 +179,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 +263,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 +278,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 +308,7 @@
     } finally {
       db.rollback();
     }
-    mergeabilityChecker.newCheck()
-        .addChange(updatedChange)
-        .reindex()
-        .run();
+    indexer.index(db, updatedChange);
     if (runHooks) {
       hooks.doPatchsetCreatedHook(updatedChange, patchSet, db);
     }
@@ -356,7 +346,7 @@
     }
   }
 
-  private void validate() throws InvalidChangeOperationException {
+  private void validate() throws InvalidChangeOperationException, IOException {
     CommitValidators cv =
         commitValidatorsFactory.create(ctl.getRefControl(), sshInfo, git);
 
@@ -372,7 +362,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);
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..fee457c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.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.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());
+    } catch (com.google.gerrit.server.auth.AuthException e) {
+      throw new AuthException(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..18b08b5 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;
@@ -44,7 +46,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 +55,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;
 
@@ -149,7 +146,6 @@
       input.notify = NotifyHandling.NONE;
     }
 
-    ChangeUpdate update = null;
     db.get().changes().beginTransaction(revision.getChange().getId());
     boolean dirty = false;
     try {
@@ -157,7 +153,7 @@
       ChangeUtil.updated(change);
       timestamp = change.getLastUpdatedOn();
 
-      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 +162,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 +289,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();
@@ -337,7 +331,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 +344,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()) {
@@ -378,7 +363,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 +378,7 @@
       }
     }
 
-    switch (Objects.firstNonNull(draftsHandling, DraftHandling.DELETE)) {
+    switch (MoreObjects.firstNonNull(draftsHandling, DraftHandling.DELETE)) {
       case KEEP:
       default:
         break;
@@ -403,13 +389,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 +404,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;
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/Publish.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Publish.java
index 88da094..87ea7ec 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/Publish.java
@@ -31,7 +31,6 @@
 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;
@@ -83,7 +82,7 @@
     }
 
     if (!allowDrafts) {
-      throw new ResourceConflictException("Draft workflow is disabled.");
+      throw new ResourceConflictException("Draft workflow is disabled");
     }
 
     PatchSet updatedPatchSet = updateDraftPatchSet(rsrc);
@@ -91,21 +90,17 @@
     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();
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..b511c20
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.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.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.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.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 IllegalStateException("not yet implemented");
+  }
+
+  @Override
+  public RestView<ChangeResource> list() {
+    throw new IllegalStateException("not yet implemented");
+  }
+
+  @Override
+  public ChangeEditResource parse(ChangeResource parent, IdString id)
+      throws ResourceNotFoundException, Exception {
+    throw new IllegalStateException("not yet implemented");
+  }
+
+  @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/PutDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
index c1fb304..949c0c6 100644
--- 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
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
+
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.changes.Side;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
@@ -24,13 +27,16 @@
 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.change.PutDraft.Input;
-import com.google.gerrit.server.util.TimeUtil;
+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;
 
@@ -51,17 +57,28 @@
 
   private final Provider<ReviewDb> db;
   private final DeleteDraft delete;
+  private final PatchLineCommentsUtil plcUtil;
+  private final ChangeUpdate.Factory updateFactory;
+  private final PatchListCache patchListCache;
 
   @Inject
-  PutDraft(Provider<ReviewDb> db, DeleteDraft delete) {
+  PutDraft(Provider<ReviewDb> db,
+      DeleteDraft delete,
+      PatchLineCommentsUtil plcUtil,
+      ChangeUpdate.Factory updateFactory,
+      PatchListCache patchListCache) {
     this.db = db;
     this.delete = delete;
+    this.plcUtil = plcUtil;
+    this.updateFactory = updateFactory;
+    this.patchListCache = patchListCache;
   }
 
   @Override
   public Response<CommentInfo> apply(DraftResource rsrc, Input in) throws
-      BadRequestException, OrmException {
+      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)) {
@@ -76,7 +93,8 @@
         && !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));
+
+      plcUtil.deleteComments(db.get(), update, Collections.singleton(c));
       c = new PatchLineComment(
           new PatchLineComment.Key(
               new Patch.Key(rsrc.getPatchSet().getId(), in.path),
@@ -84,10 +102,18 @@
           c.getLine(),
           rsrc.getAuthorId(),
           c.getParentUuid(), TimeUtil.nowTs());
-      db.get().patchComments().insert(Collections.singleton(update(c, in)));
+      setCommentRevId(c, patchListCache, rsrc.getChange(), rsrc.getPatchSet());
+      plcUtil.insertComments(db.get(), update,
+          Collections.singleton(update(c, in)));
     } else {
-      db.get().patchComments().update(Collections.singleton(update(c, in)));
+      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(new CommentInfo(c, null));
   }
 
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 6322668..036e9e8 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,6 +15,7 @@
 package com.google.gerrit.server.change;
 
 import com.google.gerrit.common.errors.EmailException;
+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.ResourceConflictException;
@@ -24,7 +25,6 @@
 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.change.Rebase.Input;
 import com.google.gerrit.server.changedetail.RebaseChange;
 import com.google.gerrit.server.project.ChangeControl;
@@ -68,7 +68,8 @@
     }
 
     try {
-      rebaseChange.get().rebase(rsrc.getPatchSet().getId(), rsrc.getUser());
+      rebaseChange.get().rebase(rsrc.getChange(), rsrc.getPatchSet().getId(),
+          rsrc.getUser());
     } catch (InvalidChangeOperationException e) {
       throw new ResourceConflictException(e.getMessage());
     } catch (IOException e) {
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..8fccfb8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java
@@ -0,0 +1,117 @@
+// 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.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.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 IllegalStateException("not yet implemented");
+  }
+
+  @Override
+  public RestView<ChangeResource> list() {
+    throw new IllegalStateException("not yet implemented");
+  }
+
+  @Override
+  public ChangeEditResource parse(ChangeResource parent, IdString id)
+      throws ResourceNotFoundException, Exception {
+    throw new IllegalStateException("not yet implemented");
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public Rebase post(ChangeResource parent) throws RestApiException {
+    return rebase;
+  }
+
+  @Singleton
+  public static class Rebase implements RestModifyView<ChangeResource, Publish.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, Publish.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..83fa938
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.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.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..f32b41c 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,13 @@
 
 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.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 +29,14 @@
 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.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 +44,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
@@ -65,17 +76,24 @@
       }
       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())) {
+      Change.Id changeId = rsrc.getChange().getId();
+      if (changeId.equals(change.getChange().getId())
+          && 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,17 +101,19 @@
     return change.getControl().isPatchVisible(ps, dbProvider.get());
   }
 
-  private List<PatchSet> find(ChangeResource change, String id)
+  private List<RevisionResource> find(ChangeResource change, String id)
       throws OrmException {
     ReviewDb db = dbProvider.get();
 
-    if (id.length() < 6 && id.matches("^[1-9][0-9]{0,4}$")) {
+    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 toResources(change, ps);
       }
       return Collections.emptyList();
     } else if (id.length() < 4 || id.length() > RevId.LEN) {
@@ -107,19 +127,60 @@
       // for all patch sets in the change.
       RevId revid = new RevId(id);
       if (revid.isComplete()) {
-        return db.patchSets().byRevision(revid).toList();
+        List<RevisionResource> list =
+            toResources(change, db.patchSets().byRevision(revid));
+        if (list.isEmpty()) {
+          return loadEdit(change, revid);
+        }
+        return list;
       } else {
-        return db.patchSets().byRevisionRange(revid, revid.max()).toList();
+        return toResources(
+            change, db.patchSets().byRevisionRange(revid, revid.max()));
       }
     } else {
       // Chance of collision rises; look at all patch sets on the change.
-      List<PatchSet> out = Lists.newArrayList();
+      List<RevisionResource> out = Lists.newArrayList();
       for (PatchSet ps : db.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> loadEdit(ChangeResource change, RevId revid)
+      throws OrmException {
+    try {
+      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));
+        }
+      }
+    } catch (AuthException | IOException e) {
+      throw new OrmException(e);
+    }
+    return Collections.emptyList();
+  }
+
+  private static List<RevisionResource> toResources(final ChangeResource change,
+      Iterable<PatchSet> patchSets) {
+    return FluentIterable.from(patchSets)
+        .transform(new Function<PatchSet, RevisionResource>() {
+          @Override
+          public RevisionResource apply(PatchSet in) {
+            return new RevisionResource(change, in);
+          }
+        }).toList();
+  }
+
+  private static List<RevisionResource> toResources(ChangeResource change,
+      PatchSet ps) {
+    return Collections.singletonList(new RevisionResource(change, ps));
+  }
 }
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..dd1f92f 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,7 +16,7 @@
 
 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.Predicate;
 import com.google.common.base.Strings;
@@ -27,9 +27,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;
@@ -45,12 +47,10 @@
 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,7 +59,8 @@
 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.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -102,6 +103,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;
@@ -118,6 +120,7 @@
       Provider<ReviewDb> dbProvider,
       GitRepositoryManager repoManager,
       IdentifiedUser.GenericFactory userFactory,
+      ChangeData.Factory changeDataFactory,
       ChangeUpdate.Factory updateFactory,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
@@ -131,6 +134,7 @@
     this.dbProvider = dbProvider;
     this.repoManager = repoManager;
     this.userFactory = userFactory;
+    this.changeDataFactory = changeDataFactory;
     this.updateFactory = updateFactory;
     this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
@@ -139,10 +143,10 @@
     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));
   }
@@ -266,7 +270,6 @@
             if (change.getStatus().isOpen()) {
               change.setStatus(Change.Status.SUBMITTED);
               change.setLastUpdatedOn(timestamp);
-              ChangeUtil.computeSortKey(change);
               return change;
             }
             return null;
@@ -374,10 +377,12 @@
   }
 
   private List<SubmitRecord> checkSubmitRule(RevisionResource rsrc,
-      boolean force) throws ResourceConflictException {
-    List<SubmitRecord> results = rsrc.getControl().canSubmit(
-        dbProvider.get(),
-        rsrc.getPatchSet());
+      boolean force) throws ResourceConflictException, OrmException {
+    ChangeData cd =
+        changeDataFactory.create(dbProvider.get(), rsrc.getControl());
+    List<SubmitRecord> results = new SubmitRuleEvaluator(cd)
+        .setPatchSet(rsrc.getPatchSet())
+        .canSubmit();
     Optional<SubmitRecord> ok = findOkRecord(results);
     if (ok.isPresent()) {
       // Rules supplied a valid solution.
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..ce22034 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,13 @@
 
 package com.google.gerrit.server.change;
 
-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.common.collect.Lists;
-import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.Url;
@@ -32,7 +32,7 @@
 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;
@@ -48,19 +48,21 @@
 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 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 +74,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 +95,7 @@
 
   @Inject
   SuggestReviewers(AccountVisibility av,
-      AccountInfo.Loader.Factory accountLoaderFactory,
+      AccountLoader.Factory accountLoaderFactory,
       AccountControl.Factory accountControlFactory,
       AccountCache accountCache,
       GroupMembers.Factory groupMembersFactory,
@@ -95,21 +103,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 +135,7 @@
   }
 
   private interface VisibilityControl {
-    boolean isVisibleTo(Account account) throws OrmException;
+    boolean isVisibleTo(Account.Id account) throws OrmException;
   }
 
   @Override
@@ -134,8 +150,12 @@
     }
 
     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) {
@@ -165,16 +185,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 +213,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 +237,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 +330,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;
         }
       }
@@ -287,7 +362,7 @@
 
     private String getSortValue() {
       return account != null
-          ? Objects.firstNonNull(account.email,
+          ? MoreObjects.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..ebe9386 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,9 +14,8 @@
 
 package com.google.gerrit.server.change;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
+import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -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 11e5938..9890f53 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;
@@ -30,11 +31,11 @@
 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;
@@ -95,6 +96,7 @@
    * E-mail notification and triggering of hooks happens for the creation of the
    * new patch set.
    *
+   * @param change the change to perform the rebase for
    * @param patchSetId the id of the patch set
    * @param uploader the user that creates the rebased patch set
    * @throws NoSuchChangeException thrown if the change to which the patch set
@@ -105,18 +107,17 @@
    * @throws IOException thrown if rebase is not possible or not needed
    * @throws InvalidChangeOperationException thrown if rebase is not allowed
    */
-  public void rebase(final PatchSet.Id patchSetId, final IdentifiedUser uploader)
+  public void rebase(Change change, PatchSet.Id patchSetId, final IdentifiedUser uploader)
       throws NoSuchChangeException, EmailException, OrmException, IOException,
       InvalidChangeOperationException {
     final Change.Id changeId = patchSetId.getParentKey();
     final ChangeControl changeControl =
-        changeControlFactory.validateFor(changeId, uploader);
+        changeControlFactory.validateFor(change, uploader);
     if (!changeControl.canRebase()) {
       throw new InvalidChangeOperationException(
           "Cannot rebase: New patch sets are not allowed to be added to change: "
               + changeId.toString());
     }
-    final Change change = changeControl.getChange();
     Repository git = null;
     RevWalk rw = null;
     ObjectInserter inserter = null;
@@ -138,7 +139,7 @@
           uploader, baseCommit, mergeUtilFactory.create(
               changeControl.getProjectControl().getProjectState(), true),
           committerIdent, true, true, ValidatePolicy.GERRIT);
-    } catch (PathConflictException e) {
+    } catch (MergeConflictException e) {
       throw new IOException(e.getMessage());
     } finally {
       if (inserter != null) {
@@ -281,7 +282,7 @@
       boolean sendMail, 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,7 +300,6 @@
 
     PatchSetInserter patchSetInserter = patchSetInserterFactory
         .create(git, revWalk, changeControl, rebasedCommit)
-        .setCopyLabels(true)
         .setValidatePolicy(validate)
         .setDraft(originalPatchSet.isDraft())
         .setUploader(uploader.getAccountId())
@@ -323,46 +323,43 @@
   }
 
   /**
-   * 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;
   }
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 b1ec6e4..cf4ab5b 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
@@ -111,7 +111,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. */
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..cb9feec 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
@@ -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;
@@ -45,8 +48,6 @@
 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,7 +72,7 @@
 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;
@@ -89,9 +90,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;
@@ -119,13 +123,13 @@
 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.ProjectCreationValidationListener;
 import com.google.inject.Inject;
 import com.google.inject.TypeLiteral;
@@ -163,6 +167,7 @@
     install(ConflictsCacheImpl.module());
     install(GroupCacheImpl.module());
     install(GroupIncludeCacheImpl.module());
+    install(MergeabilityCacheImpl.module());
     install(PatchListCacheImpl.module());
     install(ProjectCacheImpl.module());
     install(SectionSortCache.module());
@@ -184,7 +189,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 +236,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);
@@ -262,14 +267,16 @@
     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(ReindexAfterUpdate.class);
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
         .to(ProjectConfigEntry.UpdateChecker.class);
     DynamicSet.setOf(binder(), ChangeListener.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);
     DynamicItem.itemOf(binder(), AvatarProvider.class);
     DynamicSet.setOf(binder(), LifecycleListener.class);
     DynamicSet.setOf(binder(), TopMenu.class);
@@ -278,7 +285,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 +296,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/GerritServerConfigProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigProvider.java
index 92a2614..4dd9fd4 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(final SitePaths site, final 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/TrackingFootersProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFootersProvider.java
index a76b010..2554781 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
@@ -81,6 +81,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 8c1fdb6..d8dbfed 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;
@@ -94,11 +94,7 @@
     //
     try {
       encrypt("test", new Date(0), "test".getBytes("UTF-8"));
-    } catch (NoSuchProviderException e) {
-      throw new ProvisionException("PGP encryption not available", e);
-    } catch (PGPException e) {
-      throw new ProvisionException("PGP encryption not available", e);
-    } catch (IOException e) {
+    } catch (NoSuchProviderException | PGPException | IOException e) {
       throw new ProvisionException("PGP encryption not available", e);
     }
   }
@@ -112,9 +108,7 @@
     try (InputStream fin = new FileInputStream(pub);
         InputStream in = PGPUtil.getDecoderStream(fin)) {
         return new PGPPublicKeyRingCollection(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);
     }
   }
@@ -132,6 +126,7 @@
     return null;
   }
 
+  @Override
   public void store(final Account account, final ContactInformation info)
       throws ContactInformationStoreException {
     try {
@@ -155,13 +150,7 @@
       u.put("account_id", String.valueOf(account.getId().get()));
       u.put("data", encStr);
       connFactory.open(storeUrl).store(u.toString().getBytes("UTF-8"));
-    } catch (IOException e) {
-      log.error("Cannot store encrypted contact information", e);
-      throw new ContactInformationStoreException(e);
-    } catch (PGPException e) {
-      log.error("Cannot store encrypted contact information", e);
-      throw new ContactInformationStoreException(e);
-    } catch (NoSuchProviderException e) {
+    } catch (IOException | PGPException | NoSuchProviderException e) {
       log.error("Cannot store encrypted contact information", e);
       throw new ContactInformationStoreException(e);
     }
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..f646ea5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEdit.java
@@ -0,0 +1,85 @@
+// 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.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 ChangeEditUtil.editRefName(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..008a217
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditJson.java
@@ -0,0 +1,141 @@
+// 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.ActionInfo;
+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.extensions.webui.PrivateInternals_UiActionDescription;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.PatchSet;
+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();
+    out.actions = fillActions(edit);
+    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 static Map<String, ActionInfo> fillActions(ChangeEdit edit) {
+    Map<String, ActionInfo> actions = Maps.newTreeMap();
+
+    UiAction.Description descr = new UiAction.Description();
+    PrivateInternals_UiActionDescription.setId(descr, "/");
+    PrivateInternals_UiActionDescription.setMethod(descr, "DELETE");
+    descr.setTitle("Delete edit");
+    actions.put(descr.getId(), new ActionInfo(descr));
+
+    // Only expose publish action when the edit is on top of current ps
+    PatchSet.Id current = edit.getChange().currentPatchSetId();
+    PatchSet basePs = edit.getBasePatchSet();
+    if (basePs.getId().equals(current)) {
+      descr = new UiAction.Description();
+      PrivateInternals_UiActionDescription.setId(descr, "publish");
+      PrivateInternals_UiActionDescription.setMethod(descr, "POST");
+      descr.setTitle("Publish edit");
+      actions.put(descr.getId(), new ActionInfo(descr));
+    } else {
+      descr = new UiAction.Description();
+      PrivateInternals_UiActionDescription.setId(descr, "rebase");
+      PrivateInternals_UiActionDescription.setMethod(descr, "POST");
+      descr.setTitle("Rebase edit");
+      actions.put(descr.getId(), new ActionInfo(descr));
+    }
+
+    return actions;
+  }
+
+  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..986a442
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -0,0 +1,467 @@
+// 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 com.google.gerrit.server.edit.ChangeEditUtil.editRefName;
+import static com.google.gerrit.server.edit.ChangeEditUtil.editRefPrefix;
+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.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,
+    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();
+    Repository repo = gitManager.openRepository(change.getProject());
+    String refPrefix = editRefPrefix(me.getAccountId(), change.getId());
+
+    try {
+      Map<String, Ref> refs = repo.getRefDatabase().getRefs(refPrefix);
+      if (!refs.isEmpty()) {
+        throw new ResourceConflictException("edit already exists");
+      }
+
+      RevWalk rw = new RevWalk(repo);
+      try {
+        ObjectId revision = ObjectId.fromString(ps.getRevision().get());
+        String editRefName = editRefName(me.getAccountId(), change.getId(),
+            ps.getId());
+        return update(repo, me, editRefName, rw, ObjectId.zeroId(), revision);
+      } finally {
+        rw.release();
+      }
+    } finally {
+      repo.close();
+    }
+  }
+
+  /**
+   * 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 InvalidChangeOperationException
+   * @throws IOException
+   */
+  public void rebaseEdit(ChangeEdit edit, PatchSet current)
+      throws AuthException, InvalidChangeOperationException, IOException {
+    if (!currentUser.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+
+    Change change = edit.getChange();
+    IdentifiedUser me = (IdentifiedUser) currentUser.get();
+    String refName = editRefName(me.getAccountId(), change.getId(),
+        current.getId());
+    Repository repo = gitManager.openRepository(change.getProject());
+    try {
+      RevWalk rw = new RevWalk(repo);
+      BatchRefUpdate ru = repo.getRefDatabase().newBatchUpdate();
+      ObjectInserter inserter = repo.newObjectInserter();
+      try {
+        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 InvalidChangeOperationException("merge conflict");
+        }
+      } finally {
+        rw.release();
+        inserter.release();
+      }
+    } finally {
+      repo.close();
+    }
+  }
+
+  /**
+   * 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();
+    Repository repo = gitManager.openRepository(edit.getChange().getProject());
+    try {
+      RevWalk rw = new RevWalk(repo);
+      ObjectInserter inserter = repo.newObjectInserter();
+      try {
+        String refName = edit.getRefName();
+        ObjectId commit = createCommit(me, inserter, prevEdit,
+            prevEdit.getTree(),
+            msg);
+        inserter.flush();
+        return update(repo, me, refName, rw, prevEdit, commit);
+      } finally {
+        rw.release();
+        inserter.release();
+      }
+    } finally {
+      repo.close();
+    }
+  }
+
+  /**
+   * 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, 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);
+  }
+
+  /**
+   * 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);
+  }
+
+  private RefUpdate.Result modify(TreeOperation op,
+      ChangeEdit edit, String file, @Nullable RawInput content)
+      throws AuthException, IOException, InvalidChangeOperationException {
+    if (!currentUser.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+    IdentifiedUser me = (IdentifiedUser) currentUser.get();
+    Repository repo = gitManager.openRepository(edit.getChange().getProject());
+    try {
+      RevWalk rw = new RevWalk(repo);
+      ObjectInserter inserter = repo.newObjectInserter();
+      ObjectReader reader = repo.newObjectReader();
+      try {
+        String refName = edit.getRefName();
+        RevCommit prevEdit = edit.getEditCommit();
+        ObjectId newTree = writeNewTree(op,
+            rw,
+            inserter,
+            prevEdit,
+            reader,
+            file,
+            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);
+      } finally {
+        rw.release();
+        inserter.release();
+        reader.release();
+      }
+    } finally {
+      repo.close();
+    }
+  }
+
+  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, 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 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);
+        TreeWalk tw =
+            TreeWalk.forPath(rw.getObjectReader(), fileName, base.getTree());
+        if (tw == null) {
+          dce.add(new DeletePath(fileName));
+          break;
+        }
+
+        final FileMode mode = tw.getFileMode(0);
+        final ObjectId oid = tw.getObjectId(0);
+        dce.add(new PathEdit(fileName) {
+          @Override
+          public void apply(DirCacheEntry ent) {
+            ent.setFileMode(mode);
+            ent.setObjectId(oid);
+          }
+        });
+        break;
+    }
+    dce.finish();
+    return newTree.writeTree(ins);
+  }
+
+  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..9a23293
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -0,0 +1,298 @@
+// 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.Account;
+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 {
+    if (!user.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+    Repository repo = gitManager.openRepository(change.getProject());
+    try {
+      IdentifiedUser me = (IdentifiedUser) user.get();
+      String editRefPrefix = editRefPrefix(me.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());
+      RevWalk rw = new RevWalk(repo);
+      try {
+        RevCommit commit = rw.parseCommit(ref.getObjectId());
+        PatchSet basePs = getBasePatchSet(change, ref);
+        return Optional.of(new ChangeEdit(me, change, ref, commit, basePs));
+      } finally {
+        rw.release();
+      }
+    } finally {
+      repo.close();
+    }
+  }
+
+  /**
+   * 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();
+    Repository repo = gitManager.openRepository(change.getProject());
+    try {
+      RevWalk rw = new RevWalk(repo);
+      ObjectInserter inserter = repo.newObjectInserter();
+      try {
+
+        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));
+      } finally {
+        inserter.release();
+        rw.release();
+      }
+
+      // TODO(davido): This should happen in the same BatchRefUpdate.
+      deleteRef(repo, edit);
+    } finally {
+      repo.close();
+    }
+  }
+
+  /**
+   * 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);
+    }
+  }
+
+  /**
+   * Returns reference for this change edit with sharded user and change number:
+   * refs/users/UU/UUUU/edit-CCCC/P.
+   *
+   * @param accountId accout id
+   * @param changeId change number
+   * @param psId patch set number
+   * @return reference for this change edit
+   */
+  static String editRefName(Account.Id accountId, Change.Id changeId,
+      PatchSet.Id psId) {
+    return editRefPrefix(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 accout id
+   * @param changeId change number
+   * @return reference prefix for this change edit
+   */
+  static String editRefPrefix(Account.Id accountId, Change.Id changeId) {
+    return String.format("%s/edit-%d/",
+        RefNames.refsUsers(accountId),
+        changeId.get());
+  }
+
+  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..33cf241 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
@@ -14,14 +14,38 @@
 
 package com.google.gerrit.server.events;
 
+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.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 final String type = "change-abandoned";
+  public ChangeAttribute change;
+  public PatchSetAttribute patchSet;
+  public AccountAttribute abandoner;
+  public String reason;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
 }
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..507ee89 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,19 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+
 public abstract class ChangeEvent {
+
+  public long eventCreatedOn = TimeUtil.nowMs() / 1000L;
+
+  public abstract String getType();
+
+  public abstract Project.NameKey getProjectNameKey();
+
+  public abstract Change.Key getChangeKey();
+
+  public abstract String getRefName();
 }
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..f1aaa0a 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
@@ -14,13 +14,37 @@
 
 package com.google.gerrit.server.events;
 
+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.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 final String type = "change-merged";
+  public ChangeAttribute change;
+  public PatchSetAttribute patchSet;
+  public AccountAttribute submitter;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
 }
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..bf759249 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
@@ -14,14 +14,38 @@
 
 package com.google.gerrit.server.events;
 
+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.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 final String type = "change-restored";
+  public ChangeAttribute change;
+  public PatchSetAttribute patchSet;
+  public AccountAttribute restorer;
+  public String reason;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
 }
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..3b0cf9c 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
@@ -14,16 +14,40 @@
 
 package com.google.gerrit.server.events;
 
+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.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 final String type = "comment-added";
+  public ChangeAttribute change;
+  public PatchSetAttribute patchSet;
+  public AccountAttribute author;
+  public ApprovalAttribute[] approvals;
+  public String comment;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
 }
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..825b595 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
 
@@ -21,6 +22,7 @@
 import org.eclipse.jgit.transport.ReceiveCommand;
 
 public class CommitReceivedEvent extends ChangeEvent {
+  public final String type = "commit-received";
   public final ReceiveCommand command;
   public final Project project;
   public final String refName;
@@ -35,4 +37,24 @@
     this.commit = commit;
     this.user = user;
   }
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return project.getNameKey();
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return null;
+  }
+
+  @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..a595165 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
@@ -14,13 +14,37 @@
 
 package com.google.gerrit.server.events;
 
+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.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 final String type = "draft-published";
+  public ChangeAttribute change;
+  public PatchSetAttribute patchSet;
+  public AccountAttribute uploader;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
 }
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/HashtagsChangedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/HashtagsChangedEvent.java
new file mode 100644
index 0000000..c6a07b3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/HashtagsChangedEvent.java
@@ -0,0 +1,51 @@
+// 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 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.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+
+public class HashtagsChangedEvent extends ChangeEvent {
+  public final String type = "hashtags-changed";
+  public ChangeAttribute change;
+  public AccountAttribute editor;
+  public String[] added;
+  public String[] removed;
+  public String[] hashtags;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
+}
\ 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..4ec6da3 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
@@ -14,14 +14,38 @@
 
 package com.google.gerrit.server.events;
 
+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.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 final String type = "merge-failed";
+  public ChangeAttribute change;
+  public PatchSetAttribute patchSet;
+  public AccountAttribute submitter;
+  public String reason;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
 }
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..830c6cb 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
@@ -14,13 +14,37 @@
 
 package com.google.gerrit.server.events;
 
+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.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 final String type = "patchset-created";
+  public ChangeAttribute change;
+  public PatchSetAttribute patchSet;
+  public AccountAttribute uploader;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
 }
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..d248fab
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefReceivedEvent.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.events;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.IdentifiedUser;
+
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+public class RefReceivedEvent extends ChangeEvent {
+  public final String type = "ref-received";
+  public ReceiveCommand command;
+  public Project project;
+  public IdentifiedUser user;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return project.getNameKey();
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return null;
+  }
+
+  @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..4b7244e 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,6 +14,8 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.RefUpdateAttribute;
 
@@ -21,4 +23,24 @@
   public final String type = "ref-updated";
   public AccountAttribute submitter;
   public RefUpdateAttribute refUpdate;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(refUpdate.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return null;
+  }
+
+  @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..20eb30b 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
@@ -14,13 +14,37 @@
 
 package com.google.gerrit.server.events;
 
+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.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 final String type = "reviewer-added";
+  public ChangeAttribute change;
+  public PatchSetAttribute patchSet;
+  public AccountAttribute reviewer;
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
 }
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..f897dbc 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
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.server.events;
 
+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.AccountAttribute;
 import com.google.gerrit.server.data.ChangeAttribute;
 
@@ -22,4 +26,24 @@
   public ChangeAttribute change;
   public AccountAttribute changer;
   public String oldTopic;
-}
\ No newline at end of file
+
+  @Override
+  public String getType() {
+    return type;
+  }
+
+  @Override
+  public Project.NameKey getProjectNameKey() {
+    return new Project.NameKey(change.project);
+  }
+
+  @Override
+  public Change.Key getChangeKey() {
+    return new Change.Key(change.id);
+  }
+
+  @Override
+  public String getRefName() {
+    return R_HEADS + change.branch;
+  }
+}
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..848d460 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
@@ -96,5 +96,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..08bdd4b 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;
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..21465ef 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,40 +84,43 @@
       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 {
+        ObjectId noteId = null;
         for (final ObjectId commitToBan : commitsToBan) {
           try {
             revWalk.parseCommit(commitToBan);
           } catch (MissingObjectException e) {
-            // ignore exception, also not existing commits can be banned
+            // Ignore exception, non-existing commits can be banned.
           } catch (IncorrectObjectTypeException e) {
-            result.notACommit(commitToBan, e.getMessage());
+            result.notACommit(commitToBan);
             continue;
           }
-          banCommitNotes.set(commitToBan, createNoteContent(reason, inserter));
+          if (noteId == null) {
+            noteId = createNoteContent(reason, inserter);
+          }
+          banCommitNotes.set(commitToBan, noteId);
         }
-        inserter.flush();
         NotesBranchUtil notesBranchUtil =
             notesBranchUtilFactory.create(project, repo, inserter);
         NoteMap newlyCreated =
@@ -157,7 +155,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/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..5584718 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
@@ -21,12 +21,23 @@
 
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.util.List;
 
 /** Extended commit entity with code review specific metadata. */
 public class CodeReviewCommit extends RevCommit {
+  public static RevWalk newRevWalk(Repository repo) {
+    return new RevWalk(repo) {
+      @Override
+      protected RevCommit createCommit(AnyObjectId id) {
+        return new CodeReviewCommit(id);
+      }
+    };
+  }
+
   static CodeReviewCommit revisionGone(ChangeControl ctl) {
     return error(ctl, CommitMergeStatus.REVISION_GONE);
   }
@@ -43,7 +54,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,
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 4c3e5f4..ef2bc2b 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
@@ -16,7 +16,7 @@
 
 public enum CommitMergeStatus {
   /** */
-  CLEAN_MERGE("Change has been successfully merged into the git repository."),
+  CLEAN_MERGE("Change has been successfully merged into the git repository"),
 
   /** */
   CLEAN_PICK("Change has been successfully cherry-picked"),
@@ -33,6 +33,12 @@
                   + "Please rebase the change locally and upload the rebased commit for review."),
 
   /** */
+  REBASE_MERGE_CONFLICT(
+      "The 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..71a68b4 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
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkArgument;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
@@ -98,7 +99,7 @@
 
     @Override
     public String toString() {
-      return Objects.toStringHelper(this)
+      return MoreObjects.toStringHelper(this)
           .add("unchanged", unchanged)
           .add("updated", updated)
           .add("deleted", 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..f5ec962 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,15 @@
 
 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.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 +128,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 +156,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 +188,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 +203,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 +234,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 {
@@ -231,6 +257,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 +279,7 @@
     }
   }
 
+  @Override
   public String getProjectDescription(final Project.NameKey name)
       throws RepositoryNotFoundException, IOException {
     final Repository e = openRepository(name);
@@ -274,6 +312,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/changedetail/PathConflictException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java
similarity index 66%
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/MergeIdenticalTreeException.java
index 7e2f2d7..109fa76 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/MergeIdenticalTreeException.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 is already contained in destination banch. */
+public class MergeIdenticalTreeException extends Exception {
   private static final long serialVersionUID = 1L;
-
-  public PathConflictException(String msg) {
-    super(msg);
+  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 e7ff0220..112b1a8 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
@@ -28,6 +28,7 @@
 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;
@@ -63,8 +64,9 @@
 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.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;
@@ -73,7 +75,6 @@
 
 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 +86,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;
 
@@ -128,28 +130,39 @@
   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 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;
@@ -159,58 +172,58 @@
   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,
+      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.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 +244,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,21 +255,23 @@
       RefUpdate branchUpdate = openBranch();
       boolean reopen = false;
 
-      final ListMultimap<SubmitType, Change> toSubmit =
+      ListMultimap<SubmitType, Change> toSubmit =
           validateChangeList(db.changes().submitted(destBranch).toList());
-      final ListMultimap<SubmitType, CodeReviewCommit> toMergeNextTurn =
+      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);
+          SubmitStrategy strategy = createStrategy(submitType);
           preMerge(strategy, toMerge.get(submitType));
           RefUpdate update = updateBranch(strategy, branchUpdate);
           reopen = true;
@@ -265,40 +282,46 @@
             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);
 
-      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);
@@ -319,13 +342,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 +355,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 +368,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 +376,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 +398,34 @@
     return true;
   }
 
-  private void preMerge(final SubmitStrategy strategy,
-      final List<CodeReviewCommit> toMerge) throws MergeException {
+  private void 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());
   }
 
-  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 +433,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,7 +444,7 @@
         branchTip = null;
         branchUpdate.setExpectedOldObjectId(ObjectId.zeroId());
       } else {
-        for (final Change c : db.changes().submitted(destBranch).toList()) {
+        for (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."));
         }
@@ -423,50 +455,49 @@
     }
   }
 
-  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<Change> submitted) throws MergeException {
+    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 (Change chg : submitted) {
       ChangeControl ctl;
       try {
         ctl = changeControlFactory.controlFor(chg,
@@ -474,14 +505,15 @@
       } catch (NoSuchChangeException e) {
         throw new MergeException("Failed to validate changes", e);
       }
-      final Change.Id changeId = chg.getId();
+      Change.Id changeId = chg.getId();
       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());
       } catch (OrmException e) {
@@ -489,16 +521,18 @@
       }
       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,16 +548,19 @@
         // 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;
@@ -536,8 +573,11 @@
 
       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 +590,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);
             } catch (OrmException e) {
-              log.error("Cannot mark change " + chg.getId() + " merged", e);
+              logError("Cannot mark change " + chg.getId() + " merged", e);
             }
             continue;
           }
@@ -563,8 +605,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;
@@ -578,22 +623,34 @@
   }
 
   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 {
+    if (branchTip == mergeTip) {
+      logDebug("Branch already at merge tip {}, no update to perform",
+          mergeTip.name());
+      return null;
+    } else if (mergeTip == 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());
@@ -610,7 +667,11 @@
     branchUpdate.setNewObjectId(mergeTip);
     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) {
@@ -621,11 +682,11 @@
           }
 
           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;
@@ -649,6 +710,7 @@
   }
 
   private void fireRefUpdated(RefUpdate branchUpdate) {
+    logDebug("Firing ref updated hooks for {}", branchUpdate.getName());
     gitRefUpdated.fire(destBranch.getParentKey(), branchUpdate);
     hooks.doRefUpdatedHook(destBranch, branchUpdate, getAccount(mergeTip));
   }
@@ -663,28 +725,42 @@
     return account;
   }
 
-  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 String getByAccountName(CodeReviewCommit codeReviewCommit) {
+    Account account = getAccount(codeReviewCommit);
+    if (account != null && account.getFullName() != null) {
+      return " by " + account.getFullName();
+    }
+    return "";
+  }
+
+  private void updateChangeStatus(List<Change> submitted)
+      throws NoSuchChangeException {
+    logDebug("Updating change status for {} changes", submitted);
+    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(), c.getDest(), s);
 
       try {
         switch (s) {
           case CLEAN_MERGE:
-            setMerged(c, message(c, txt));
+            setMerged(c, message(c, txt + getByAccountName(commit)));
             break;
 
           case CLEAN_REBASE:
           case CLEAN_PICK:
-            setMerged(c, message(c, txt + " as " + commit.name()));
+            setMerged(c, message(c, txt + " as " + commit.name()
+                + getByAccountName(commit)));
             break;
 
           case ALREADY_MERGED:
@@ -705,23 +781,27 @@
             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) {
+  private void updateSubscriptions(List<Change> submitted) {
     if (mergeTip != null && (branchTip == null || branchTip != mergeTip)) {
+      logDebug("Updating submodule subscriptions for {} changes",
+          submitted.size());
       SubmoduleOp subOp =
           subOpFactory.create(destBranch, mergeTip, rw, repo,
               destProject.getProject(), submitted, commits,
@@ -729,30 +809,35 @@
       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");
@@ -772,20 +857,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");
 
@@ -803,21 +891,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) {
@@ -830,18 +918,21 @@
   }
 
   private void setMerged(Change c, ChangeMessage msg)
-      throws OrmException, IOException, NoSuchChangeException {
+      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());
 
@@ -852,23 +943,21 @@
         cmUtil.addChangeMessage(db, update, msg);
       }
       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) {
+      try {
+        hooks.doChangeMergedHook(c,
+            accountCache.get(submitter.getAccountId()).getAccount(),
+            merged, db);
+      } catch (OrmException ex) {
+        logError("Cannot run hook for submitted patch set " + c.getId(), ex);
+      }
+    }
   }
 
   private Change setMergedPatchSet(Change.Id changeId, final PatchSet.Id merged)
@@ -877,10 +966,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.
@@ -888,7 +973,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);
@@ -911,7 +996,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;
         }
 
@@ -923,7 +1008,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);
         }
       }
 
@@ -939,11 +1024,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);
   }
 
@@ -955,10 +1042,17 @@
       @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 {
@@ -968,26 +1062,37 @@
             && Objects.equal(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) {
@@ -1032,7 +1137,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();
@@ -1058,12 +1163,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());
           }
@@ -1071,7 +1176,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);
         }
       }
 
@@ -1085,7 +1190,7 @@
       try {
         indexFuture.checkedGet();
       } catch (IOException e) {
-        log.error("Failed to index new change message", e);
+        logError("Failed to index new change message", e);
       }
     }
 
@@ -1095,7 +1200,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);
       }
     }
   }
@@ -1104,7 +1209,8 @@
     Exception err = null;
     try {
       openSchema();
-      for (Change c : db.changes().byProjectOpenAll(destBranch.getParentKey())) {
+      for (Change c
+          : db.changes().byProjectOpenAll(destBranch.getParentKey())) {
         abandonOneChange(c);
       }
       db.close();
@@ -1115,9 +1221,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);
     }
   }
 
@@ -1163,4 +1268,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/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index b35d7e4..4a6c8de 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,6 +20,7 @@
 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;
@@ -52,6 +53,7 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.merge.Merger;
+import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
 import org.eclipse.jgit.merge.ThreeWayMerger;
 import org.eclipse.jgit.revwalk.FooterKey;
 import org.eclipse.jgit.revwalk.FooterLine;
@@ -79,8 +81,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;
@@ -89,6 +89,12 @@
     return cfg.getBoolean("core", null, "useRecursiveMerge", true);
   }
 
+  public static ThreeWayMergeStrategy getMergeStrategy(Config cfg) {
+    return useRecursiveMerge(cfg)
+        ? MergeStrategy.RECURSIVE
+        : MergeStrategy.RESOLVE;
+  }
+
   public static interface Factory {
     MergeUtil create(ProjectState project);
     MergeUtil create(ProjectState project, boolean useContentMerge);
@@ -173,7 +179,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);
 
@@ -181,7 +188,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();
@@ -192,7 +199,7 @@
       mergeCommit.setMessage(commitMsg);
       return rw.parseCommit(commit(inserter, mergeCommit));
     } else {
-      return null;
+      throw new MergeConflictException("merge conflict");
     }
   }
 
@@ -216,8 +223,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');
@@ -226,8 +233,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');
@@ -372,8 +379,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 {
@@ -605,9 +611,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(
@@ -624,8 +632,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/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index a905385..cbece42 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;
@@ -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";
@@ -152,7 +151,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 +219,10 @@
     this.projectName = projectName;
   }
 
+  public Project.NameKey getName() {
+    return projectName;
+  }
+
   public Project getProject() {
     return project;
   }
@@ -318,24 +321,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 +365,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 +395,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 +412,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 +506,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 +521,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 +617,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 +638,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 +670,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);
@@ -767,31 +764,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 +798,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 +814,7 @@
     saveContributorAgreements(rc, keepGroups);
     saveAccessSections(rc, keepGroups);
     saveNotifySections(rc, keepGroups);
-    groupsByUUID.keySet().retainAll(keepGroups);
+    groupList.retainUUIDs(keepGroups);
     saveLabelSections(rc);
     savePluginSections(rc);
 
@@ -1102,30 +1087,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 +1100,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..0606dcc 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;
@@ -33,6 +35,7 @@
 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 +43,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 +53,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 +77,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,7 +84,6 @@
 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;
@@ -94,11 +97,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 +111,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 +135,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 +179,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";
@@ -276,7 +277,6 @@
   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 +297,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 +309,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 +324,7 @@
   private final Provider<Submit> submitProvider;
   private final MergeQueue mergeQueue;
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
+  private final NotesMigration notesMigration;
 
   private final List<CommitValidationMessage> messages = new ArrayList<>();
   private ListMultimap<Error, String> errors = LinkedListMultimap.create();
@@ -338,12 +337,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 +360,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,7 +373,8 @@
       final Provider<Submit> submitProvider,
       final MergeQueue mergeQueue,
       final ChangeKindCache changeKindCache,
-      final DynamicMap<ProjectConfigEntry> pluginConfigEntries) throws IOException {
+      final DynamicMap<ProjectConfigEntry> pluginConfigEntries,
+      final NotesMigration notesMigration) throws IOException {
     this.currentUser = (IdentifiedUser) projectControl.getCurrentUser();
     this.db = db;
     this.changeDataFactory = changeDataFactory;
@@ -384,7 +382,6 @@
     this.schemaFactory = schemaFactory;
     this.accountResolver = accountResolver;
     this.optionParserFactory = optionParserFactory;
-    this.createChangeSenderFactory = createChangeSenderFactory;
     this.mergedSenderFactory = mergedSenderFactory;
     this.replacePatchSetFactory = replacePatchSetFactory;
     this.gitRefUpdated = gitRefUpdated;
@@ -405,7 +402,6 @@
     this.changeUpdateExector = changeUpdateExector;
     this.requestScopePropagator = requestScopePropagator;
     this.indexer = indexer;
-    this.mergeabilityChecker = mergeabilityChecker;
     this.sshInfo = sshInfo;
     this.allProjectsName = allProjectsName;
     this.receiveConfig = config;
@@ -416,17 +412,19 @@
     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.messageSender = new ReceivePackMessageSender();
 
     ProjectState ps = projectControl.getProjectState();
 
+    this.newChangeForAllNotInTarget = ps.isCreateNewChangeForAllNotInTarget();
     rp.setAllowCreates(true);
     rp.setAllowDeletes(true);
     rp.setAllowNonFastForwards(true);
@@ -476,7 +474,7 @@
     });
     advHooks.add(rp.getAdvertiseRefsHook());
     advHooks.add(new ReceiveCommitsAdvertiseRefsHook(
-        db, projectControl.getProject().getNameKey()));
+        db, queryProvider, projectControl.getProject().getNameKey()));
     rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
   }
 
@@ -554,7 +552,7 @@
 
     parseCommands(commands);
     if (magicBranch != null && magicBranch.cmd.getResult() == NOT_ATTEMPTED) {
-      newChanges = selectNewChanges();
+      selectNewAndReplacedChangesFromMagicBranch();
     }
     preparePatchSetsForReplace();
 
@@ -588,28 +586,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 +615,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
@@ -672,15 +661,22 @@
       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:");
       for (ReplaceRequest u : updated) {
@@ -1003,7 +999,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 +1104,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;
@@ -1120,7 +1119,8 @@
     @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,7 +1135,7 @@
       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);
@@ -1148,28 +1148,32 @@
       labels.put(v.getLabel(), v.getValue());
     }
 
-    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 +1216,6 @@
       }
       return ref.substring(0, split);
     }
-
-    void setCmdLineParser(CmdLineParser clp) {
-      this.clp = clp;
-    }
   }
 
   private void parseMagicBranch(final ReceiveCommand cmd) {
@@ -1225,13 +1225,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,7 +1266,7 @@
       return;
     }
 
-    if (magicBranch.isDraft()
+    if (magicBranch.draft
         && (!receiveConfig.allowDrafts
             || projectControl.controlForRef("refs/drafts/" + ref)
             .isBlocked(Permission.PUSH))) {
@@ -1281,18 +1281,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 +1330,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 +1350,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 +1436,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 +1459,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 +1500,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<Change> changes = p.destChanges.toList();
         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,7 +1531,8 @@
           // 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) {
@@ -1502,14 +1541,16 @@
           if (requestReplace(magicBranch.cmd, false, changes.get(0), 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 +1563,33 @@
       //
       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;
     }
     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 +1602,12 @@
   private class ChangeLookup {
     final RevCommit commit;
     final Change.Key changeKey;
-    final ResultSet<Change> changes;
+    final ResultSet<Change> destChanges;
 
     ChangeLookup(RevCommit c, Change.Key key) throws OrmException {
       commit = c;
       changeKey = key;
-      changes = db.changes().byBranchKey(magicBranch.dest, key);
+      destChanges = db.changes().byBranchKey(magicBranch.dest, key);
     }
   }
 
@@ -1594,7 +1628,7 @@
           TimeUtil.nowTs());
       change.setTopic(magicBranch.topic);
       ins = changeInserterFactory.create(ctl, change, c)
-          .setDraft(magicBranch.isDraft());
+          .setDraft(magicBranch.draft);
       cmd = new ReceiveCommand(ObjectId.zeroId(), c,
           ins.getPatchSet().getRefName());
     }
@@ -1634,7 +1668,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 +1681,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);
       }
     }
@@ -1729,7 +1743,6 @@
           req.validate(false);
           if (req.skip && req.cmd == null) {
             itr.remove();
-            replaceByCommit.remove(req.newCommit);
           }
         }
       }
@@ -1923,7 +1936,7 @@
       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());
@@ -1993,14 +2006,23 @@
       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());
       try {
         change = db.changes().get(change.getId());
@@ -2024,7 +2046,7 @@
         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));
@@ -2052,7 +2074,6 @@
                   } else {
                     change.setStatus(Change.Status.NEW);
                   }
-                  change.setLastSha1MergeTested(null);
                   change.setCurrentPatchSet(info);
 
                   final List<String> idList = newCommit.getFooterLines(CHANGE_ID);
@@ -2089,10 +2110,7 @@
       if (cmd.getResult() == NOT_ATTEMPTED) {
         cmd.execute(rp);
       }
-      CheckedFuture<?, IOException> f = mergeabilityChecker.newCheck()
-          .addChange(change)
-          .reindex()
-          .runAsync();
+      CheckedFuture<?, IOException> f = indexer.indexAsync(change.getId());
       workQueue.getDefaultQueue()
           .submit(requestScopePropagator.wrap(new Runnable() {
         @Override
@@ -2129,7 +2147,7 @@
             change, currentUser.getAccount(), newPatchSet, db);
       }
 
-      if (magicBranch != null && magicBranch.isSubmit()) {
+      if (magicBranch != null && magicBranch.submit) {
         submit(changeCtl, newPatchSet);
       }
 
@@ -2138,18 +2156,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 +2270,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 +2305,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 +2317,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 +2328,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 = openChangesByKey(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 = openChangesByKey(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 +2385,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) {
@@ -2398,23 +2436,11 @@
     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> openChangesByKey(Branch.NameKey branch)
       throws OrmException {
-    final Map<Change.Key, Change.Id> r = new HashMap<>();
+    final Map<Change.Key, Change> r = new HashMap<>();
     for (Change c : db.changes().byBranchOpenAll(branch)) {
-      r.put(c.getKey(), c.getId());
+      r.put(c.getKey(), c);
     }
     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..5580f20 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,12 @@
     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().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..266ce1a 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
@@ -43,6 +43,7 @@
     mergeQueue = mq;
   }
 
+  @Override
   public void run() {
     final HashSet<Branch.NameKey> pending = new HashSet<>();
     try {
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..0c384b7 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
@@ -51,6 +51,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) {
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..2b73ff9 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
@@ -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());
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..799e220 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
@@ -332,7 +332,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..599a305 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,6 +14,7 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
 
 import org.eclipse.jgit.dircache.DirCache;
@@ -26,6 +27,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,6 +42,7 @@
 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;
 
@@ -175,11 +178,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 +241,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 {
@@ -249,20 +278,23 @@
         if (Objects.equal(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());
         }
       }
@@ -277,28 +309,8 @@
         if (Objects.equal(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
@@ -315,6 +327,35 @@
           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.disableRefLog();
+        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 8c11aec..0b9f7e6 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
@@ -17,7 +17,7 @@
 import com.google.common.collect.Lists;
 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;
@@ -312,6 +312,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
@@ -333,35 +334,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 {
@@ -397,7 +406,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..17cd06a 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,11 @@
 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.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;
@@ -84,15 +86,16 @@
           // taking the delta relative to that one parent and redoing
           // that on the current merge tip.
           //
-
-          mergeTip = writeCherryPickCommit(mergeTip, n);
-
-          if (mergeTip != null) {
+          try {
+            mergeTip = writeCherryPickCommit(mergeTip, n);
             newCommits.put(mergeTip.getPatchsetId().getParentKey(), mergeTip);
-          } else {
+          } catch (MergeConflictException mce) {
             n.setStatusCode(CommitMergeStatus.PATH_CONFLICT);
+            mergeTip = null;
+          } catch (MergeIdenticalTreeException mie) {
+            n.setStatusCode(CommitMergeStatus.ALREADY_MERGED);
+            mergeTip = null;
           }
-
         } 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
@@ -131,7 +134,8 @@
 
   private CodeReviewCommit writeCherryPickCommit(CodeReviewCommit mergeTip,
       CodeReviewCommit n) throws IOException, OrmException,
-      NoSuchChangeException {
+      NoSuchChangeException, MergeConflictException,
+      MergeIdenticalTreeException {
 
     args.rw.parseBody(n);
 
@@ -157,10 +161,6 @@
             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);
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..32eeafc 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
@@ -52,6 +52,7 @@
     return false;
   }
 
+  @Override
   public boolean dryRun(final CodeReviewCommit mergeTip,
       final CodeReviewCommit toMerge) throws MergeException {
     return args.mergeUtil.canFastForward(args.mergeSorter, mergeTip, args.rw,
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..17841f4 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,10 +20,10 @@
 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.RebaseSorter;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -109,8 +109,8 @@
             newMergeTip.setStatusCode(CommitMergeStatus.CLEAN_REBASE);
             newCommits.put(newPatchSet.getId().getParentKey(), newMergeTip);
             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);
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-server/src/main/java/com/google/gerrit/server/git/validators/ValidationMessage.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/ValidationMessage.java
new file mode 100644
index 0000000..e1098aa
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/ValidationMessage.java
@@ -0,0 +1,33 @@
+// 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;
+
+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 614138a..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,20 +117,18 @@
         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.evictMemberIn(agi.getIncludeUUID());
+        groupIncludeCache.evictParentGroupsOf(agi.getIncludeUUID());
       }
-      groupIncludeCache.evictMembersOf(group.getGroupUUID());
+      groupIncludeCache.evictSubgroupsOf(group.getGroupUUID());
     }
 
     return result;
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 555744e..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()));
       }
@@ -91,9 +91,9 @@
       writeAudits(toRemove);
       db.get().accountGroupById().delete(toRemove);
       for (final AccountGroupById g : toRemove) {
-        groupIncludeCache.evictMemberIn(g.getIncludeUUID());
+        groupIncludeCache.evictParentGroupsOf(g.getIncludeUUID());
       }
-      groupIncludeCache.evictMembersOf(internalGroup.getGroupUUID());
+      groupIncludeCache.evictSubgroupsOf(internalGroup.getGroupUUID());
     }
 
     return Response.none();
@@ -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/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/index/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
index 41dfba5..d8fb7db 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,11 @@
 
 package com.google.gerrit.server.index;
 
-import com.google.common.base.Objects;
+import com.google.common.base.Function;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Splitter;
+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,12 +27,11 @@
 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;
@@ -79,7 +81,7 @@
         @Override
         public String get(ChangeData input, FillArgs args)
             throws OrmException {
-          return ChangeStatusPredicate.VALUES.get(
+          return ChangeStatusPredicate.canonicalize(
               input.change().getStatus());
         }
       };
@@ -117,18 +119,6 @@
         }
       };
 
-  @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();
-        }
-      };
-
   /** Topic, a short annotation on the branch. */
   public static final FieldDef<ChangeData, String> TOPIC =
       new FieldDef.Single<ChangeData, String>(
@@ -136,24 +126,10 @@
         @Override
         public String get(ChangeData input, FillArgs args)
             throws OrmException {
-          return Objects.firstNonNull(input.change().getTopic(), "");
+          return MoreObjects.firstNonNull(input.change().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>(
@@ -165,43 +141,6 @@
         }
       };
 
-  @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());
-        }
-      };
-
   /** List of full file paths modified in the current patch set. */
   public static final FieldDef<ChangeData, Iterable<String>> PATH =
       new FieldDef.Repeatable<ChangeData, String>(
@@ -225,6 +164,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>(
@@ -408,7 +366,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 +377,25 @@
       };
 
   /** 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;
+          return input.isMergeable() ? "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 {
+          return input.isMergeable() ? "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..3c0d715 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
@@ -55,8 +55,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 +83,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 +97,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,7 +118,7 @@
    */
   public CheckedFuture<?, IOException> indexAsync(Change.Id id) {
     return executor != null
-        ? submit(new Task(id, false))
+        ? submit(new IndexTask(id))
         : Futures.<Object, IOException> immediateCheckedFuture(null);
   }
 
@@ -150,40 +151,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 +174,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 +213,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 +235,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..4f057c5 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
@@ -57,8 +53,4 @@
   @Override
   public void markReady(boolean ready) throws IOException {
   }
-
-  @Override
-  public void delete(int id) throws IOException {
-  }
 }
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/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..6bcd92d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java
@@ -0,0 +1,169 @@
+// 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 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.Account;
+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.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.QueueProvider.QueueType;
+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.util.Providers;
+
+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 ThreadLocalRequestContext tl;
+  private final SchemaFactory<ReviewDb> schemaFactory;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final ChangeIndexer.Factory indexerFactory;
+  private final IndexCollection indexes;
+  private final ListeningExecutorService executor;
+
+  @Inject
+  ReindexAfterUpdate(
+      ThreadLocalRequestContext tl,
+      SchemaFactory<ReviewDb> schemaFactory,
+      IdentifiedUser.GenericFactory userFactory,
+      ChangeIndexer.Factory indexerFactory,
+      IndexCollection indexes,
+      @IndexExecutor(QueueType.BATCH) ListeningExecutorService executor) {
+    this.tl = tl;
+    this.schemaFactory = schemaFactory;
+    this.userFactory = userFactory;
+    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.getOwner(), c.getId())));
+            }
+            return Futures.allAsList(result);
+          }
+        });
+  }
+
+  private abstract class Task<V> implements Callable<V> {
+    protected ReviewDb db;
+    protected Event event;
+
+    protected Task(Event event) {
+      this.event = event;
+    }
+
+    @Override
+    public final V call() throws Exception {
+      try {
+        db = schemaFactory.open();
+        return impl();
+      } catch (Exception e) {
+        log.error("Failed to reindex changes after " + event, e);
+        throw e;
+      } finally {
+        if (db != null) {
+          db.close();
+        }
+      }
+    }
+
+    protected abstract V impl() throws Exception;
+  }
+
+  private class GetChanges extends Task<List<Change>> {
+    private GetChanges(Event event) {
+      super(event);
+    }
+
+    @Override
+    protected List<Change> impl() throws OrmException {
+      String ref = event.getRefName();
+      Project.NameKey project = new Project.NameKey(event.getProjectName());
+      if (ref.equals(RefNames.REFS_CONFIG)) {
+        return db.changes().byProjectOpenAll(project).toList();
+      } else {
+        return db.changes().byBranchOpenAll(new Branch.NameKey(project, ref))
+            .toList();
+      }
+    }
+  }
+
+  private class Index extends Task<Void> {
+    private final Account.Id user;
+    private final Change.Id id;
+
+    Index(Event event, Account.Id user, Change.Id id) {
+      super(event);
+      this.user = user;
+      this.id = id;
+    }
+
+    @Override
+    protected Void impl() throws OrmException, IOException {
+      RequestContext context = new RequestContext() {
+        @Override
+        public CurrentUser getCurrentUser() {
+          return userFactory.create(user);
+        }
+
+        @Override
+        public Provider<ReviewDb> getReviewDbProvider() {
+          return Providers.of(db);
+        }
+      };
+      RequestContext old = tl.setContext(context);
+      try {
+        Change c = db.changes().get(id);
+        indexerFactory.create(executor, indexes).index(db, c);
+        return null;
+      } finally {
+        tl.setContext(old);
+      }
+    }
+  }
+}
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 82%
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 5ee240b..8b029dd 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,12 @@
 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.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.MultiProgressMonitor;
 import com.google.gerrit.server.git.MultiProgressMonitor.Task;
 import com.google.gerrit.server.patch.PatchListLoader;
@@ -43,13 +45,14 @@
 import org.eclipse.jgit.diff.DiffEntry;
 import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTree;
@@ -62,6 +65,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;
@@ -72,9 +76,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;
@@ -111,37 +115,52 @@
   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,
       ChangeData.Factory changeDataFactory,
       GitRepositoryManager repoManager,
-      @IndexExecutor ListeningExecutorService executor,
+      @IndexExecutor(BATCH) ListeningExecutorService executor,
       ChangeIndexer.Factory indexerFactory,
-      @Nullable MergeabilityChecker mergeabilityChecker) {
+      @GerritServerConfig Config config) {
     this.schemaFactory = schemaFactory;
     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);
@@ -150,11 +169,8 @@
     final AtomicBoolean ok = new AtomicBoolean(true);
 
     for (final Project.NameKey project : projects) {
-      if (!updateMergeable(project)) {
-        ok.set(false);
-      }
       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() {
@@ -162,13 +178,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);
@@ -189,7 +205,7 @@
           fail(project, e);
           throw e;
         }
-      }, MoreExecutors.sameThreadExecutor());
+      }, MoreExecutors.directExecutor());
     }
 
     try {
@@ -208,18 +224,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 (IOException 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) {
@@ -239,8 +243,13 @@
               byId.put(r.getObjectId(), changeDataFactory.create(db, c));
             }
           }
-          new ProjectIndexer(indexer, byId, repo, done, failed, verboseWriter)
-              .call();
+          new ProjectIndexer(indexer,
+              mergeStrategy,
+              byId,
+              repo,
+              done,
+              failed,
+              verboseWriter).call();
         } catch (RepositoryNotFoundException rnfe) {
           log.error(rnfe.getMessage());
         } finally {
@@ -259,8 +268,9 @@
     };
   }
 
-  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;
     private final ProgressMonitor done;
     private final ProgressMonitor failed;
@@ -268,16 +278,15 @@
     private final Repository repo;
     private RevWalk walk;
 
-    public ProjectIndexer(ChangeIndexer indexer,
-        Multimap<ObjectId, ChangeData> changesByCommitId, Repository repo) {
-      this(indexer, changesByCommitId, repo,
-          NullProgressMonitor.INSTANCE, NullProgressMonitor.INSTANCE, null);
-    }
-
-    ProjectIndexer(ChangeIndexer indexer,
-        Multimap<ObjectId, ChangeData> changesByCommitId, Repository repo,
-        ProgressMonitor done, ProgressMonitor failed, PrintWriter verboseWriter) {
+    private ProjectIndexer(ChangeIndexer indexer,
+        ThreeWayMergeStrategy mergeStrategy,
+        Multimap<ObjectId, ChangeData> changesByCommitId,
+        Repository repo,
+        ProgressMonitor done,
+        ProgressMonitor failed,
+        PrintWriter verboseWriter) {
       this.indexer = indexer;
+      this.mergeStrategy = mergeStrategy;
       this.byId = changesByCommitId;
       this.repo = repo;
       this.done = done;
@@ -376,7 +385,7 @@
           walk.parseBody(a);
           return walk.parseTree(a.getTree());
         case 2:
-          return PatchListLoader.automerge(repo, walk, b);
+          return PatchListLoader.automerge(repo, walk, b, mergeStrategy);
         default:
           return null;
       }
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..405b3a8 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))
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..abd779b 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
@@ -62,7 +62,7 @@
   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;
@@ -83,7 +83,7 @@
       @AnonymousCowardName String anonymousCowardName,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
       AllProjectsName allProjectsName,
-      ChangeQueryBuilder.Factory queryBuilder,
+      ChangeQueryBuilder queryBuilder,
       Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       RuntimeInstance velocityRuntime,
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..bc3ff4f 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
@@ -128,7 +128,11 @@
   /** 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 +319,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..667d834 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,7 +81,7 @@
       final boolean newChange, final IdentifiedUser currentUser,
       final Change updatedChange, final PatchSet updatedPatchSet,
       final LabelTypes labelTypes)
-      throws OrmException, IOException, PatchSetInfoNotAvailableException {
+      throws OrmException, IOException {
     final Repository git = repoManager.openRepository(updatedChange.getProject());
     try {
       final RevWalk revWalk = new RevWalk(git);
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 3d16372..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,
@@ -166,7 +166,7 @@
       for (AccountGroupMember m : db.accountGroupMembers().byGroup(ig.getId())) {
         matching.accounts.add(m.getAccountId());
       }
-      for (AccountGroup.UUID m : args.groupIncludes.membersOf(uuid)) {
+      for (AccountGroup.UUID m : args.groupIncludes.subgroupsOf(uuid)) {
         if (seen.add(m)) {
           q.add(m);
         }
@@ -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/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..01fa6b1 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,12 +177,14 @@
   }
 
   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. */
+  abstract public 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.
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..e97111d 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;
@@ -548,9 +273,8 @@
       return;
     }
     RevWalk walk = new RevWalk(reader);
-    try {
-      Change change = getChange();
-      Parser parser = new Parser(change, rev, walk, repoManager);
+    try (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,23 @@
         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 +323,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..92248d4
--- /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.PatchLineComment;
+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.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.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);
+  }
+}
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..2eacd09
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
@@ -0,0 +1,335 @@
+// 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.base.Objects;
+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.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 {
+    ObjectInserter inserter = repo.newObjectInserter();
+    try {
+      update.setInserter(inserter);
+      update.writeCommit(batch);
+    } finally {
+      inserter.release();
+    }
+  }
+
+  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 static abstract 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.equal(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.equal(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..23231de 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);
@@ -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..652f37e 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,8 +227,7 @@
    *    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);
@@ -232,6 +237,7 @@
 
     if (note[ptr.value] == '\n') {
       range.setEndLine(startLine);
+      ptr.value += 1;
       return range;
     } else if (note[ptr.value] == ':') {
       range.setStartLine(startLine);
@@ -368,7 +374,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 +395,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 +416,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 +440,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 +461,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 +524,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..d40358b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -0,0 +1,173 @@
+// 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;
+    }
+
+    RevWalk walk = new RevWalk(reader);
+    try (DraftCommentNotesParser parser = new DraftCommentNotesParser(
+        getChangeId(), walk, rev, repoManager, draftsProject, author)) {
+      parser.parseDraftComments();
+
+      buildCommentTable(draftBaseComments, parser.draftBaseComments);
+      buildCommentTable(draftPsComments, parser.draftPsComments);
+      noteMap = parser.noteMap;
+    } finally {
+      walk.release();
+    }
+  }
+
+  @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/PatchFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
index 81f4352..2e63a76 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
@@ -57,9 +57,9 @@
         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;
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 4eede00..0c8c5ec 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
@@ -21,7 +21,9 @@
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeUtil;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.diff.DiffEntry;
@@ -35,6 +37,7 @@
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
@@ -45,8 +48,8 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.merge.MergeFormatter;
 import org.eclipse.jgit.merge.MergeResult;
-import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.merge.ResolveMerger;
+import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
 import org.eclipse.jgit.patch.FileHeader;
 import org.eclipse.jgit.patch.FileHeader.PatchType;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -72,11 +75,14 @@
 
   private final GitRepositoryManager repoManager;
   private final PatchListCache patchListCache;
+  private final ThreeWayMergeStrategy mergeStrategy;
 
   @Inject
-  PatchListLoader(GitRepositoryManager mgr, PatchListCache plc) {
+  PatchListLoader(GitRepositoryManager mgr, PatchListCache plc,
+      @GerritServerConfig Config cfg) {
     repoManager = mgr;
     patchListCache = plc;
+    mergeStrategy = MergeUtil.getMergeStrategy(cfg);
   }
 
   @Override
@@ -121,7 +127,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);
       }
 
@@ -152,7 +158,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);
@@ -170,7 +176,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();
 
@@ -191,8 +197,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());
@@ -224,7 +230,7 @@
     }
   }
 
-  private static RevObject aFor(final PatchListKey key,
+  private RevObject aFor(final PatchListKey key,
       final Repository repo, final RevWalk rw, final RevCommit b)
       throws IOException {
     if (key.getOldId() != null) {
@@ -240,20 +246,20 @@
         return r;
       }
       case 2:
-        return automerge(repo, rw, b);
+        return automerge(repo, rw, b, mergeStrategy);
       default:
         // TODO(sop) handle an octopus merge.
         return null;
     }
   }
 
-  public static RevTree automerge(Repository repo, RevWalk rw, RevCommit b)
-      throws IOException {
-    return automerge(repo, rw, b, true);
+  public static RevTree automerge(Repository repo, RevWalk rw, RevCommit b,
+      ThreeWayMergeStrategy mergeStrategy) throws IOException {
+    return automerge(repo, rw, b, mergeStrategy, true);
   }
 
   public static RevTree automerge(Repository repo, RevWalk rw, RevCommit b,
-      boolean save) throws IOException {
+      ThreeWayMergeStrategy mergeStrategy, boolean save) throws IOException {
     String hash = b.name();
     String refName = RefNames.REFS_CACHE_AUTOMERGE
         + hash.substring(0, 2)
@@ -264,8 +270,7 @@
       return rw.parseTree(ref.getObjectId());
     }
 
-    ObjectId treeId;
-    ResolveMerger m = (ResolveMerger) MergeStrategy.RESOLVE.newMerger(repo, true);
+    ResolveMerger m = (ResolveMerger) mergeStrategy.newMerger(repo, true);
     final ObjectInserter ins = repo.newObjectInserter();
     try {
       DirCache dc = DirCache.newInCore();
@@ -297,6 +302,7 @@
         return null;
       }
 
+      ObjectId treeId;
       if (couldMerge) {
         treeId = m.getResultTreeId();
 
@@ -320,7 +326,8 @@
         Map<String, ObjectId> resolved = new HashMap<>();
         for (Map.Entry<String, MergeResult<? extends Sequence>> entry : r.entrySet()) {
           MergeResult<? extends Sequence> p = entry.getValue();
-          TemporaryBuffer buf = new TemporaryBuffer.LocalFile(10 * 1024 * 1024);
+          TemporaryBuffer buf =
+              new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024);
           try {
             fmt.formatMerge(buf, p, "BASE", oursName, theirsName, "UTF-8");
             buf.close();
@@ -381,17 +388,18 @@
         treeId = dc.writeTree(ins);
       }
       ins.flush();
+
+      if (save) {
+        RefUpdate update = repo.updateRef(refName);
+        update.setNewObjectId(treeId);
+        update.disableRefLog();
+        update.forceUpdate();
+      }
+
+      return rw.lookupTree(treeId);
     } finally {
       ins.release();
     }
-
-    if (save) {
-      RefUpdate update = repo.updateRef(refName);
-      update.setNewObjectId(treeId);
-      update.disableRefLog();
-      update.forceUpdate();
-    }
-    return rw.parseTree(treeId);
   }
 
   private static ObjectId emptyTree(final Repository repo) throws IOException {
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..42713d0 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,11 +20,11 @@
 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.inject.Inject;
 
@@ -436,7 +436,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;
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..4cb984f 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,7 @@
   private final PatchSet.Id psa;
   private final PatchSet.Id psb;
   private final AccountDiffPreference diffPrefs;
+  private final ChangeEditUtil editReader;
 
   private final Change.Id changeId;
   private boolean loadHistory = true;
@@ -99,6 +105,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 +118,7 @@
     this.control = control;
     this.aicFactory = aicFactory;
     this.plcUtil = plcUtil;
+    this.editReader = editReader;
 
     this.fileName = fileName;
     this.psa = patchSetA;
@@ -130,7 +138,8 @@
 
   @Override
   public PatchScript call() throws OrmException, NoSuchChangeException,
-      LargeObjectException {
+      LargeObjectException, AuthException,
+      InvalidChangeOperationException, IOException {
     validatePatchSetId(psa);
     validatePatchSetId(psb);
 
@@ -197,12 +206,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 +229,15 @@
     }
   }
 
+  private ObjectId getEditRev() throws AuthException,
+      NoSuchChangeException, IOException {
+    Optional<ChangeEdit> 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;
@@ -338,7 +360,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/Text.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java
index f12b02b..1939c84 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,8 +43,7 @@
   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 {
+  public static Text forCommit(ObjectReader reader, AnyObjectId commitId) throws IOException {
     RevWalk rw = new RevWalk(reader);
     RevCommit c;
     if (commitId instanceof RevCommit) {
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..6925dcc 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,8 @@
 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.gerrit.server.securestore.SecureStoreProvider;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
@@ -121,5 +123,6 @@
 
   @Override
   protected void configure() {
+    bind(SecureStore.class).toProvider(SecureStoreProvider.class);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/HttpModuleGenerator.java
similarity index 60%
copy from gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/plugins/HttpModuleGenerator.java
index cd07320..dd0ce67 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/HttpModuleGenerator.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 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,16 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd;
+package com.google.gerrit.server.plugins;
 
-public class GerritUiOptions {
-  private final boolean headless;
 
-  public GerritUiOptions(boolean headless) {
-    this.headless = headless;
-  }
+public interface HttpModuleGenerator extends ModuleGenerator {
+  void export(String javascript);
 
-  public boolean enableDefaultUi() {
-    return !headless;
+  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..7c35014 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 {
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..d7d0efd 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;
@@ -274,6 +274,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..79ad9ee 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;
@@ -187,7 +187,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 +204,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..8b99286 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>,"
@@ -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/ServerPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
index ee5e90a..a1d8ea5 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) {
@@ -266,44 +269,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/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 6a720e8..e69d356 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;
@@ -84,20 +66,6 @@
       }
     }
 
-    public ChangeControl controlFor(Change.Id id, CurrentUser user)
-        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, user);
-    }
-
     public ChangeControl validateFor(Change change, CurrentUser user)
         throws NoSuchChangeException, OrmException {
       ChangeControl c = controlFor(change, user);
@@ -106,15 +74,6 @@
       }
       return c;
     }
-
-    public ChangeControl validateFor(Change.Id id, CurrentUser user)
-        throws NoSuchChangeException, OrmException {
-      ChangeControl c = controlFor(id, user);
-      if (!c.isVisible(db.get())) {
-        throw new NoSuchChangeException(c.getChange().getId());
-      }
-      return c;
-    }
   }
 
   public static class Factory {
@@ -175,19 +134,6 @@
     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;
@@ -413,8 +359,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() {
@@ -425,306 +376,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-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..2543818 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,29 @@
 
 package com.google.gerrit.server.project;
 
+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.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 Project.NameKey getProject() {
+    return project.getNameKey();
+  }
+
   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..e5e7bed 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
@@ -65,13 +70,14 @@
       RevWalk rw = new RevWalk(repo);
       try {
         RevCommit commit = rw.parseCommit(objectId);
-        if (!parent.getControl().canReadCommit(rw, commit)) {
+        rw.parseBody(commit);
+        if (!parent.getControl().canReadCommit(db.get(), rw, commit)) {
           throw new ResourceNotFoundException(id);
         }
         for (int i = 0; i < commit.getParentCount(); i++) {
-          rw.parseCommit(commit.getParent(i));
+          rw.parseBody(rw.parseCommit(commit.getParent(i)));
         }
-        return new CommitResource(parent.getControl(), commit);
+        return new CommitResource(parent, commit);
       } catch (MissingObjectException | IncorrectObjectTypeException e) {
         throw new ResourceNotFoundException(id);
       } finally {
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..47a313f 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
@@ -43,6 +43,7 @@
   public InheritedBooleanInfo useContributorAgreements;
   public InheritedBooleanInfo useContentMerge;
   public InheritedBooleanInfo useSignedOffBy;
+  public InheritedBooleanInfo createNewChangeForAllNotInTarget;
   public InheritedBooleanInfo requireChangeId;
   public MaxObjectSizeLimitInfo maxObjectSizeLimit;
   public SubmitType submitType;
@@ -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 =
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 3dcf9f4..983549d 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.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Branch;
 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.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -67,6 +68,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;
@@ -74,10 +76,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;
@@ -129,7 +133,8 @@
         }
       }
 
-      if (!refControl.canCreate(rw, object, true)) {
+      rw.reset();
+      if (!refControl.canCreate(db.get(), rw, object)) {
         throw new AuthException("Cannot create \"" + ref + "\"");
       }
 
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..be7c272 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,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.common.collect.Lists;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -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..428c7b2 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
@@ -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/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/FilesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java
index 2d0af29..f0544fe 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,7 +19,6 @@
 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;
 
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..f383230 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,7 +40,7 @@
   @Override
   public FileResource parse(CommitResource parent, IdString id)
       throws ResourceNotFoundException {
-    return new FileResource(parent.getNameKey(), parent.getCommit().getName(),
+    return new FileResource(parent.getProject(), parent.getCommit().getName(),
         id.get());
   }
 
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..00f25bc 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,25 @@
 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(), 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..0530a4c 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,12 +36,14 @@
 
 @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
@@ -61,7 +65,7 @@
         RevWalk rw = new RevWalk(repo);
         try {
           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");
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-server/src/main/java/com/google/gerrit/server/project/GetTag.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetTag.java
new file mode 100644
index 0000000..5b78e08
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetTag.java
@@ -0,0 +1,28 @@
+// 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.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 7c8b29f..9dfdb25 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.RefDatabase;
 import org.eclipse.jgit.lib.Repository;
+import org.kohsuke.args4j.Option;
 
 import java.io.IOException;
 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;
@@ -144,6 +168,63 @@
     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) {
+              return in.ref.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) {
+                return a.run(in.ref);
+              }
+            }));
+      } catch (IllegalArgumentException e) {
+        throw new BadRequestException(e.getMessage());
+      }
+    }
     return branches;
   }
 
@@ -161,6 +242,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;
   }
 
@@ -169,6 +254,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..07fd095 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
@@ -112,29 +112,25 @@
       throws IOException {
     List<DashboardInfo> list = Lists.newArrayList();
     TreeWalk tw = new TreeWalk(rw.getObjectReader());
-    try {
-      tw.addTree(rw.parseTree(ref.getObjectId()));
-      tw.setRecursive(true);
-      while (tw.next()) {
-        if (tw.getFileMode(0) == FileMode.REGULAR_FILE) {
-          try {
-            list.add(DashboardsCollection.parse(
-                definingProject,
-                ref.getName().substring(REFS_DASHBOARDS.length()),
-                tw.getPathString(),
-                new BlobBasedConfig(null, git, tw.getObjectId(0)),
-                project,
-                setDefault));
-          } catch (ConfigInvalidException e) {
-            log.warn(String.format(
-                "Cannot parse dashboard %s:%s:%s: %s",
-                definingProject.getName(), ref.getName(), tw.getPathString(),
-                e.getMessage()));
-          }
+    tw.addTree(rw.parseTree(ref.getObjectId()));
+    tw.setRecursive(true);
+    while (tw.next()) {
+      if (tw.getFileMode(0) == FileMode.REGULAR_FILE) {
+        try {
+          list.add(DashboardsCollection.parse(
+              definingProject,
+              ref.getName().substring(REFS_DASHBOARDS.length()),
+              tw.getPathString(),
+              new BlobBasedConfig(null, git, tw.getObjectId(0)),
+              project,
+              setDefault));
+        } catch (ConfigInvalidException e) {
+          log.warn(String.format(
+              "Cannot parse dashboard %s:%s:%s: %s",
+              definingProject.getName(), ref.getName(), tw.getPathString(),
+              e.getMessage()));
         }
       }
-    } 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..e12b38a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
@@ -0,0 +1,159 @@
+// 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 {
+    Repository repo = getRepository(resource.getNameKey());
+
+    String tagName = id.get();
+    if (!tagName.startsWith(Constants.R_TAGS)) {
+      tagName = Constants.R_TAGS + tagName;
+    }
+
+    try {
+      RevWalk rw = new RevWalk(repo);
+      try {
+        Ref ref = repo.getRefDatabase().getRef(tagName);
+        if (ref != null && !visibleTags(resource.getControl(), repo,
+            ImmutableMap.of(ref.getName(), ref)).isEmpty()) {
+          return createTagInfo(ref, rw);
+        }
+      } finally {
+        rw.dispose();
+      }
+    } finally {
+      repo.close();
+    }
+    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..ace221d 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);
@@ -71,6 +73,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..111385c 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;
@@ -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) {
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 63d2b35..53fc8d2 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
@@ -24,6 +24,7 @@
 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;
@@ -60,23 +61,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) {
@@ -92,9 +92,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)) {
@@ -109,7 +113,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<>();
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..fc79974 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);
     }
@@ -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/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index b9cead3..62d625f 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;
@@ -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/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
index 9fa9b52..0e4ff98 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
@@ -32,11 +32,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;
@@ -66,6 +66,7 @@
     public InheritableBoolean useContributorAgreements;
     public InheritableBoolean useContentMerge;
     public InheritableBoolean useSignedOffBy;
+    public InheritableBoolean createNewChangeForAllNotInTarget;
     public InheritableBoolean requireChangeId;
     public String maxObjectSizeLimit;
     public SubmitType submitType;
@@ -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);
       }
@@ -177,7 +183,7 @@
           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) {
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..db67ce0 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,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
 import com.google.common.base.Strings;
 import com.google.gerrit.common.ChangeHooks;
@@ -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")) {
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 00ecee3..669afc6 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
@@ -24,6 +24,7 @@
 import com.google.gerrit.common.errors.InvalidNameException;
 import com.google.gerrit.extensions.api.projects.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;
 
@@ -231,12 +238,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;
     }
@@ -255,10 +263,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 {
@@ -275,7 +297,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;
         }
@@ -297,6 +319,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.
@@ -380,6 +424,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/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/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..deef4ea 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.common.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/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..82260b4 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,9 @@
       //
       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)) {
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..4fb4455 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,68 +16,54 @@
 
 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<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),
-        new ChangeStatusPredicate(Change.Status.ABANDONED));
+    return or(ChangeStatusPredicate.open(),
+        ChangeStatusPredicate.forStatus(Change.Status.ABANDONED));
   }
 
   @SuppressWarnings("unchecked")
   @NoCostComputation
   @Rewrite("-status:abandoned")
   public Predicate<ChangeData> r00_notAbandoned() {
-    return or(ChangeStatusPredicate.open(dbProvider),
-        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;
+    return or(ChangeStatusPredicate.open(),
+        ChangeStatusPredicate.forStatus(Change.Status.MERGED));
   }
 
   private static final class InvalidProvider<T> implements Provider<T> {
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/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index ed64015..0f1a7bb 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;
@@ -78,7 +85,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 +126,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 +160,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 +170,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 +190,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 +235,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 +268,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;
   }
@@ -519,12 +559,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 +583,51 @@
     return submitRecords;
   }
 
+  public void setMergeable(boolean mergeable) {
+    this.mergeable = mergeable;
+  }
+
+  public boolean isMergeable() throws OrmException {
+    if (mergeable == null) {
+      Change c = change();
+      if (c.getStatus() == Change.Status.MERGED) {
+        mergeable = true;
+      } else {
+        PatchSet ps = currentPatchSet();
+        if (ps == null) {
+          throw new OrmException("Missing patch set for mergeability check");
+        }
+        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();
+    return MoreObjects.toStringHelper(this).addValue(getId()).toString();
   }
 
   public static class ChangedLines {
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..6c9a8c1 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
@@ -15,9 +15,11 @@
 package com.google.gerrit.server.query.change;
 
 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 +27,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 +73,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 +94,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 +120,18 @@
 
 
   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<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 +145,19 @@
     final TrackingFooters trackingFooters;
     final boolean allowsDrafts;
 
+    private final Provider<CurrentUser> self;
+
     @Inject
     @VisibleForTesting
-    public Arguments(Provider<ReviewDb> dbProvider,
+    public Arguments(Provider<ReviewDb> db,
         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 +170,131 @@
         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, 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<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.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, 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 +304,7 @@
 
   @Operator
   public Predicate<ChangeData> after(String value) throws QueryParseException {
-    return new AfterPredicate(schema(args.indexes), value);
+    return new AfterPredicate(value);
   }
 
   @Operator
@@ -253,47 +313,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));
-
     } else if (PAT_CHANGE_ID.matcher(query).matches()) {
       return new ChangeIdPredicate(args, 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(args, 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);
   }
 
   @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 +365,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 +389,7 @@
     }
 
     if ("mergeable".equalsIgnoreCase(value)) {
-      return new IsMergeablePredicate();
+      return new IsMergeablePredicate(schema(args.indexes), args.fillArgs);
     }
 
     try {
@@ -366,17 +425,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 +449,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 +468,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,7 +545,7 @@
   }
 
   @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);
   }
@@ -493,13 +554,12 @@
   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 +569,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 +639,8 @@
         user);
   }
 
-  public Predicate<ChangeData> is_visible() {
-    return visibleto(currentUser);
+  public Predicate<ChangeData> is_visible() throws QueryParseException {
+    return visibleto(args.getCurrentUser());
   }
 
   @Operator
@@ -636,47 +708,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 +737,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 +764,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));
@@ -804,11 +825,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..7d1cdd1 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,68 @@
  * 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> forStatus(Change.Status status) {
+    return parse(status.name());
+  }
+
+  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));
+  private ChangeStatusPredicate(Change.Status status) {
+    super(ChangeField.STATUS, canonicalize(status));
     this.status = status;
   }
 
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..32dff46 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
@@ -23,10 +23,10 @@
 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;
@@ -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;
           }
@@ -112,12 +111,7 @@
             Repository repo =
                 args.repoManager.openRepository(otherChange.getProject());
             try {
-              RevWalk rw = new RevWalk(repo) {
-                @Override
-                protected RevCommit createCommit(AnyObjectId id) {
-                  return new CodeReviewCommit(id);
-                }
-              };
+              RevWalk rw = CodeReviewCommit.newRevWalk(repo);
               try {
                 RevFlag canMergeFlag = rw.newFlag("CAN_MERGE");
                 CodeReviewCommit commit =
@@ -153,19 +147,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..40ae816
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -0,0 +1,58 @@
+// 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.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. */
+public class InternalChangeQuery {
+  private static Predicate<ChangeData> project(Project.NameKey projectName) {
+    return new ProjectPredicate(projectName.get());
+  }
+
+  private final QueryProcessor qp;
+
+  @Inject
+  InternalChangeQuery(QueryProcessor queryProcessor) {
+    qp = queryProcessor;
+  }
+
+  public InternalChangeQuery setLimit(int n) {
+    qp.setLimit(n);
+    return this;
+  }
+
+  public List<ChangeData> byProjectOpen(Project.NameKey projectName)
+      throws OrmException {
+    return query(and(project(projectName), 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..0c1d492 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;
@@ -49,7 +50,11 @@
   private final Arguments args;
   private final CurrentUser user;
 
-  IsStarredByPredicate(Arguments args, CurrentUser user) {
+  IsStarredByPredicate(Arguments args) throws QueryParseException {
+    this(args, args.getIdentifiedUser());
+  }
+
+  private IsStarredByPredicate(Arguments args, IdentifiedUser user) {
     super(predicates(args, 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/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/OutputStreamQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
new file mode 100644
index 0000000..8f33c01
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -0,0 +1,410 @@
+// 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.NoSuchChangeException;
+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);
+      } 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;
+      }
+    }
+  }
+
+  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..af87d1c 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,6 +15,7 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.common.collect.Lists;
+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;
@@ -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..96ca226 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,172 @@
 
 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.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 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) {
     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;
   }
 
-  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.
+      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;
-
-      case JSON:
-        out.print(gson.toJson(data));
-        out.print('\n');
-        break;
+  private int getEffectiveLimit(Predicate<ChangeData> p) {
+    List<Integer> possibleLimits = new ArrayList<>(3);
+    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/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/schema/AllProjectsCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index df49a01..07de4c1 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;
@@ -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..bf857ae 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,7 +68,6 @@
     allUsersCreator = auc;
     serverUser = au;
     dataSourceType = dst;
-    versionNbr = version.getVersionNbr();
   }
 
   public void create(final ReviewDb db) throws OrmException, IOException,
@@ -86,7 +81,7 @@
     }
 
     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..dc94eb5 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,7 +21,6 @@
 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.SQLException;
@@ -32,13 +31,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_103> C = Schema_103.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;
@@ -49,7 +45,7 @@
     this.versionNbr = guessVersion(getClass());
   }
 
-  public static int guessVersion(Class<?> c) {
+  private static int guessVersion(Class<?> c) {
     String n = c.getName();
     n = n.substring(n.lastIndexOf('_') + 1);
     while (n.startsWith("0"))
@@ -57,12 +53,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;
@@ -92,6 +82,7 @@
     try {
       final List<String> pruneList = Lists.newArrayList();
       s.pruneSchema(new StatementExecutor() {
+        @Override
         public void execute(String sql) {
           pruneList.add(sql);
         }
@@ -130,7 +121,13 @@
     }
   }
 
-  /** 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 {
   }
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-server/src/main/java/com/google/gerrit/server/schema/Current.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_100.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_100.java
index b16c977..0902194 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_100.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.
@@ -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_100 extends SchemaVersion {
+  @Inject
+  Schema_100(Provider<Schema_99> prior) {
+    super(prior);
+  }
 
-import java.lang.annotation.Retention;
-
-/** Indicates the {@link SchemaVersion} is the current one. */
-@Retention(RUNTIME)
-@BindingAnnotation
-public @interface Current {
+  // No database migration; merges are rechecked on reindex.
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpdatePrimaryKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_101.java
similarity index 61%
rename from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpdatePrimaryKeys.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_101.java
index 79bd730..888a30f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpdatePrimaryKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_101.java
@@ -12,10 +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.server.schema;
 
 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;
@@ -25,9 +24,9 @@
 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 com.google.inject.Provider;
 
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
@@ -40,87 +39,63 @@
 import java.util.Map;
 import java.util.TreeMap;
 
-public class UpdatePrimaryKeys implements InitStep {
+public class Schema_101 extends SchemaVersion {
 
   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;
+  Schema_101(Provider<Schema_100> prior) {
+    super(prior);
   }
 
   @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();
+  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;
     }
-  }
 
-  @Inject(optional = true)
-  void setSchemaFactory(SchemaFactory<ReviewDb> dbFactory) {
-    this.dbFactory = dbFactory;
+    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...");
+    JdbcExecutor executor = new JdbcExecutor(conn);
+    try {
+      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");
+    } finally {
+      executor.close();
+    }
   }
 
   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);
-        }
+    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();
     }
+    return corrections;
   }
 
   private List<String> relationPK(RelationModel rm) {
@@ -162,9 +137,10 @@
   }
 
   private void recreatePK(StatementExecutor executor, String tableName,
-      PrimaryKey pk) throws OrmException {
+      PrimaryKey pk, UpdateUI ui) throws OrmException {
     if (pk.oldNameInDb == null) {
-      ui.message("WARN: primary key for table %s didn't exist ... ", tableName);
+      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
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..bcefe78
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_102.java
@@ -0,0 +1,51 @@
+// 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.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+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 (Statement stmt = schema.getConnection().createStatement()) {
+      stmt.executeUpdate("DROP INDEX changes_byProjectOpen");
+      if (dialect instanceof DialectPostgreSQL) {
+        stmt.executeUpdate("CREATE INDEX changes_byProjectOpen"
+            + " ON changes (dest_project_name, last_updated_on)"
+            + " WHERE open = 'Y'");
+      } else {
+        stmt.executeUpdate("CREATE INDEX changes_byProjectOpen"
+            + " ON changes (open, dest_project_name, last_updated_on)");
+      }
+    }
+  }
+}
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_103.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_103.java
index b16c977..60a5213 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_103.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.
@@ -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_103 extends SchemaVersion {
+  @Inject
+  Schema_103(Provider<Schema_102> prior) {
+    super(prior);
+  }
 
-import java.lang.annotation.Retention;
-
-/** Indicates the {@link SchemaVersion} is the current one. */
-@Retention(RUNTIME)
-@BindingAnnotation
-public @interface Current {
+  // Adds originalSubject column
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java
index 76dbdf5..a44eed8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java
@@ -22,6 +22,7 @@
   @Inject
   Schema_52() {
     super(new Provider<SchemaVersion>() {
+      @Override
       public SchemaVersion get() {
         throw new ProvisionException("Cannot upgrade from 51");
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
index 697f303..c4f8458 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
@@ -34,9 +34,9 @@
 import com.google.gerrit.extensions.common.InheritableBoolean;
 import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.SystemConfig;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_59.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_59.java
index d90ecc1..1fdc182 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_59.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_59.java
@@ -14,29 +14,14 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import java.sql.SQLException;
-import java.util.List;
-
 public class Schema_59 extends SchemaVersion {
   @Inject
   Schema_59(Provider<Schema_58> prior) {
     super(prior);
   }
 
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException,
-      SQLException {
-    List<Change> allChanges = db.changes().all().toList();
-    for (Change change : allChanges) {
-      change.setMergeable(true);
-      change.setLastSha1MergeTested(null);
-    }
-    db.changes().update(allChanges);
-  }
-}
\ No newline at end of file
+  // Don't migrate columns; they are removed in Schema_100.
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java
index 636e0c6..ae046fa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -38,7 +39,6 @@
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -101,7 +101,7 @@
       ui.message("Moved contributor agreements to project.config");
 
       // Create the auto verify groups.
-      List<AccountGroup.UUID> adminGroupUUIDs = getAdministrateServerGroups(db, config);
+      List<AccountGroup.UUID> adminGroupUUIDs = getAdministrateServerGroups(config);
       for (ContributorAgreement agreement : agreements.values()) {
         if (agreement.getAutoVerify() != null) {
           getOrCreateGroupForIndividuals(db, config, adminGroupUUIDs, agreement);
@@ -273,7 +273,7 @@
   }
 
   private List<AccountGroup.UUID> getAdministrateServerGroups(
-      ReviewDb db, ProjectConfig cfg) {
+      ProjectConfig cfg) {
     List<PermissionRule> rules = cfg.getAccessSection(AccessSection.GLOBAL_CAPABILITIES)
        .getPermission(GlobalCapability.ADMINISTRATE_SERVER)
        .getRules();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_67.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_67.java
index bec2f3f..b57dcb9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_67.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_67.java
@@ -24,6 +24,7 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Statement;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_77.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_77.java
index a5166b6..0bcd56a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_77.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_77.java
@@ -80,7 +80,7 @@
         alterTable(db, "ALTER TABLE %s MODIFY %s varchar(255)");
       }
       migratePatchSetApprovals(db, labelTypes);
-      migrateLabelsToAllProjects(db, labelTypes);
+      migrateLabelsToAllProjects(labelTypes);
     } catch (RepositoryNotFoundException e) {
       throw new OrmException(e);
     } catch (SQLException e) {
@@ -104,9 +104,8 @@
     }
   }
 
-  private void migrateLabelsToAllProjects(ReviewDb db,
-      LegacyLabelTypes labelTypes) throws SQLException,
-      RepositoryNotFoundException, IOException, ConfigInvalidException {
+  private void migrateLabelsToAllProjects(LegacyLabelTypes labelTypes)
+      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
     Repository git = mgr.openRepository(allProjects);
 
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_81.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_81.java
index 1e3b2b7..2456929 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_81.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_81.java
@@ -60,14 +60,8 @@
   protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException,
       SQLException {
     try {
-      migrateStartReplicationCapability(db, scanForReplicationPlugin());
-    } catch (RepositoryNotFoundException e) {
-      throw new OrmException(e);
-    } catch (SQLException e) {
-      throw new OrmException(e);
-    } catch (IOException e) {
-      throw new OrmException(e);
-    } catch (ConfigInvalidException e) {
+      migrateStartReplicationCapability(scanForReplicationPlugin());
+    } catch (IOException | ConfigInvalidException e) {
       throw new OrmException(e);
     }
   }
@@ -87,9 +81,8 @@
     return matches;
   }
 
-  private void migrateStartReplicationCapability(ReviewDb db, File[] matches)
-      throws SQLException, RepositoryNotFoundException, IOException,
-      ConfigInvalidException {
+  private void migrateStartReplicationCapability(File[] matches)
+      throws RepositoryNotFoundException, IOException, ConfigInvalidException {
     Description d = new Description();
     if (matches == null || matches.length == 0) {
       d.what = Description.Action.REMOVE;
@@ -97,11 +90,11 @@
       d.what = Description.Action.RENAME;
       d.prefix = nameOf(matches[0]);
     }
-    migrateStartReplicationCapability(db, d);
+    migrateStartReplicationCapability(d);
   }
 
-  private void migrateStartReplicationCapability(ReviewDb db, Description d)
-      throws SQLException, RepositoryNotFoundException, IOException,
+  private void migrateStartReplicationCapability(Description d)
+      throws RepositoryNotFoundException, IOException,
       ConfigInvalidException {
     Repository git = mgr.openRepository(allProjects);
     try {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_99.java
similarity index 65%
copy from gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_99.java
index cd07320..b7fab7f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_99.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 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,16 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd;
+package com.google.gerrit.server.schema;
 
-public class GerritUiOptions {
-  private final boolean headless;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
 
-  public GerritUiOptions(boolean headless) {
-    this.headless = headless;
-  }
-
-  public boolean enableDefaultUi() {
-    return !headless;
+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..51e21a7 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) {
     }
   };
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..9a07354 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,10 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.List;
 
 @Singleton
-@Export(DefaultSecureStore.NAME)
 public class DefaultSecureStore implements SecureStore {
-  public static final String NAME = "default";
-
   private final FileBasedConfig sec;
 
   @Inject
@@ -52,6 +49,11 @@
   }
 
   @Override
+  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);
@@ -62,6 +64,17 @@
   }
 
   @Override
+  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);
+    }
+    save();
+  }
+
+  @Override
   public void unset(String section, String subsection, String name) {
     sec.unset(section, subsection, name);
     save();
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..19d183f 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,17 @@
 
 package com.google.gerrit.server.securestore;
 
-import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import java.util.List;
 
-@ExtensionPoint
 public interface SecureStore {
 
   String get(String section, String subsection, String name);
 
+  String[] getList(String section, String subsection, String name);
+
   void set(String section, String subsection, String name, String value);
 
+  void setList(String section, String subsection, String name, List<String> values);
+
   void unset(String section, String subsection, String name);
 }
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
index b925105..0da7567 100644
--- 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.securestore;
 
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
 
 import java.io.File;
@@ -57,7 +58,7 @@
 
   @Override
   public String toString() {
-    return Objects.toStringHelper(this).add("storeName", storeName)
+    return MoreObjects.toStringHelper(this).add("storeName", storeName)
         .add("className", className).add("file", pluginFile).toString();
   }
 
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..c830ee6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/SecureStoreProvider.java
@@ -0,0 +1,85 @@
+// 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.SiteLibraryLoaderUtil;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+
+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 secureStoreClassName;
+
+  private SecureStore instance;
+
+  @Inject
+  SecureStoreProvider(
+      Injector injector,
+      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);
+    }
+    this.injector = injector;
+    this.libdir = sitePaths.lib_dir;
+    this.secureStoreClassName =
+        cfg.getString("gerrit", null, "secureStoreClass");
+  }
+
+  @Override
+  public SecureStore get() {
+    if (instance == null) {
+      instance = injector.getInstance(getSecureStoreImpl());
+    }
+    return instance;
+  }
+
+  @SuppressWarnings("unchecked")
+  private Class<? extends SecureStore> getSecureStoreImpl() {
+    if (Strings.isNullOrEmpty(secureStoreClassName)) {
+      return DefaultSecureStore.class;
+    }
+
+    SiteLibraryLoaderUtil.loadSiteLib(libdir);
+    try {
+      return (Class<? extends SecureStore>) Class.forName(secureStoreClassName);
+    } catch (ClassNotFoundException e) {
+      String msg =
+          String.format("Cannot load secure store class: %s",
+              secureStoreClassName);
+      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..0192355 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
@@ -106,6 +106,11 @@
   }
 
   @Override
+  public int hashCode() {
+    return 17 * value  + name.hashCode();
+  }
+
+  @Override
   public String toString() {
     return format();
   }
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/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_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/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-types.properties
index 817790f..a2b6770 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-types.properties
@@ -7,12 +7,16 @@
 cs = text/x-csharp
 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
@@ -20,6 +24,7 @@
 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 +36,13 @@
 py = text/x-python
 r = text/r-src
 rb = text/x-ruby
+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..206e4c6 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,12 @@
 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.TimeUtil;
 import com.google.gerrit.common.changes.Side;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -37,22 +39,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 +69,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 +109,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 Drafts 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<DraftResource>> draftViews =
+        createMock(DynamicMap.class);
+    final TypeLiteral<DynamicMap<RestView<DraftResource>>> draftViewsType =
+        new TypeLiteral<DynamicMap<RestView<DraftResource>>>() {};
+
+    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 +166,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 +231,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,32 +244,56 @@
         "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);
@@ -250,35 +302,64 @@
   }
 
   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 +371,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 +387,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 +403,53 @@
       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));
+        MoreObjects.firstNonNull(ci.side, Side.REVISION));
+    assertEquals(TimeUtil.roundToSecond(plc.getWrittenOn()),
+        TimeUtil.roundToSecond(ci.updated));
     assertEquals(plc.getRange(), ci.range);
   }
 
   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 +457,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..51aa283
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java
@@ -0,0 +1,279 @@
+// 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 static com.google.gerrit.testutil.TestChanges.incrementPatchSet;
+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.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.testutil.InMemoryDatabase;
+import com.google.gerrit.testutil.InMemoryRepositoryManager;
+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.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 {
+    schemaFactory = InMemoryDatabase.newDatabase();
+    schemaFactory.create();
+    db = schemaFactory.open();
+    repoManager = new InMemoryRepositoryManager();
+    checker = new ConsistencyChecker(Providers.<ReviewDb> of(db), repoManager);
+    project = new Project.NameKey("repo");
+    repo = new TestRepository<>(repoManager.createRepository(project));
+    userId = new Account.Id(1);
+    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 = newChange(project, userId);
+    db.changes().insert(singleton(c));
+    RevCommit commit1 = repo.branch(c.currentPatchSetId().toRefName()).commit()
+        .parent(tip).create();
+    PatchSet ps1 = newPatchSet(c.currentPatchSetId(), commit1, userId);
+    db.patchSets().insert(singleton(ps1));
+
+    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));
+
+    assertProblems(c);
+  }
+
+  @Test
+  public void validMergedChange() throws Exception {
+    Change c = newChange(project, userId);
+    c.setStatus(Change.Status.MERGED);
+    db.changes().insert(singleton(c));
+    RevCommit commit1 = repo.branch(c.currentPatchSetId().toRefName()).commit()
+        .parent(tip).create();
+    PatchSet ps1 = newPatchSet(c.currentPatchSetId(), commit1, userId);
+    db.patchSets().insert(singleton(ps1));
+
+    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));
+    PatchSet ps = newPatchSet(c.currentPatchSetId(),
+        ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), userId);
+    db.patchSets().insert(singleton(ps));
+    assertProblems(c, "Destination repository not found: otherproject");
+  }
+
+  @Test
+  public void invalidRevision() throws Exception {
+    Change c = newChange(project, userId);
+    db.changes().insert(singleton(c));
+
+    PatchSet ps = new PatchSet(c.currentPatchSetId());
+    ps.setRevision(new RevId("fooooooooooooooooooooooooooooooooooooooo"));
+    ps.setUploader(userId);
+    ps.setCreatedOn(TimeUtil.nowTs());
+    db.patchSets().insert(singleton(ps));
+
+    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));
+
+    assertProblems(c,
+        "Invalid revision on patch set 1:"
+        + " fooooooooooooooooooooooooooooooooooooooo");
+  }
+
+  @Test
+  public void patchSetObjectMissing() throws Exception {
+    Change c = newChange(project, userId);
+    db.changes().insert(singleton(c));
+    PatchSet ps = newPatchSet(c.currentPatchSetId(),
+        ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), userId);
+    db.patchSets().insert(singleton(ps));
+
+    assertProblems(c,
+        "Object missing: patch set 1: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+  }
+
+  @Test
+  public void currentPatchSetMissing() throws Exception {
+    Change c = newChange(project, userId);
+    db.changes().insert(singleton(c));
+    assertProblems(c, "Current patch set 1 not found");
+  }
+
+  @Test
+  public void duplicatePatchSetRevisions() throws Exception {
+    Change c = newChange(project, userId);
+    db.changes().insert(singleton(c));
+    RevCommit commit1 = repo.branch(c.currentPatchSetId().toRefName()).commit()
+        .parent(tip).create();
+    PatchSet ps1 = newPatchSet(c.currentPatchSetId(), commit1, userId);
+    db.patchSets().insert(singleton(ps1));
+
+    incrementPatchSet(c);
+    PatchSet ps2 = newPatchSet(c.currentPatchSetId(), commit1, userId);
+    db.patchSets().insert(singleton(ps2));
+
+    assertProblems(c,
+        "Multiple patch sets pointing to " + commit1.name() + ": [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 = newChange(project, userId);
+    db.changes().insert(singleton(c));
+    RevCommit commit = repo.commit().create();
+    PatchSet ps = newPatchSet(c.currentPatchSetId(), commit, userId);
+    db.patchSets().insert(singleton(ps));
+
+    assertProblems(c, "Destination ref not found (may be new branch): master");
+  }
+
+  @Test
+  public void mergedChangeIsNotMerged() throws Exception {
+    Change c = newChange(project, userId);
+    c.setStatus(Change.Status.MERGED);
+    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,
+        "Patch set 1 (" + commit.name() + ") is not merged into destination ref"
+        + " master (" + tip.name() + "), but change status is MERGED");
+  }
+
+  @Test
+  public void newChangeIsMerged() throws Exception {
+    Change c = newChange(project, userId);
+    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));
+    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 = newChange(project, userId);
+    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));
+    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 void assertProblems(Change c, String... expected) {
+    assertThat(Lists.transform(checker.check(c).problems(),
+          new Function<ProblemInfo, String>() {
+            @Override
+            public String apply(ProblemInfo in) {
+              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..dc9724c 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();
@@ -125,6 +126,7 @@
         .setAnnotated(true).call();
   }
 
+  @Override
   @After
   public void tearDown() throws Exception {
     revWalk.release();
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..426fb93
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/edit/ChangeEditTest.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.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 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 = ChangeEditUtil.editRefName(accountId, changeId, psId);
+    assertEquals("refs/users/42/1000042/edit-56414/50", refName);
+  }
+}
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..a3c5286 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,6 +22,7 @@
 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;
@@ -33,7 +34,6 @@
 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);
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..f4f989f 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;
@@ -749,7 +749,7 @@
 
     expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
     final ResultSet<SubmoduleSubscription> incorrectSubscriptions =
-        new ListResultSet<SubmoduleSubscription>(Collections
+        new ListResultSet<>(Collections
             .singletonList(new SubmoduleSubscription(sourceBranchNameKey,
                 targetBranchNameKey, "target-project")));
     expect(subscriptions.bySubmodule(targetBranchNameKey)).andReturn(
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..c5ebc02 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, 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..d9f86bd 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;
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..53d9fb1
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -0,0 +1,218 @@
+// 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.release();
+  }
+
+  @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 {
+    ObjectInserter ins = testRepo.getRepository().newObjectInserter();
+    try {
+      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;
+    } finally {
+      ins.release();
+    }
+  }
+
+  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..c356a19 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,54 @@
   }
 
   @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();
+    RevWalk walk = new RevWalk(repo);
+    try {
+      RevCommit commit = walk.parseCommit(update.getRevision());
+      walk.parseBody(commit);
+      assertTrue(commit.getFullMessage().endsWith("Hashtags: tag1,tag2\n"));
+    } finally {
+      walk.release();
+    }
+  }
+
+  @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 +413,114 @@
   }
 
   @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);
+
+    RevWalk rw = new RevWalk(repo);
+    try {
+      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();
+      rw.release();
+    }
+  }
+
+  @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());
+
+    RevWalk rw = new RevWalk(repo);
+    try {
+      bru.execute(rw, NullProgressMonitor.INSTANCE);
+    } finally {
+      rw.release();
+    }
+
+    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 +543,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 +636,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 +673,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,28 +686,28 @@
     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);
@@ -897,14 +728,14 @@
         + "1:1-2:1\n"
         + CommentsInNotesUtil.formatTime(serverIdent, time1) + "\n"
         + "Author: Other Account <2@gerrit>\n"
-        + "UUID: uuid\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: uuid\n"
+        + "UUID: uuid2\n"
         + "Bytes: 9\n"
         + "comment 2\n"
         + "\n"
@@ -913,7 +744,7 @@
         + "3:1-4:1\n"
         + CommentsInNotesUtil.formatTime(serverIdent, time3) + "\n"
         + "Author: Other Account <2@gerrit>\n"
-        + "UUID: uuid\n"
+        + "UUID: uuid3\n"
         + "Bytes: 9\n"
         + "comment 3\n"
         + "\n",
@@ -924,7 +755,8 @@
   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,19 +765,19 @@
     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);
@@ -966,14 +798,14 @@
         + "1:1-2:1\n"
         + CommentsInNotesUtil.formatTime(serverIdent, time1) + "\n"
         + "Author: Other Account <2@gerrit>\n"
-        + "UUID: uuid\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: uuid\n"
+        + "UUID: uuid2\n"
         + "Bytes: 9\n"
         + "comment 2\n"
         + "\n",
@@ -986,7 +818,8 @@
       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 +827,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 +860,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 +871,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 +920,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 +928,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 +964,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 +976,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 +999,185 @@
     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 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 ChangeNotes newNotes(Change c) throws OrmException {
-    return new ChangeNotes(repoManager, c).load();
-  }
+    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 truncate(Timestamp ts) {
-    return new Timestamp((ts.getTime() / 1000) * 1000);
-  }
+    ChangeNotes notes = newNotes(c);
+    Multimap<PatchSet.Id, PatchLineComment> commentsForBase =
+        notes.getBaseComments();
+    Multimap<PatchSet.Id, PatchLineComment> commentsForPs =
+        notes.getPatchSetComments();
 
-  private static Timestamp after(Change c, long millis) {
-    return new Timestamp(c.getCreatedOn().getTime() + millis);
-  }
-
-  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..86f9702
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -0,0 +1,260 @@
+// 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;
+    }
+    RevWalk walk = new RevWalk(repo);
+    try {
+      RevCommit commit = walk.parseCommit(id);
+      walk.parseBody(commit);
+      return commit;
+    } finally {
+      walk.release();
+    }
+  }
+
+  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 dc3529c..6f159fc 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
@@ -55,6 +55,8 @@
   private final AccountGroup.UUID fixers = new AccountGroup.UUID("test.fixers");
   private Project.NameKey localKey = new Project.NameKey("local");
   private ProjectConfig local;
+  private Project.NameKey parentKey = new Project.NameKey("parent");
+  private ProjectConfig parent;
   private final Util util;
 
   public RefControlTest() {
@@ -63,9 +65,14 @@
 
   @Before
   public void setUp() throws Exception {
+    parent = new ProjectConfig(parentKey);
+    parent.load(newRepository(parentKey));
+    util.add(parent);
+
     local = new ProjectConfig(localKey);
     local.load(newRepository(localKey));
     util.add(local);
+    local.getProject().setParentName(parentKey);
   }
 
   @Test
@@ -139,8 +146,8 @@
 
   @Test
   public void testInheritRead_SingleBranchDeniesUpload() {
-    allow(util.getParentConfig(), READ, REGISTERED_USERS, "refs/*");
-    allow(util.getParentConfig(), PUSH, REGISTERED_USERS, "refs/for/refs/*");
+    allow(parent, READ, REGISTERED_USERS, "refs/*");
+    allow(parent, PUSH, REGISTERED_USERS, "refs/for/refs/*");
     allow(local, READ, REGISTERED_USERS, "refs/heads/foobar");
     doNotInherit(local, READ, "refs/heads/foobar");
     doNotInherit(local, PUSH, "refs/for/refs/heads/foobar");
@@ -157,8 +164,8 @@
 
   @Test
   public void testBlockPushDrafts() {
-    allow(util.getParentConfig(), PUSH, REGISTERED_USERS, "refs/for/refs/*");
-    block(util.getParentConfig(), PUSH, ANONYMOUS_USERS, "refs/drafts/*");
+    allow(parent, PUSH, REGISTERED_USERS, "refs/for/refs/*");
+    block(parent, PUSH, ANONYMOUS_USERS, "refs/drafts/*");
 
     ProjectControl u = util.user(local);
     assertTrue("can upload refs/heads/master",
@@ -169,8 +176,8 @@
 
   @Test
   public void testBlockPushDraftsUnblockAdmin() {
-    block(util.getParentConfig(), PUSH, ANONYMOUS_USERS, "refs/drafts/*");
-    allow(util.getParentConfig(), PUSH, ADMIN, "refs/drafts/*");
+    block(parent, PUSH, ANONYMOUS_USERS, "refs/drafts/*");
+    allow(parent, PUSH, ADMIN, "refs/drafts/*");
 
     assertTrue("push is blocked for anonymous to refs/drafts/master",
         util.user(local).controlForRef("refs/drafts/refs/heads/master")
@@ -182,8 +189,8 @@
 
   @Test
   public void testInheritRead_SingleBranchDoesNotOverrideInherited() {
-    allow(util.getParentConfig(), READ, REGISTERED_USERS, "refs/*");
-    allow(util.getParentConfig(), PUSH, REGISTERED_USERS, "refs/for/refs/*");
+    allow(parent, READ, REGISTERED_USERS, "refs/*");
+    allow(parent, PUSH, REGISTERED_USERS, "refs/for/refs/*");
     allow(local, READ, REGISTERED_USERS, "refs/heads/foobar");
 
     ProjectControl u = util.user(local);
@@ -198,20 +205,20 @@
 
   @Test
   public void testInheritDuplicateSections() throws Exception {
-    allow(util.getParentConfig(), READ, ADMIN, "refs/*");
+    allow(parent, READ, ADMIN, "refs/*");
     allow(local, READ, DEVS, "refs/heads/*");
-    local.getProject().setParentName(util.getParentConfig().getProject().getName());
     assertTrue("a can read", util.user(local, "a", ADMIN).isVisible());
 
-    local = new ProjectConfig(new Project.NameKey("local"));
+    local = new ProjectConfig(localKey);
     local.load(newRepository(localKey));
+    local.getProject().setParentName(parentKey);
     allow(local, READ, DEVS, "refs/*");
     assertTrue("d can read", util.user(local, "d", DEVS).isVisible());
   }
 
   @Test
   public void testInheritRead_OverrideWithDeny() {
-    allow(util.getParentConfig(), READ, REGISTERED_USERS, "refs/*");
+    allow(parent, READ, REGISTERED_USERS, "refs/*");
     deny(local, READ, REGISTERED_USERS, "refs/*");
 
     ProjectControl u = util.user(local);
@@ -220,7 +227,7 @@
 
   @Test
   public void testInheritRead_AppendWithDenyOfRef() {
-    allow(util.getParentConfig(), READ, REGISTERED_USERS, "refs/*");
+    allow(parent, READ, REGISTERED_USERS, "refs/*");
     deny(local, READ, REGISTERED_USERS, "refs/heads/*");
 
     ProjectControl u = util.user(local);
@@ -232,7 +239,7 @@
 
   @Test
   public void testInheritRead_OverridesAndDeniesOfRef() {
-    allow(util.getParentConfig(), READ, REGISTERED_USERS, "refs/*");
+    allow(parent, READ, REGISTERED_USERS, "refs/*");
     deny(local, READ, REGISTERED_USERS, "refs/*");
     allow(local, READ, REGISTERED_USERS, "refs/heads/*");
 
@@ -245,7 +252,7 @@
 
   @Test
   public void testInheritSubmit_OverridesAndDeniesOfRef() {
-    allow(util.getParentConfig(), SUBMIT, REGISTERED_USERS, "refs/*");
+    allow(parent, SUBMIT, REGISTERED_USERS, "refs/*");
     deny(local, SUBMIT, REGISTERED_USERS, "refs/*");
     allow(local, SUBMIT, REGISTERED_USERS, "refs/heads/*");
 
@@ -257,7 +264,7 @@
 
   @Test
   public void testCannotUploadToAnyRef() {
-    allow(util.getParentConfig(), READ, REGISTERED_USERS, "refs/*");
+    allow(parent, READ, REGISTERED_USERS, "refs/*");
     allow(local, READ, DEVS, "refs/heads/*");
     allow(local, PUSH, DEVS, "refs/for/refs/heads/*");
 
@@ -307,7 +314,7 @@
   @Test
   public void testSortWithRegex() {
     allow(local, READ, DEVS, "^refs/heads/.*");
-    allow(util.getParentConfig(), READ, ANONYMOUS_USERS, "^refs/heads/.*-QA-.*");
+    allow(parent, READ, ANONYMOUS_USERS, "^refs/heads/.*-QA-.*");
 
     ProjectControl u = util.user(local, DEVS), d = util.user(local, DEVS);
     assertTrue("u can read", u.controlForRef("refs/heads/foo-QA-bar").isVisible());
@@ -317,7 +324,7 @@
   @Test
   public void testBlockRule_ParentBlocksChild() {
     allow(local, PUSH, DEVS, "refs/tags/*");
-    block(util.getParentConfig(), PUSH, ANONYMOUS_USERS, "refs/tags/*");
+    block(parent, PUSH, ANONYMOUS_USERS, "refs/tags/*");
     ProjectControl u = util.user(local, DEVS);
     assertFalse("u can't update tag", u.controlForRef("refs/tags/V10").canUpdate());
   }
@@ -326,7 +333,7 @@
   public void testBlockRule_ParentBlocksChildEvenIfAlreadyBlockedInChild() {
     allow(local, PUSH, DEVS, "refs/tags/*");
     block(local, PUSH, ANONYMOUS_USERS, "refs/tags/*");
-    block(util.getParentConfig(), PUSH, ANONYMOUS_USERS, "refs/tags/*");
+    block(parent, PUSH, ANONYMOUS_USERS, "refs/tags/*");
 
     ProjectControl u = util.user(local, DEVS);
     assertFalse("u can't update tag", u.controlForRef("refs/tags/V10").canUpdate());
@@ -335,7 +342,7 @@
   @Test
   public void testBlockLabelRange_ParentBlocksChild() {
     allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
-    block(util.getParentConfig(), LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+    block(parent, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
 
     ProjectControl u = util.user(local, DEVS);
 
@@ -350,7 +357,7 @@
   public void testBlockLabelRange_ParentBlocksChildEvenIfAlreadyBlockedInChild() {
     allow(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
     block(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
-    block(util.getParentConfig(), LABEL + "Code-Review", -2, +2, DEVS,
+    block(parent, LABEL + "Code-Review", -2, +2, DEVS,
         "refs/heads/*");
 
     ProjectControl u = util.user(local, DEVS);
@@ -412,7 +419,7 @@
 
   @Test
   public void testUnblockInLocal_Fails() {
-    block(util.getParentConfig(), PUSH, ANONYMOUS_USERS, "refs/heads/*");
+    block(parent, PUSH, ANONYMOUS_USERS, "refs/heads/*");
     allow(local, PUSH, fixers, "refs/heads/*");
 
     ProjectControl f = util.user(local, fixers);
@@ -421,8 +428,8 @@
 
   @Test
   public void testUnblockInParentBlockInLocal() {
-    block(util.getParentConfig(), PUSH, ANONYMOUS_USERS, "refs/heads/*");
-    allow(util.getParentConfig(), PUSH, DEVS, "refs/heads/*");
+    block(parent, PUSH, ANONYMOUS_USERS, "refs/heads/*");
+    allow(parent, PUSH, DEVS, "refs/heads/*");
     block(local, PUSH, DEVS, "refs/heads/*");
 
     ProjectControl d = util.user(local, DEVS);
@@ -430,7 +437,7 @@
   }
 
   @Test
-  public void testUnblockVisibilityByREGISTEREDUsers() {
+  public void testUnblockVisibilityByRegisteredUsers() {
     block(local, READ, ANONYMOUS_USERS, "refs/heads/*");
     allow(local, READ, REGISTERED_USERS, "refs/heads/*");
 
@@ -440,7 +447,7 @@
 
   @Test
   public void testUnblockInLocalVisibilityByRegisteredUsers_Fails() {
-    block(util.getParentConfig(), READ, ANONYMOUS_USERS, "refs/heads/*");
+    block(parent, READ, ANONYMOUS_USERS, "refs/heads/*");
     allow(local, READ, REGISTERED_USERS, "refs/heads/*");
 
     ProjectControl u = util.user(local, REGISTERED_USERS);
@@ -459,7 +466,7 @@
 
   @Test
   public void testUnblockInLocalForceEditTopicName_Fails() {
-    block(util.getParentConfig(), EDIT_TOPIC_NAME, ANONYMOUS_USERS, "refs/heads/*");
+    block(parent, EDIT_TOPIC_NAME, ANONYMOUS_USERS, "refs/heads/*");
     allow(local, EDIT_TOPIC_NAME, DEVS, "refs/heads/*").setForce(true);
 
     ProjectControl u = util.user(local, REGISTERED_USERS);
@@ -502,7 +509,7 @@
 
   @Test
   public void testUnblockInLocalRange_Fails() {
-    block(util.getParentConfig(), LABEL + "Code-Review", -1, 1, ANONYMOUS_USERS,
+    block(parent, LABEL + "Code-Review", -1, 1, ANONYMOUS_USERS,
         "refs/heads/*");
     allow(local, LABEL + "Code-Review", -2, +2, DEVS, "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 c0f35ba..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,19 +184,21 @@
   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("parent");
-  private final ProjectConfig parent = new ProjectConfig(allProjectsName);
+  private final AllProjectsName allProjectsName =
+      new AllProjectsName("All-Projects");
+  private final ProjectConfig allProjects;
 
   public Util() {
     all = new HashMap<>();
     repoManager = new InMemoryRepositoryManager();
     try {
       Repository repo = repoManager.createRepository(allProjectsName);
-      parent.load(repo);
-      parent.getLabelSections().put(CR.getName(), CR);
-      add(parent);
+      allProjects = new ProjectConfig(new Project.NameKey(allProjectsName.get()));
+      allProjects.load(repo);
+      allProjects.getLabelSections().put(CR.getName(), CR);
+      add(allProjects);
     } catch (IOException | ConfigInvalidException e) {
       throw new RuntimeException(e);
     }
@@ -239,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);
       }
     });
 
@@ -281,25 +297,26 @@
       injector.getInstance(ChangeControl.AssistedFactory.class);
   }
 
-  public ProjectConfig getParentConfig() {
-    return this.parent;
-  }
-
-  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) {
@@ -312,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..482751d 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;
     }
@@ -933,19 +1128,29 @@
   }
 
   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 +1163,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 +1184,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/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/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/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/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 6b2f7c7..72495b3 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
@@ -26,7 +26,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;
@@ -49,10 +48,10 @@
 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.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;
@@ -79,6 +78,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");
@@ -90,7 +94,6 @@
     cfg.setBoolean("index", "lucene", "testInmemory", true);
     cfg.setInt("index", "lucene", "testVersion",
         ChangeSchemas.getLatest().getVersion());
-    return cfg;
   }
 
   private final Config cfg;
@@ -122,8 +125,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(
@@ -149,6 +150,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() {
@@ -160,7 +163,6 @@
     install(new DefaultCacheFactory.Module());
     install(new SmtpEmailSender.Module());
     install(new SignedTokenEmailTokenVerifier.Module());
-    install(new MergeabilityChecksExecutorModule());
 
     IndexType indexType = null;
     try {
@@ -191,9 +193,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 97%
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..2f3453b 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;
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..11ba665 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,91 @@
 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) {
+    PatchSet ps = new PatchSet(id);
+    ps.setRevision(new RevId(revision.name()));
+    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-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..f7d4f8f 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();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index a2a5632..93d2e5b 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -92,6 +92,7 @@
     }
   }
 
+  @Override
   public boolean authenticate(String username,
       final PublicKey suppliedKey, final ServerSession session) {
     final SshSession sd = session.getAttribute(SshSession.KEY);
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 6051442..94967f0 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
@@ -145,7 +145,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
@@ -261,26 +261,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) {
@@ -298,12 +298,12 @@
 
   @Override
   public synchronized void stop() {
-    if (acceptor != null) {
+    if (daemonAcceptor != null) {
       try {
-        acceptor.dispose();
+        daemonAcceptor.dispose();
         log.info("Stopped Gerrit SSHD");
       } finally {
-        acceptor = null;
+        daemonAcceptor = null;
       }
     }
   }
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..e093cfb 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;
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/CreateProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index fb39dee5..219dab5 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
@@ -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..cd20f2a 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,6 +31,7 @@
   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");
 
@@ -98,5 +99,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..0afdd60 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;
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..7753961 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;
@@ -30,8 +31,8 @@
 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,6 +40,7 @@
 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;
@@ -50,6 +52,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;
@@ -101,6 +105,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,6 +117,9 @@
   @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);
@@ -147,6 +157,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 +172,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 +235,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 +302,10 @@
         applyReview(patchSet, review);
       }
 
+      if (rebaseChange){
+        revisionApi(patchSet).rebase();
+      }
+
       if (submitChange) {
         revisionApi(patchSet).submit();
       }
@@ -251,8 +315,7 @@
       } else if (deleteDraftPatchSet) {
         revisionApi(patchSet).delete();
       }
-    } catch (IllegalStateException | InvalidChangeOperationException
-        | RestApiException e) {
+    } catch (IllegalStateException | RestApiException e) {
       throw error(e.getMessage());
     }
   }
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..b484a3f 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,21 +280,30 @@
     }
   }
 
-  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()) {
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..0d7cf4d 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
@@ -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/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-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 fc73973..1d2ee84 100644
--- a/gerrit-war/BUCK
+++ b/gerrit-war/BUCK
@@ -9,8 +9,10 @@
     '//gerrit-httpd:httpd',
     '//gerrit-lucene:lucene',
     '//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 ef5e53f..81d8498 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-SNAPSHOT</version>
+  <version>2.11-SNAPSHOT</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..37f8d25 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;
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 56b092e..266c50d 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
@@ -27,7 +27,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;
@@ -37,6 +36,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;
@@ -278,7 +278,6 @@
     modules.add(new WorkQueue.Module());
     modules.add(new ChangeHookRunner.Module());
     modules.add(new ReceiveCommitsExecutorModule());
-    modules.add(new MergeabilityChecksExecutorModule());
     modules.add(new IntraLineWorkerPool.Module());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
     modules.add(new InternalAccountDirectory.Module());
@@ -310,7 +309,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());
@@ -339,11 +338,13 @@
     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) {
       modules.add(new OpenIdModule());
     }
+    modules.add(sysInjector.getInstance(GetUserFilter.Module.class));
 
     return sysInjector.createChildInjector(modules);
   }
diff --git a/lib/BUCK b/lib/BUCK
index 42170ee..634df67 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -35,9 +35,9 @@
 
 maven_jar(
   name = 'gwtjsonrpc',
-  id = 'gwtjsonrpc:gwtjsonrpc:1.5',
-  bin_sha1 = '8995287e2c3c866e826d06993904e2c8d7961e4b',
-  src_sha1 = 'c9461f6c0490f26720e3ff15b5607320eab89d96',
+  id = 'gwtjsonrpc:gwtjsonrpc:1.6',
+  bin_sha1 = '3673018b2d26a428d8fac6d2defce45e79f76452',
+  src_sha1 = '3d638e807dc8e6435f819eaade9b45d370cd164f',
   license = 'Apache2.0',
   repository = GERRIT,
 )
@@ -51,8 +51,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,8 +116,8 @@
 
 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'],
 )
@@ -132,8 +132,8 @@
 
 maven_jar(
   name = 'parboiled-java',
-  id = 'org.parboiled:parboiled-java:1.1.6',
-  sha1 = 'cb2ffa720f75b2fce8cfd1875599319e75ea9557',
+  id = 'org.parboiled:parboiled-java:1.1.6-14-g76586a4',
+  sha1 = '8cbe0bd4d41c4e2dc0db1107719ca44132f87fed',
   license = 'Apache2.0',
   deps = [
     ':parboiled-core',
@@ -142,7 +142,8 @@
     '//lib/ow2:ow2-asm-util',
   ],
   attach_source = False,
-  visibility = [],
+  visibility = ['//gerrit-server:server_tests'],
+  repository = GERRIT,
 )
 
 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/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..22fbd91
--- /dev/null
+++ b/lib/auto/BUCK
@@ -0,0 +1,39 @@
+include_defs('//lib/maven.defs')
+
+# TODO(dborowitz): All rules but auto-value are public only because of the hacky
+# way we have to include them via auto_value.defs.
+maven_jar(
+  name = 'auto-common',
+  id = 'com.google.auto:auto-common:0.3',
+  sha1 = '4073ab16ab4aceb9a217273da6442166bf51ae16',
+  license = 'Apache2.0',
+  deps = ['//lib:guava'],
+  visibility = ['PUBLIC'],
+)
+
+maven_jar(
+  name = 'auto-service',
+  id = 'com.google.auto.service:auto-service:1.0-rc2',
+  sha1 = '51033a5b8fcf7039159e35b6878f106ccd5fb35f',
+  license = 'Apache2.0',
+  deps = [
+    ':auto-common',
+    '//lib:guava',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+maven_jar(
+  name = 'auto-value',
+  id = 'com.google.auto.value:auto-value:1.0-rc2',
+  sha1 = '73141b5aa77021f058d1a5391595d04ef17e8602',
+  license = 'Apache2.0',
+  deps = [
+    ':auto-common',
+    ':auto-service',
+    '//lib:guava',
+    '//lib:velocity',
+    '//lib/ow2:ow2-asm',
+  ],
+  visibility = ['PUBLIC'],
+)
diff --git a/lib/auto/auto_value.defs b/lib/auto/auto_value.defs
new file mode 100644
index 0000000..59a114f
--- /dev/null
+++ b/lib/auto/auto_value.defs
@@ -0,0 +1,25 @@
+# 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:guava',
+  '//lib:velocity',
+  '//lib/auto:auto-common',
+  '//lib/auto:auto-service',
+  '//lib/auto:auto-value',
+  '//lib/commons:collections',
+  '//lib/commons:lang',
+  '//lib/commons:oro',
+  '//lib/ow2:ow2-asm',
+]
+
+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..cc0880e
--- /dev/null
+++ b/lib/codemirror/cm.defs
@@ -0,0 +1,81 @@
+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',
+  '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..d28a0b6 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.2.v20140723'
 EXCLUDE = ['about.html']
 
 maven_jar(
   name = 'servlet',
   id = 'org.eclipse.jetty:jetty-servlet:' + VERSION,
-  sha1 = 'f2327faaf09a3f306babc209f9a7ae01b1528464',
+  sha1 = '98d3dde183295cf3356c7ac2c968e5b684fcaba1',
   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 = 'b66f8f4b9afd82af24b9f7ffcd5312eb628ee0c9',
   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 = '457063f20e3676ddd7e7c4dfce90ef74dd54a276',
   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 = 'dbf076744c15ef879c89464c1d0cfd3a250a9766',
   license = 'Apache2.0',
   exported_deps = [
     ':continuation',
@@ -62,7 +49,7 @@
 maven_jar(
   name = 'jmx',
   id = 'org.eclipse.jetty:jetty-jmx:' + VERSION,
-  sha1 = '1258d5ac618b120026da8a82283e6cb8ff4638a6',
+  sha1 = '64b3ff4d0cee0acee363a1c98193332e8f845a6e',
   license = 'Apache2.0',
   exported_deps = [
     ':continuation',
@@ -74,7 +61,7 @@
 maven_jar(
   name = 'continuation',
   id = 'org.eclipse.jetty:jetty-continuation:' + VERSION,
-  sha1 = 'e5bf20cdcd9c2878677f3c0f43baea2725f8c59e',
+  sha1 = 'a39e8cee9c36d159c6ab3283eb59f8bc2fd16c43',
   license = 'Apache2.0',
   exclude = EXCLUDE,
 )
@@ -82,7 +69,7 @@
 maven_jar(
   name = 'http',
   id = 'org.eclipse.jetty:jetty-http:' + VERSION,
-  sha1 = 'a132617cb898afc9d4ce5d586e11ad90b9831fff',
+  sha1 = '6301fc80fa66d9b1462613fd9c137402663b4d1e',
   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 = 'f2237b1705fcac83cedf8390eb4b29ac845cde1c',
   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 = '2e199b14a57b0336b4f2c9fdf8add525d6112833',
   license = 'Apache2.0',
   exclude = EXCLUDE,
   visibility = [],
diff --git a/lib/jgit/BUCK b/lib/jgit/BUCK
index 8ddf3f9..1f9be46 100644
--- a/lib/jgit/BUCK
+++ b/lib/jgit/BUCK
@@ -1,12 +1,12 @@
 include_defs('//lib/maven.defs')
 
-VERS = '3.5.3.201412180710-r'
+VERS = '3.6.0.201412230720-r'
 
 maven_jar(
   name = 'jgit',
   id = 'org.eclipse.jgit:org.eclipse.jgit:' + VERS,
-  bin_sha1 = '9f3781c7163ee6fa380a4518564a5abb097d9e27',
-  src_sha1 = 'e6d8548522624ffa3094e43130e5dc958f359187',
+  bin_sha1 = 'b005b69d9f5b4dba636a95403d5cb62bad5c486d',
+  src_sha1 = '9f8ced1e1f5c9ba6a3084e35004a19a24776478a',
   license = 'jgit',
   unsign = True,
   deps = [':ewah'],
@@ -20,7 +20,7 @@
 maven_jar(
   name = 'jgit-servlet',
   id = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + VERS,
-  sha1 = 'f2678e1feefd8b90b3c47d40ebc2b9426e3b69f4',
+  sha1 = '92cdf015b62c8a4f8fc1f6fd8b1835931bd4b4d6',
   license = 'jgit',
   deps = [':jgit'],
   unsign = True,
@@ -33,7 +33,7 @@
 maven_jar(
   name = 'jgit-archive',
   id = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + VERS,
-  sha1 = '66705b6630a89c9e6e7950798ea2d7f8a4a82cd7',
+  sha1 = '359c1f666e4bdc2db795b6c60a7635f6be929a66',
   license = 'jgit',
   deps = [':jgit',
     '//lib/commons:compress',
@@ -49,7 +49,7 @@
 maven_jar(
   name = 'junit',
   id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + VERS,
-  sha1 = '47e821761059770dfd3f443dc7f14d5381fb6f4f',
+  sha1 = 'cb029dba3fafb329078904028db171d9c460ada8',
   license = 'DO_NOT_DISTRIBUTE',
   unsign = True,
   deps = [':jgit'],
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 5f4006f..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:'
@@ -40,7 +41,8 @@
     sha1 = '', bin_sha1 = '', src_sha1 = '',
     repository = MAVEN_CENTRAL,
     attach_source = True,
-    visibility = ['PUBLIC']):
+    visibility = ['PUBLIC'],
+    local_license = False):
   from os import path
 
   parts = id.split(':')
@@ -89,7 +91,10 @@
     cmd = ' '.join(cmd),
     out = binjar,
   )
-  license = ['//lib:LICENSE-' + license]
+  license = ':LICENSE-' + license
+  if not local_license:
+    license = '//lib' + license
+  license = [license]
 
   if src_sha1 or attach_source:
     cmd = ['$(exe //tools:download_file)', '-o', '$OUT', '-u', srcurl]
@@ -135,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 6582905..39ffdc2 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit 6582905669d0ccdd009f839936f8209010ae9d6f
+Subproject commit 39ffdc20f021484c1ce1dca2ae9f00fa10a482f4
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 50e3b57..225c088 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 50e3b57294f445a51252c6d858aaf402ea3a24c7
+Subproject commit 225c088073252d5dce48e5cb73cc12e38ed319b8
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 6170241..ba82486 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 61702414c046dd6b811c9137b765f9db422f83db
+Subproject commit ba824869c6b24348647f26e04cf80e1ae82266ec
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index 73c2381..3c59757 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit 73c2381c5768f216b3a4abb1c623f8d0134a9600
+Subproject commit 3c5975763148a6ea2bfa867a96ba5498ae5767f2
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/default.defs b/tools/default.defs
index 27efa11..acd7f8f 100644
--- a/tools/default.defs
+++ b/tools/default.defs
@@ -14,9 +14,39 @@
 
 # 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
 
+# Add AutoValue support to java_library.
+_buck_java_library = java_library
+def java_library(*args, **kwargs):
+  _set_auto_value(kwargs)
+  _buck_java_library(*args, **kwargs)
+
+# Add AutoValue support to java_test.
+_buck_java_test = java_test
+def java_test(*args, **kwargs):
+  _set_auto_value(kwargs)
+  _buck_java_test(*args, **kwargs)
+
+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 +68,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 +108,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'
@@ -139,52 +174,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/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/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();