Merge branch 'stable-2.10'

* stable-2.10:
  Fix login redirect for non default (root) context

Conflicts:
	gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java

Change-Id: I5b6479eeaaca467b68c568e7bd372e61198e2165
diff --git a/.buckconfig b/.buckconfig
index 43bfa5e..0d6d484 100644
--- a/.buckconfig
+++ b/.buckconfig
@@ -6,12 +6,13 @@
   war_deploy = //tools/maven:war_deploy
   war_install = //tools/maven:war_install
   chrome = //:chrome
-  docs = //Documentation:html
+  docs = //Documentation:searchfree
   firefox = //:firefox
   gerrit = //:gerrit
   release = //:release
   safari = //:safari
   withdocs = //:withdocs
+  codeserver = //lib/gwt:codeserver
 
 [buildfile]
   includes = //tools/default.defs
diff --git a/.buckversion b/.buckversion
index a0c6bc2..914f3a9 100644
--- a/.buckversion
+++ b/.buckversion
@@ -1 +1 @@
-0fe4569e871fd6588f7cbfb4b1d4a14baa791a9f
+c38b9899f7952382963cb7822bbdfc9104b42e70
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..b0cf0cb 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,11 +81,12 @@
 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.source=1.7
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 8ff2eb6..e814daf 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -820,6 +820,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
 
@@ -1134,7 +1145,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]]
@@ -1193,13 +1205,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
 
@@ -1208,6 +1213,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..89e10da 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 not in
+	target branch. If the pushed commit is merge commit, this flag is
+	ignored for that push. This option also does not accept merge
+	commits in commit chain to avoid accidental creation of a large
+	number of open changes.
+	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-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..1816759 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -145,6 +145,19 @@
 
 oldTopic:: Topic name before it was changed.
 
+==== Hashtags Changed
+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
+
 == SEE ALSO
 
 * link:json.html[JSON Data Formats]
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 5c6e3e7..42ceb6f 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -820,19 +820,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
@@ -1502,6 +1505,12 @@
 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.
+
 [[gitweb]]
 === Section gitweb
 
@@ -1713,6 +1722,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
@@ -1739,6 +1753,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
@@ -2007,9 +2039,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
 
@@ -2378,6 +2421,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
 
@@ -2526,6 +2654,17 @@
 +
 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.
+
+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
@@ -2997,6 +3136,24 @@
 +
 By default, true.
 
+[[sshd.rekeyBytesLimit]]sshd.rekeyBytesLimit::
++
+Sshd Mina will issue a rekeying after a certain amount of data.
+This configuration option allows you to tweak that setting.
++
+By default, 1073741824 (bytes, 1GB).
++
+The rekeyBytesLimit cannot be set to lower than 32.
+
+[[sshd.rekeyTimeLimit]]sshd.rekeyTimeLimit::
++
+Sshd Mina will issue a rekeying after a certain amount of time.
+This configuration option allows you to tweak that setting.
++
+By default, 1h.
++
+Set to 0 to disable this check.
+
 [[suggest]]
 === Section suggest
 
@@ -3017,6 +3174,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
@@ -3024,6 +3191,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..07f22f3 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-edited --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-validation.txt b/Documentation/config-validation.txt
index 5d23c79..8f76d90 100644
--- a/Documentation/config-validation.txt
+++ b/Documentation/config-validation.txt
@@ -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..3b11382 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.
@@ -30,10 +32,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 +46,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 +199,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:
@@ -288,6 +292,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 +389,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 +463,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 +501,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 +571,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
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 95a5554..6dc8d1c 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -78,6 +78,20 @@
   * 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:
 ====
@@ -179,7 +193,7 @@
     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...) +
@@ -187,6 +201,9 @@
   * 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
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index 384bb74..adc62b2 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -47,24 +47,47 @@
 * 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
+Due to a problem where the codeserver does not correctly identify the connected
+user agent (already fixed upstream but not yet released), the used user agent
+must be explicitly set in `GerritGwtUI.gwt.xml` for SDM to work:
 
-* Modify the name to be unique.
+[source,xml]
+----
+  <set-property name="user.agent" value="gecko1_8" />
+----
 
-* 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.
+or
 
-* Switch to Common tab.
-* Change Save as to be Local file.
-* Close the Debug Configurations dialog and save the changes when prompted.
+[source,xml]
+----
+  <set-property name="user.agent" value="safari" />
+----
+
+* 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
+* 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
+
+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
 
 
 [[known-problems]]
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 507e0e4..d63d403 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 {
@@ -1259,6 +1259,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 +1753,33 @@
 ----
 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.
+
 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-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..b78ee13 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -331,8 +331,18 @@
   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`
+* 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].
+** Documentation html files must be extracted from
+`buck-out/gen/Documentation/searchfree.zip` after generating with:
+
+----
+  buck build docs
+----
+
 * Update Google Code project links
 ** Go to http://code.google.com/p/gerrit/admin
 ** Update the documentation link in the `Resources` section of the
@@ -369,31 +379,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:
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/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/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/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..439fbc5 100644
--- a/Documentation/project-configuration.txt
+++ b/Documentation/project-configuration.txt
@@ -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..e069d85 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
diff --git a/Documentation/replace_macros.py b/Documentation/replace_macros.py
index 7623382..b159250 100755
--- a/Documentation/replace_macros.py
+++ b/Documentation/replace_macros.py
@@ -84,6 +84,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:
@@ -99,7 +103,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-accounts.txt b/Documentation/rest-api-accounts.txt
index 6e9e077..24da5e4 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -1417,48 +1417,51 @@
 preferences of a user.
 
 [options="header",width="50%",cols="1,^1,5"]
-|=====================================
-|Field Name              ||Description
-|`context`               ||
+|===========================================
+|Field Name                    ||Description
+|`context`                     ||
 The number of lines of context when viewing a patch.
-|`expand_all_comments`   |not set if `false`|
+|`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.
-|=====================================
+|===========================================
 
 [[diff-preferences-input]]
 === DiffPreferencesInput
@@ -1467,48 +1470,51 @@
 updated.
 
 [options="header",width="50%",cols="1,^1,5"]
-|=====================================
-|Field Name              ||Description
-|`context`               |optional|
+|===========================================
+|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
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index e5fd1b6..19ad9f6 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -295,9 +295,9 @@
   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.
 --
 
 .Request
@@ -1138,6 +1138,227 @@
   HTTP/1.1 204 No Content
 ----
 
+[[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
+----
+
+[[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...
+----
+
+[[publish-edit]]
+=== Publish Change Edit
+--
+'POST /changes/link:#change-id[\{change-id\}]/publish_edit
+--
+
+Promotes change edit to a regular patch set.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/publish_edit 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\}]/rebase_edit
+--
+
+Rebases change edit on top of latest patch set.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/rebase_edit 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
@@ -1933,9 +2154,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
@@ -3139,12 +3357,15 @@
 === DiffFileMetaInfo
 The `DiffFileMetaInfo` entity contains meta information about a file diff.
 
-[options="header",width="50%",cols="1,6"]
+[options="header",width="50%",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]]
@@ -3433,6 +3654,9 @@
 [[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"]
 |===========================
@@ -3440,24 +3664,33 @@
 |`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.
 |`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.
+|`reviewed`     |optional|
+Indicates whether the caller is authenticated and has commented on the
+current revision. Only set if link:#reviewed[REVIEWED] option is requested.
 |'web_links'   |optional|
 Links to the patch set in external sites as a list of
-link:#web-link-info[WebLinkInfo] entities.
+link:#web-link-info[WebLinkInfo] entities. Only set if
+link:#web-links[WEB_LINKS] option is requested.
 |===========================
 
 [[rule-input]]
@@ -3595,8 +3828,43 @@
 |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",width="50%",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",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`restore_path`|optional|Path to file to restore.
+|===========================
+
 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..46622b1 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"
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 4d9e02c..296184b 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>'.
@@ -1573,6 +1790,9 @@
 |`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]]
@@ -1622,25 +1842,28 @@
 configuration.
 
 [options="header",width="50%",cols="1,^2,4"]
-|=========================================
-|Field Name                  ||Description
-|`description`               |optional|
+|=======================================================
+|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,14 +1885,14 @@
 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.
@@ -1680,50 +1903,55 @@
 The `ConfigInput` entity describes a new project configuration.
 
 [options="header",width="50%",cols="1,^2,4"]
-|=========================================
-|Field Name                  ||Description
-|`description`               |optional|
+|======================================================
+|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.
 |=========================================
@@ -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|
@@ -2037,6 +2268,24 @@
 |`size_of_packed_objects`  |Size of packed objects in bytes.
 |======================================
 
+[[tag-info]]
+=== TagInfo
+The `TagInfo` entity contains information about a tag.
+
+[options="header",width="50%",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.
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-review-ui.txt b/Documentation/user-review-ui.txt
index bb1aeef..3d5e418 100644
--- a/Documentation/user-review-ui.txt
+++ b/Documentation/user-review-ui.txt
@@ -1046,6 +1046,11 @@
 +
 Controls whether the top menu is shown.
 
+- `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`:
 +
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.11.txt b/ReleaseNotes/ReleaseNotes-2.11.txt
new file mode 100644
index 0000000..c12d913
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.11.txt
@@ -0,0 +1,72 @@
+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
+----
+
+Release Highlights
+------------------
+
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=505[Issue 505]:
+Code changes can be done directly in browser.
++
+Files can be added, deleted, restored or amended directly in browser
+in context of change edit. 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 has been removed to close a
+security vulnerability.  Now only administrators are allowed to generate
+and delete other user's http passwords via the REST or SSH interface.
+We would encourage you to clean up your project.config setting after upgrading.
+
+Upgrades
+--------
+
+TODO
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..b9d7c5d 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/gerrit-acceptance-tests/BUCK b/gerrit-acceptance-tests/BUCK
index 3c26720..fdf41dc 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,12 @@
     '//lib:jsch',
     '//lib:junit',
     '//lib:servlet-api-3_1',
+    '//lib:truth',
 
-    '//lib/commons:httpclient',
-    '//lib/commons:httpcore',
+    '//lib/hamcrest:hamcrest-core',
+    '//lib/hamcrest:hamcrest-library',
+    '//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..3af2914 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
@@ -17,22 +17,36 @@
 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.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.Util.block;
 import static org.junit.Assert.assertEquals;
 
 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.account.GroupCache;
 import com.google.gerrit.server.change.ChangeJson;
+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 +55,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 +76,9 @@
   public Config baseConfig;
 
   @Inject
+  protected AllProjectsName allProjects;
+
+  @Inject
   protected AccountCreator accounts;
 
   @Inject
@@ -77,6 +96,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;
@@ -218,4 +249,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..c0ba921 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
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.testutil.TempFileUtil;
 
 import com.jcraft.jsch.JSch;
 import com.jcraft.jsch.JSchException;
@@ -100,6 +101,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,7 +141,7 @@
   }
 
   public static void rm(Git gApi, String path)
-      throws GitAPIException, IOException {
+      throws GitAPIException {
     gApi.rm()
         .addFilepattern(path)
         .call();
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..3c0fc93 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,
@@ -151,28 +170,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 +225,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 +244,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 +263,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/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index c95f8c3..77c685c 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
@@ -21,17 +21,13 @@
 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")
@@ -42,7 +38,7 @@
   }
 
   @Test
-  public void self() throws RestApiException {
+  public void self() throws Exception {
     AccountInfo info = gApi
         .accounts()
         .self()
@@ -53,8 +49,7 @@
   }
 
   @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()
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..c4ebc29 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,68 @@
 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);
   }
 
   @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 +116,43 @@
   }
 
   @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(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(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(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 +161,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 +171,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.size()).is(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.size()).is(1);
     results = query("status:closed");
-    assertTrue(results.isEmpty());
+    assertThat(results.isEmpty()).isTrue();
   }
 
   @Test
@@ -197,9 +190,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.size()).is(2);
+    assertThat(results.get(0).changeId).isEqualTo(r2.getChangeId());
+    assertThat(results.get(1).changeId).isEqualTo(r1.getChangeId());
   }
 
   @Test
@@ -207,7 +200,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 +209,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.size()).is(1);
+    assertThat(Iterables.getOnlyElement(results).changeId)
+        .isEqualTo(r2.getChangeId());
   }
 
   @Test
@@ -224,17 +219,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(result.messages).isNull();
+    assertThat(result.revisions).isNull();
+    assertThat(result.actions).isNull();
   }
 
   @Test
@@ -244,23 +240,23 @@
         .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(result.messages.size()).is(1);
+    assertThat(result.actions.isEmpty()).isFalse();
 
     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.actions.isEmpty()).isFalse();
   }
 
   @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()).isTrue();
   }
 
   @Test
@@ -273,9 +269,29 @@
         .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("");
   }
 }
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..e682a19 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.size()).is(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.size()).is(0);
 
     List<ProjectInfo> matchingProject = gApi.projects().list()
         .withPrefix("fo")
         .get();
-    assertEquals(1, matchingProject.size());
+    assertThat(matchingProject.size()).is(1);
 
     List<ProjectInfo> limitOneProject = gApi.projects().list()
         .withLimit(1)
         .get();
-    assertEquals(1, limitOneProject.size());
+    assertThat(limitOneProject.size()).is(1);
 
     List<ProjectInfo> startAtOneProjects = gApi.projects().list()
         .withStart(1)
         .get();
-    assertEquals(allProjects.size() - 1, startAtOneProjects.size());
+    assertThat(startAtOneProjects.size()).is(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..dd852e2 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,8 @@
 
 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.junit.Assert.fail;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -26,7 +25,6 @@
 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.restapi.AuthException;
@@ -49,8 +47,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 +56,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 +65,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 +80,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 +93,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 +109,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 +118,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(orig.get().messages.size()).is(1);
+    ChangeApi cherry = orig.revision(r.getCommit().name())
+        .cherryPick(in);
+    assertThat(orig.get().messages.size()).is(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 +154,74 @@
     ChangeApi orig = gApi.changes()
         .id("p~master~" + r.getChangeId());
 
-    assertEquals(1, orig.get().messages.size());
+    assertThat(orig.get().messages.size()).is(1);
     ChangeApi cherry = orig.revision(r.getCommit().name())
         .cherryPick(in);
-    assertEquals(2, orig.get().messages.size());
+    assertThat(orig.get().messages.size()).is(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(orig.get().messages.size()).is(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 +234,23 @@
         .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());
-  }
-
-  protected RevisionApi revision(PushOneCommit.Result r) throws Exception {
-    return gApi.changes()
+    boolean isEmpty = gApi.changes()
         .id(r.getChangeId())
-        .current();
+        .current()
+        .reviewed()
+        .isEmpty() ;
+    assertThat(isEmpty).isTrue();
   }
 
   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..efdc919
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -0,0 +1,671 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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 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.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Optional;
+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.common.EditInfo;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.ChangeEdits.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.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);
+    assertNotNull(ps);
+    String changeId2 = newChange2(git, admin.getIdent());
+    change2 = getChange(changeId2);
+    assertNotNull(change2);
+    ps2 = getCurrentPatchSet(changeId2);
+    assertNotNull(ps2);
+    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 {
+    assertEquals(RefUpdate.Result.NEW,
+        modifier.createEdit(
+            change,
+            ps));
+    assertEquals(RefUpdate.Result.FORCED,
+        modifier.modifyFile(
+            editUtil.byChange(change).get(),
+            FILE_NAME,
+            CONTENT_NEW));
+    editUtil.delete(editUtil.byChange(change).get());
+    assertFalse(editUtil.byChange(change).isPresent());
+  }
+
+  @Test
+  public void publishEdit() throws Exception {
+    assertEquals(RefUpdate.Result.NEW,
+        modifier.createEdit(
+            change,
+            getCurrentPatchSet(changeId)));
+    assertEquals(RefUpdate.Result.FORCED,
+        modifier.modifyFile(
+            editUtil.byChange(change).get(),
+            FILE_NAME,
+            CONTENT_NEW2));
+    editUtil.publish(editUtil.byChange(change).get());
+    assertFalse(editUtil.byChange(change).isPresent());
+  }
+
+  @Test
+  public void publishEditRest() throws Exception {
+    PatchSet oldCurrentPatchSet = getCurrentPatchSet(changeId);
+    assertEquals(RefUpdate.Result.NEW,
+        modifier.createEdit(
+            change,
+            oldCurrentPatchSet));
+    assertEquals(RefUpdate.Result.FORCED,
+        modifier.modifyFile(
+            editUtil.byChange(change).get(),
+            FILE_NAME,
+            CONTENT_NEW));
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    RestResponse r = adminSession.post(urlPublish());
+    assertEquals(SC_NO_CONTENT, r.getStatusCode());
+    edit = editUtil.byChange(change);
+    assertFalse(edit.isPresent());
+    PatchSet newCurrentPatchSet = getCurrentPatchSet(changeId);
+    assertFalse(oldCurrentPatchSet.getId().equals(newCurrentPatchSet.getId()));
+  }
+
+  @Test
+  public void deleteEditRest() throws Exception {
+    assertEquals(RefUpdate.Result.NEW,
+        modifier.createEdit(
+            change,
+            ps));
+    assertEquals(RefUpdate.Result.FORCED,
+        modifier.modifyFile(
+            editUtil.byChange(change).get(),
+            FILE_NAME,
+            CONTENT_NEW));
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    RestResponse r = adminSession.delete(urlEdit());
+    assertEquals(SC_NO_CONTENT, r.getStatusCode());
+    edit = editUtil.byChange(change);
+    assertFalse(edit.isPresent());
+  }
+
+  @Test
+  public void rebaseEdit() throws Exception {
+    assertEquals(RefUpdate.Result.NEW,
+        modifier.createEdit(
+            change,
+            ps));
+    assertEquals(RefUpdate.Result.FORCED,
+        modifier.modifyFile(
+            editUtil.byChange(change).get(),
+            FILE_NAME,
+            CONTENT_NEW));
+    ChangeEdit edit = editUtil.byChange(change).get();
+    PatchSet current = getCurrentPatchSet(changeId);
+    assertEquals(current.getPatchSetId() - 1,
+        edit.getBasePatchSet().getPatchSetId());
+    Date beforeRebase = edit.getEditCommit().getCommitterIdent().getWhen();
+    modifier.rebaseEdit(edit, current);
+    edit = editUtil.byChange(change).get();
+    assertArrayEquals(CONTENT_NEW,
+        toBytes(fileUtil.getContent(edit.getChange().getProject(),
+            edit.getRevision().get(), FILE_NAME)));
+    assertArrayEquals(CONTENT_NEW2,
+        toBytes(fileUtil.getContent(edit.getChange().getProject(),
+            edit.getRevision().get(), FILE_NAME2)));
+    assertEquals(current.getPatchSetId(),
+        edit.getBasePatchSet().getPatchSetId());
+    Date afterRebase = edit.getEditCommit().getCommitterIdent().getWhen();
+    assertFalse(beforeRebase.equals(afterRebase));
+  }
+
+  @Test
+  public void rebaseEditRest() throws Exception {
+    assertEquals(RefUpdate.Result.NEW,
+        modifier.createEdit(
+            change,
+            ps));
+    assertEquals(RefUpdate.Result.FORCED,
+        modifier.modifyFile(
+            editUtil.byChange(change).get(),
+            FILE_NAME,
+            CONTENT_NEW));
+    ChangeEdit edit = editUtil.byChange(change).get();
+    PatchSet current = getCurrentPatchSet(changeId);
+    assertEquals(current.getPatchSetId() - 1,
+        edit.getBasePatchSet().getPatchSetId());
+    Date beforeRebase = edit.getEditCommit().getCommitterIdent().getWhen();
+    RestResponse r = adminSession.post(urlRebase());
+    assertEquals(SC_NO_CONTENT, r.getStatusCode());
+    edit = editUtil.byChange(change).get();
+    assertArrayEquals(CONTENT_NEW,
+        toBytes(fileUtil.getContent(edit.getChange().getProject(),
+            edit.getRevision().get(), FILE_NAME)));
+    assertArrayEquals(CONTENT_NEW2,
+        toBytes(fileUtil.getContent(edit.getChange().getProject(),
+            edit.getRevision().get(), FILE_NAME2)));
+    assertEquals(current.getPatchSetId(),
+        edit.getBasePatchSet().getPatchSetId());
+    Date afterRebase = edit.getEditCommit().getCommitterIdent().getWhen();
+    assertFalse(beforeRebase.equals(afterRebase));
+  }
+
+  @Test
+  public void updateExistingFile() throws Exception {
+    assertEquals(RefUpdate.Result.NEW,
+        modifier.createEdit(
+            change,
+            ps));
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertEquals(RefUpdate.Result.FORCED,
+        modifier.modifyFile(
+            edit.get(),
+            FILE_NAME,
+            CONTENT_NEW));
+    edit = editUtil.byChange(change);
+    assertArrayEquals(CONTENT_NEW,
+        toBytes(fileUtil.getContent(edit.get().getChange().getProject(),
+            edit.get().getRevision().get(), FILE_NAME)));
+    editUtil.delete(edit.get());
+    edit = editUtil.byChange(change);
+    assertFalse(edit.isPresent());
+  }
+
+  @Test
+  public void retrieveEdit() throws Exception {
+    RestResponse r = adminSession.get(urlEdit());
+    assertEquals(SC_NO_CONTENT, r.getStatusCode());
+    assertEquals(RefUpdate.Result.NEW,
+        modifier.createEdit(
+            change,
+            ps));
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertEquals(RefUpdate.Result.FORCED,
+        modifier.modifyFile(
+            edit.get(),
+            FILE_NAME,
+            CONTENT_NEW));
+    edit = editUtil.byChange(change);
+    EditInfo info = toEditInfo(false);
+    assertEquals(edit.get().getRevision().get(), info.commit.commit);
+    assertEquals(1, info.commit.parents.size());
+
+    edit = editUtil.byChange(change);
+    editUtil.delete(edit.get());
+
+    r = adminSession.get(urlEdit());
+    assertEquals(SC_NO_CONTENT, r.getStatusCode());
+  }
+
+  @Test
+  public void retrieveFilesInEdit() throws Exception {
+    assertEquals(RefUpdate.Result.NEW,
+        modifier.createEdit(
+            change,
+            ps));
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertEquals(RefUpdate.Result.FORCED,
+        modifier.modifyFile(
+            edit.get(),
+            FILE_NAME,
+            CONTENT_NEW));
+
+    EditInfo info = toEditInfo(true);
+    assertEquals(2, info.files.size());
+    List<String> l = Lists.newArrayList(info.files.keySet());
+    assertEquals("/COMMIT_MSG", l.get(0));
+    assertEquals("foo", l.get(1));
+  }
+
+  @Test
+  public void deleteExistingFile() throws Exception {
+    assertEquals(RefUpdate.Result.NEW,
+        modifier.createEdit(
+            change,
+            ps));
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertEquals(RefUpdate.Result.FORCED,
+        modifier.deleteFile(
+            edit.get(),
+            FILE_NAME));
+    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());
+    assertEquals(SC_NO_CONTENT, r.getStatusCode());
+    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());
+    assertEquals(SC_NOT_FOUND, r.getStatusCode());
+  }
+
+  @Test
+  public void deleteExistingFileRest() throws Exception {
+    assertEquals(RefUpdate.Result.NEW,
+        modifier.createEdit(
+            change,
+            ps));
+    assertEquals(SC_NO_CONTENT, adminSession.delete(urlEditFile())
+        .getStatusCode());
+    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 {
+    assertEquals(RefUpdate.Result.NEW,
+        modifier.createEdit(
+            change2,
+            ps2));
+    Optional<ChangeEdit> edit = editUtil.byChange(change2);
+    assertEquals(RefUpdate.Result.FORCED,
+        modifier.restoreFile(
+            edit.get(),
+            FILE_NAME));
+    edit = editUtil.byChange(change2);
+    assertArrayEquals(CONTENT_OLD,
+        toBytes(fileUtil.getContent(edit.get().getChange().getProject(),
+            edit.get().getRevision().get(), FILE_NAME)));
+  }
+
+  @Test
+  public void restoreDeletedFileInPatchSetRest() throws Exception {
+    Post.Input in = new Post.Input();
+    in.restorePath = FILE_NAME;
+    assertEquals(SC_NO_CONTENT, adminSession.post(urlEdit2(),
+        in).getStatusCode());
+    Optional<ChangeEdit> edit = editUtil.byChange(change2);
+    assertArrayEquals(CONTENT_OLD,
+        toBytes(fileUtil.getContent(edit.get().getChange().getProject(),
+            edit.get().getRevision().get(), FILE_NAME)));
+  }
+
+  @Test
+  public void amendExistingFile() throws Exception {
+    assertEquals(RefUpdate.Result.NEW,
+        modifier.createEdit(
+            change,
+            ps));
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertEquals(RefUpdate.Result.FORCED,
+        modifier.modifyFile(
+            edit.get(),
+            FILE_NAME,
+            CONTENT_NEW));
+    edit = editUtil.byChange(change);
+    assertArrayEquals(CONTENT_NEW,
+        toBytes(fileUtil.getContent(edit.get().getChange().getProject(),
+            edit.get().getRevision().get(), FILE_NAME)));
+    assertEquals(RefUpdate.Result.FORCED,
+        modifier.modifyFile(
+            edit.get(),
+            FILE_NAME,
+            CONTENT_NEW2));
+    edit = editUtil.byChange(change);
+    assertArrayEquals(CONTENT_NEW2,
+        toBytes(fileUtil.getContent(edit.get().getChange().getProject(),
+            edit.get().getRevision().get(), FILE_NAME)));
+  }
+
+  @Test
+  public void createAndChangeEditInOneRequestRest() throws Exception {
+    Put.Input in = new Put.Input();
+    in.content = RestSession.newRawInput(CONTENT_NEW);
+    assertEquals(SC_NO_CONTENT, adminSession.putRaw(urlEditFile(),
+        in.content).getStatusCode());
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertArrayEquals(CONTENT_NEW,
+        toBytes(fileUtil.getContent(edit.get().getChange().getProject(),
+            edit.get().getRevision().get(), FILE_NAME)));
+    in.content = RestSession.newRawInput(CONTENT_NEW2);
+    assertEquals(SC_NO_CONTENT, adminSession.putRaw(urlEditFile(),
+        in.content).getStatusCode());
+    edit = editUtil.byChange(change);
+    assertArrayEquals(CONTENT_NEW2,
+        toBytes(fileUtil.getContent(edit.get().getChange().getProject(),
+            edit.get().getRevision().get(), FILE_NAME)));
+  }
+
+  @Test
+  public void changeEditRest() throws Exception {
+    assertEquals(RefUpdate.Result.NEW,
+        modifier.createEdit(
+            change,
+            ps));
+    Put.Input in = new Put.Input();
+    in.content = RestSession.newRawInput(CONTENT_NEW);
+    assertEquals(SC_NO_CONTENT, adminSession.putRaw(urlEditFile(),
+        in.content).getStatusCode());
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertArrayEquals(CONTENT_NEW,
+        toBytes(fileUtil.getContent(edit.get().getChange().getProject(),
+            edit.get().getRevision().get(), FILE_NAME)));
+  }
+
+  @Test
+  public void emptyPutRequest() throws Exception {
+    assertEquals(RefUpdate.Result.NEW,
+        modifier.createEdit(
+            change,
+            ps));
+    assertEquals(SC_NO_CONTENT, adminSession.put(urlEditFile())
+        .getStatusCode());
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertArrayEquals("".getBytes(),
+        toBytes(fileUtil.getContent(edit.get().getChange().getProject(),
+            edit.get().getRevision().get(), FILE_NAME)));
+  }
+
+  @Test
+  public void createEmptyEditRest() throws Exception {
+    assertEquals(SC_NO_CONTENT, adminSession.post(urlEdit()).getStatusCode());
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertArrayEquals(CONTENT_OLD,
+        toBytes(fileUtil.getContent(edit.get().getChange().getProject(),
+            edit.get().getRevision().get(), FILE_NAME)));
+  }
+
+  @Test
+  public void getFileContentRest() throws Exception {
+    Put.Input in = new Put.Input();
+    in.content = RestSession.newRawInput(CONTENT_NEW);
+    assertEquals(SC_NO_CONTENT, adminSession.putRaw(urlEditFile(),
+        in.content).getStatusCode());
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertEquals(RefUpdate.Result.FORCED,
+        modifier.modifyFile(
+            edit.get(),
+            FILE_NAME,
+            CONTENT_NEW2));
+    edit = editUtil.byChange(change);
+    RestResponse r = adminSession.get(urlEditFile());
+    assertEquals(SC_OK, r.getStatusCode());
+    String content = r.getEntityContent();
+    assertEquals(StringUtils.newStringUtf8(CONTENT_NEW2),
+        StringUtils.newStringUtf8(Base64.decodeBase64(content)));
+  }
+
+  @Test
+  public void getFileNotFoundRest() throws Exception {
+    assertEquals(RefUpdate.Result.NEW,
+        modifier.createEdit(
+            change,
+            ps));
+    assertEquals(SC_NO_CONTENT, adminSession.delete(urlEditFile())
+        .getStatusCode());
+    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());
+    assertEquals(SC_NO_CONTENT, r.getStatusCode());
+  }
+
+  @Test
+  public void addNewFile() throws Exception {
+    assertEquals(RefUpdate.Result.NEW,
+        modifier.createEdit(
+            change,
+            ps));
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertEquals(RefUpdate.Result.FORCED,
+        modifier.modifyFile(
+            edit.get(),
+            FILE_NAME2,
+            CONTENT_NEW));
+    edit = editUtil.byChange(change);
+    assertArrayEquals(CONTENT_NEW,
+        toBytes(fileUtil.getContent(edit.get().getChange().getProject(),
+            edit.get().getRevision().get(), FILE_NAME2)));
+  }
+
+  @Test
+  public void addNewFileAndAmend() throws Exception {
+    assertEquals(RefUpdate.Result.NEW,
+        modifier.createEdit(
+            change,
+            ps));
+    Optional<ChangeEdit> edit = editUtil.byChange(change);
+    assertEquals(RefUpdate.Result.FORCED,
+        modifier.modifyFile(
+            edit.get(),
+            FILE_NAME2,
+            CONTENT_NEW));
+    edit = editUtil.byChange(change);
+    assertArrayEquals(CONTENT_NEW,
+        toBytes(fileUtil.getContent(edit.get().getChange().getProject(),
+            edit.get().getRevision().get(), FILE_NAME2)));
+    assertEquals(RefUpdate.Result.FORCED,
+        modifier.modifyFile(
+            edit.get(),
+            FILE_NAME2,
+            CONTENT_NEW2));
+    edit = editUtil.byChange(change);
+    assertArrayEquals(CONTENT_NEW2,
+        toBytes(fileUtil.getContent(edit.get().getChange().getProject(),
+            edit.get().getRevision().get(), FILE_NAME2)));
+  }
+
+  @Test
+  public void writeNoChanges() throws Exception {
+    assertEquals(RefUpdate.Result.NEW,
+        modifier.createEdit(
+            change,
+            ps));
+    try {
+      modifier.modifyFile(
+          editUtil.byChange(change).get(),
+          FILE_NAME,
+          CONTENT_OLD);
+      fail();
+    } catch (InvalidChangeOperationException e) {
+      assertEquals("no changes were made", e.getMessage());
+    }
+  }
+
+  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 byte[] toBytes(BinaryResult content) throws Exception {
+    ByteArrayOutputStream os = new ByteArrayOutputStream();
+    content.writeTo(os);
+    return os.toByteArray();
+  }
+
+  private String urlEdit() {
+    return "/changes/"
+        + change.getChangeId()
+        + "/edit";
+  }
+
+  private String urlEdit2() {
+    return "/changes/"
+        + change2.getChangeId()
+        + "/edit/";
+  }
+
+  private String urlEditFile() {
+    return urlEdit()
+        + "/"
+        + FILE_NAME;
+  }
+
+  private String urlGetFiles() {
+    return urlEdit()
+        + "?list";
+  }
+
+  private String urlPublish() {
+    return "/changes/"
+        + change.getChangeId()
+        + "/publish_edit";
+  }
+
+  private String urlRebase() {
+    return "/changes/"
+        + change.getChangeId()
+        + "/rebase_edit";
+  }
+
+  private EditInfo toEditInfo(boolean files) throws IOException {
+    RestResponse r = adminSession.get(files ? urlGetFiles() : urlEdit());
+    assertEquals(SC_OK, r.getStatusCode());
+    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..238b2e3 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -15,8 +15,11 @@
 package com.google.gerrit.acceptance.git;
 
 import static com.google.gerrit.acceptance.GitUtil.cloneProject;
+import static org.hamcrest.Matchers.is;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeThat;
 
+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 +27,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
   }
@@ -179,22 +195,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
+    assumeThat(notesMigration.enabled(), is(true));
+
+    // 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();
+    assertEquals(expected, hashtags);
+
+    // 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();
+    assertEquals(expected, hashtags);
+  }
+
+  @Test
+  public void testPushForMasterWithMultipleHashtags() throws GitAPIException,
+      OrmException, IOException, RestApiException {
+
+    // Hashtags currently only work when noteDB is enabled
+    assumeThat(notesMigration.enabled(), is(true));
+
+    // 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();
+    assertEquals(expected, hashtags);
+
+    // 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();
+    assertEquals(expected, hashtags);
+  }
+
+  @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
+    assumeThat(notesMigration.enabled(), is(false));
+    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..00e91bc 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,8 +14,6 @@
 
 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;
@@ -24,98 +22,45 @@
 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 +70,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 +86,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 +118,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 +133,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 +148,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");
 
@@ -207,15 +165,13 @@
   }
 
   @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 +181,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");
@@ -255,19 +209,6 @@
     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());
-  }
-
   private PatchSetApproval getSubmitter(PatchSet.Id patchSetId)
       throws OrmException {
     Change c = db.changes().get(patchSetId.getParentKey());
@@ -317,24 +258,37 @@
     }
   }
 
-  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();
+      Ref tagRef = repo.getRef(tag.name);
+      assertTrue(tagRef != null);
+      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());
+          assertTrue(object instanceof RevTag);
+          RevTag tagObject = (RevTag)object;
+          assertEquals(annotatedTag.message, tagObject.getFullMessage());
+          assertEquals(annotatedTag.tagger, tagObject.getTaggerIdent());
+          taggedCommit = tagObject.getObject();
+        } finally {
+          rw.dispose();
+        }
+      } else {
+        taggedCommit = tagRef.getObjectId();
+      }
+      ObjectId headCommit = repo.getRef(branch).getObjectId();
+      assertTrue(taggedCommit != null);
       assertEquals(headCommit, taggedCommit);
     } 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..40a2d16
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
@@ -0,0 +1,263 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+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());
+      assertEquals(RefUpdate.Result.NEW, mtu.update());
+
+      // branch-tag -> branch
+      RefUpdate btu = repo.updateRef("refs/tags/branch-tag");
+      btu.setExpectedOldObjectId(ObjectId.zeroId());
+      btu.setNewObjectId(repo.getRef("refs/heads/branch").getObjectId());
+      assertEquals(RefUpdate.Result.NEW, btu.update());
+    } 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()));
+    assertFalse(sshSession.getError(), sshSession.hasError());
+
+    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();
+    assertEquals(filtered, Ordering.natural().sortedCopy(s.split(out)));
+  }
+}
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..faace57 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
@@ -16,8 +16,8 @@
 
 import static org.junit.Assert.assertEquals;
 
-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,18 +43,10 @@
   @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");
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..f469a51 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
@@ -25,14 +25,11 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.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.eclipse.jgit.errors.ConfigInvalidException;
 import org.junit.Test;
@@ -41,20 +38,8 @@
 
 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");
@@ -78,10 +63,7 @@
   }
 
   @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();
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..7a2f569 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
@@ -24,6 +24,7 @@
   public boolean flushCaches;
   public boolean generateHttpPassword;
   public boolean killTask;
+  public boolean modifyAccount;
   public boolean priority;
   public QueryLimit queryLimit;
   public boolean runAs;
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..627fd58 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
@@ -30,13 +30,13 @@
 
 public class GetAccountIT extends AbstractDaemonTest {
   @Test
-  public void getNonExistingAccount_NotFound() throws IOException {
+  public void getNonExistingAccount_NotFound() throws Exception {
     assertEquals(HttpStatus.SC_NOT_FOUND,
         adminSession.get("/accounts/non-existing").getStatusCode());
   }
 
   @Test
-  public void getAccount() throws IOException {
+  public void getAccount() throws Exception {
     // by formatted string
     testGetAccount("/accounts/"
         + Url.encode(admin.fullName + " <" + admin.email + ">"), admin);
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..bf286cf 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
@@ -20,23 +20,20 @@
 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 {
+      throws Exception {
     assertEquals(HttpStatus.SC_NOT_FOUND,
         adminSession.get("/accounts/non-existing/preferences.diff").getStatusCode());
   }
 
   @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());
     DiffPreferencesInfo diffPreferences =
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..c558df0 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
@@ -22,9 +22,7 @@
 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.junit.Test;
 
 import java.io.IOException;
@@ -32,8 +30,7 @@
 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);
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..6d1ab25 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
@@ -19,7 +19,6 @@
 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;
@@ -40,7 +39,6 @@
 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 +66,6 @@
 import java.util.List;
 
 public abstract class AbstractSubmit extends AbstractDaemonTest {
-
-  @Inject
-  private GitRepositoryManager repoManager;
-
   @Inject
   private ChangeNotes.Factory notesFactory;
 
@@ -213,7 +207,7 @@
   }
 
   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(
@@ -235,7 +229,9 @@
     Repository repo = localGit.getRepository();
     RevCommit localHead = getHead(repo);
     RevCommit remoteHead = getRemoteHead();
-    assertNotEquals(localHead.getId(), remoteHead.getId());
+    assertFalse(
+        String.format("%s not equal %s", localHead.name(), remoteHead.name()),
+        localHead.getId().equals(remoteHead.getId()));
     assertEquals(1, remoteHead.getParentCount());
     if (!contentMerge) {
       assertEquals(getLatestDiff(repo), getLatestRemoteDiff());
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..22069cc 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
@@ -24,10 +24,9 @@
 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 +36,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 +46,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
@@ -84,8 +79,7 @@
   }
 
   @Test
-  public void defaultMessage() throws GitAPIException, IOException,
-      RestApiException {
+  public void defaultMessage() throws Exception {
     String changeId = createChange().getChangeId();
     ChangeInfo c = get(changeId);
     assertNotNull(c.messages);
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..2cc293c 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
@@ -15,7 +15,6 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static com.google.gerrit.acceptance.GitUtil.initSsh;
 import static com.google.gerrit.common.data.Permission.LABEL;
 import static org.junit.Assert.assertEquals;
@@ -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);
   }
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..3df39c7 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
@@ -27,8 +27,6 @@
 import com.google.gerrit.server.change.ChangeJson.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,8 +40,7 @@
   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);
 
@@ -52,8 +49,7 @@
   }
 
   @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);
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..703f65e 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
@@ -23,12 +23,10 @@
 import com.google.gerrit.acceptance.RestSession;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeStatus;
-import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwtorm.server.OrmException;
 
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -36,8 +34,7 @@
 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);
@@ -49,8 +46,7 @@
   }
 
   @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);
@@ -61,8 +57,7 @@
   }
 
   @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);
@@ -75,8 +70,7 @@
   }
 
   @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);
@@ -87,7 +81,7 @@
     assertEquals(ChangeStatus.NEW, get(triplet).status);
   }
 
-  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/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/SubmitByCherryPickIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
index 12d002e..d951ed3 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
@@ -19,17 +19,12 @@
 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,8 +35,7 @@
   }
 
   @Test
-  public void submitWithCherryPickIfFastForwardPossible() throws JSchException,
-      IOException, GitAPIException {
+  public void submitWithCherryPickIfFastForwardPossible() throws Exception {
     Git git = createProject();
     PushOneCommit.Result change = createChange(git);
     submit(change.getChangeId());
@@ -159,8 +153,7 @@
   }
 
   @Test
-  public void submitMultipleChanges()
-      throws JSchException, IOException, GitAPIException, OrmException {
+  public void submitMultipleChanges() throws Exception {
     Git git = createProject();
     RevCommit initialHead = getRemoteHead();
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
index 9512c7a..8999013 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
@@ -19,16 +19,11 @@
 
 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 {
@@ -52,8 +47,7 @@
   }
 
   @Test
-  public void submitMultipleChanges()
-      throws JSchException, IOException, GitAPIException, OrmException {
+  public void submitMultipleChanges() throws Exception {
     Git git = createProject();
     RevCommit initialHead = getRemoteHead();
 
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..d5452d0 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
@@ -5,16 +5,11 @@
 
 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 {
@@ -37,8 +32,7 @@
   }
 
   @Test
-  public void submitMultipleChanges()
-      throws JSchException, IOException, GitAPIException, OrmException {
+  public void submitMultipleChanges() throws Exception {
     Git git = createProject();
     RevCommit initialHead = getRemoteHead();
 
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..2a5c84e 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
@@ -31,14 +31,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 +47,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,8 +66,7 @@
 
   @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);
@@ -91,8 +78,7 @@
        @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);
@@ -100,16 +86,14 @@
 
   @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);
   }
 
   @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);
@@ -161,6 +145,51 @@
     assertEquals("User2", Iterables.getOnlyElement(reviewers).account.name);
   }
 
+  @Test
+  @GerritConfig(name = "suggest.maxSuggestedReviewers", value = "2")
+  public void suggestReviewersMaxNbrSuggestions() throws Exception {
+    String changeId = createChange().getChangeId();
+    List<SuggestedReviewerInfo> reviewers =
+        suggestReviewers(changeId, "user", 5);
+    assertEquals(2, reviewers.size());
+  }
+
+  @Test
+  @GerritConfig(name = "suggest.fullTextSearch", value = "true")
+  public void suggestReviewersFullTextSearch() throws Exception {
+    String changeId = createChange().getChangeId();
+    List<SuggestedReviewerInfo> reviewers =
+        suggestReviewers(changeId, "ser", 5);
+    assertEquals(4, reviewers.size());
+  }
+
+  @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);
+    assertEquals(2, reviewers.size());
+  }
+
+  @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());
+    assertEquals(1, suggestedReviewerInfos.size());
+  }
+
   private List<SuggestedReviewerInfo> suggestReviewers(RestSession session,
       String changeId, String query, int n) throws IOException {
     return newGson().fromJson(
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..d58173e 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
@@ -17,7 +17,6 @@
 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;
@@ -27,13 +26,11 @@
 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,17 +40,8 @@
 
 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);
@@ -68,21 +56,21 @@
   }
 
   @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());
   }
 
   @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());
   }
 
   @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);
@@ -106,21 +94,21 @@
   }
 
   @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());
   }
 
   @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());
   }
 
   @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);
@@ -136,12 +124,12 @@
   }
 
   @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/",
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..666a71a 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
@@ -15,7 +15,6 @@
 package com.google.gerrit.acceptance.rest.config;
 
 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;
@@ -24,13 +23,11 @@
 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,17 +36,8 @@
 
 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);
@@ -64,36 +52,36 @@
   }
 
   @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());
   }
 
   @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());
   }
 
   @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());
   }
 
   @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());
   }
 
   @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");
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..f693a17 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
@@ -27,12 +27,10 @@
 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());
     CacheInfo result = newGson().fromJson(r.getReader(), CacheInfo.class);
@@ -56,19 +54,19 @@
   }
 
   @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());
   }
 
   @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());
   }
 
   @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());
   }
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..4253c30 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
@@ -25,13 +25,12 @@
 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());
@@ -45,13 +44,13 @@
   }
 
   @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());
   }
 
-  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..ec587da 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
@@ -25,13 +25,12 @@
 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());
@@ -51,7 +50,7 @@
   }
 
   @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());
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..39a303e 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
@@ -31,7 +31,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,7 +38,7 @@
 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());
     Map<String, CacheInfo> result =
@@ -67,13 +66,13 @@
   }
 
   @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());
   }
 
   @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());
     List<String> result =
@@ -85,7 +84,7 @@
   }
 
   @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());
     String result = new String(Base64.decode(r.getEntityContent()), UTF_8.name());
@@ -96,7 +95,7 @@
   }
 
   @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());
   }
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..9f9383f 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
@@ -26,13 +26,12 @@
 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());
     List<TaskInfo> result =
@@ -53,7 +52,7 @@
   }
 
   @Test
-  public void listTasksWithoutViewQueueCapability() throws IOException {
+  public void listTasksWithoutViewQueueCapability() throws Exception {
     RestResponse r = userSession.get("/config/server/tasks/");
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
     List<TaskInfo> result =
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..7c8a3a3 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
@@ -31,9 +31,7 @@
 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 +39,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,33 +50,14 @@
 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 IOException {
+  public void addToNonExistingGroup_NotFound() throws Exception {
     assertEquals(HttpStatus.SC_NOT_FOUND,
         PUT("/groups/non-existing/members/admin").getStatusCode());
   }
 
   @Test
-  public void removeFromNonExistingGroup_NotFound() throws IOException {
+  public void removeFromNonExistingGroup_NotFound() throws Exception {
     assertEquals(HttpStatus.SC_NOT_FOUND,
         DELETE("/groups/non-existing/members/admin"));
   }
@@ -102,7 +77,7 @@
   }
 
   @Test
-  public void addExistingMember_OK() throws IOException {
+  public void addExistingMember_OK() throws Exception {
     assertEquals(HttpStatus.SC_OK,
         PUT("/groups/Administrators/members/admin").getStatusCode());
   }
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..5811126 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
@@ -22,26 +22,15 @@
 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);
@@ -52,7 +41,7 @@
   }
 
   @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";
@@ -68,15 +57,14 @@
   }
 
   @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());
   }
 
   @Test
   public void testCreateGroupWhenGroupAlreadyExists_Conflict()
-      throws OrmException, JSchException, IOException {
+      throws Exception {
     RestResponse r = adminSession.put("/groups/Administrators");
     assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/DefaultGroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/DefaultGroupsIT.java
index 0d229ca..de3dee2 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,6 +14,7 @@
 
 package com.google.gerrit.acceptance.rest.group;
 
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import com.google.common.collect.Sets;
@@ -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,16 +41,17 @@
 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");
+    assertFalse(session.getError(), session.hasError());
     assertTrue(result.contains("Administrators"));
     assertTrue(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 =
@@ -65,7 +63,7 @@
   }
 
   @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());
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/GroupPropertiesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupPropertiesIT.java
index c630919..e1db8e9 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
@@ -24,7 +24,6 @@
 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,20 +31,13 @@
 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";
 
@@ -87,7 +79,7 @@
   }
 
   @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";
@@ -126,7 +118,7 @@
   }
 
   @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";
@@ -151,7 +143,7 @@
   }
 
   @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";
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..542574f 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
@@ -57,7 +57,7 @@
   }
 
   @Test
-  public void listOneGroupMember() throws IOException {
+  public void listOneGroupMember() throws Exception {
     assertEquals(GET_ONE("/groups/Administrators/members/admin").name,
         admin.fullName);
   }
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..7a09e5a 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
@@ -26,29 +26,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 +55,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";
@@ -93,8 +82,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/BanCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BanCommitIT.java
index f4414a2..063ded3 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
@@ -29,16 +29,13 @@
 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");
 
@@ -57,7 +54,7 @@
   }
 
   @Test
-  public void banAlreadyBannedCommit() throws IOException, GitAPIException {
+  public void banAlreadyBannedCommit() throws Exception {
     RestResponse r =
         adminSession.put("/projects/" + project.get() + "/ban/",
             BanCommit.Input.fromCommits("a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96"));
@@ -73,7 +70,7 @@
   }
 
   @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"));
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..433c1f0 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
@@ -16,7 +16,6 @@
 
 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;
 
@@ -24,30 +23,13 @@
 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,7 +38,7 @@
   }
 
   @Test
-  public void createBranch_Forbidden() throws IOException {
+  public void createBranch_Forbidden() throws Exception {
     RestResponse r =
         userSession.put("/projects/" + project.get()
             + "/branches/" + branch.getShortName());
@@ -64,7 +46,7 @@
   }
 
   @Test
-  public void createBranchByAdmin() throws IOException {
+  public void createBranchByAdmin() throws Exception {
     RestResponse r =
         adminSession.put("/projects/" + project.get()
             + "/branches/" + branch.getShortName());
@@ -77,7 +59,7 @@
   }
 
   @Test
-  public void branchAlreadyExists_Conflict() throws IOException {
+  public void branchAlreadyExists_Conflict() throws Exception {
     RestResponse r =
         adminSession.put("/projects/" + project.get()
             + "/branches/" + branch.getShortName());
@@ -90,8 +72,7 @@
   }
 
   @Test
-  public void createBranchByProjectOwner() throws IOException,
-      ConfigInvalidException {
+  public void createBranchByProjectOwner() throws Exception {
     grantOwner();
 
     RestResponse r =
@@ -106,8 +87,7 @@
   }
 
   @Test
-  public void createBranchByAdminCreateReferenceBlocked() throws IOException,
-      ConfigInvalidException {
+  public void createBranchByAdminCreateReferenceBlocked() throws Exception {
     blockCreateReference();
     RestResponse r =
         adminSession.put("/projects/" + project.get()
@@ -122,7 +102,7 @@
 
   @Test
   public void createBranchByProjectOwnerCreateReferenceBlocked_Forbidden()
-      throws IOException, ConfigInvalidException {
+      throws Exception {
     grantOwner();
     blockCreateReference();
     RestResponse r =
@@ -131,27 +111,13 @@
     assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
   }
 
-  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..4b74d92 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -24,25 +24,15 @@
 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,24 +48,10 @@
 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();
+    ProjectInfo p = gApi.projects().name(newProjectName).create().get();
     assertEquals(newProjectName, p.name);
     ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
     assertNotNull(projectState);
@@ -84,7 +60,7 @@
   }
 
   @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());
@@ -97,7 +73,7 @@
   }
 
   @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);
@@ -105,7 +81,7 @@
   }
 
   @Test
-  public void testCreateProjectWithProperties() throws IOException {
+  public void testCreateProjectWithProperties() throws Exception {
     final String newProjectName = "newProject";
     ProjectInput in = new ProjectInput();
     in.description = "Test description";
@@ -128,7 +104,7 @@
   }
 
   @Test
-  public void testCreateChildProject() throws IOException {
+  public void testCreateChildProject() throws Exception {
     final String parentName = "parent";
     RestResponse r = adminSession.put("/projects/" + parentName);
     r.consume();
@@ -142,7 +118,7 @@
 
   @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);
@@ -150,7 +126,7 @@
   }
 
   @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,7 +145,7 @@
 
   @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);
@@ -177,7 +153,7 @@
   }
 
   @Test
-  public void testCreatePermissionOnlyProject() throws IOException {
+  public void testCreatePermissionOnlyProject() throws Exception {
     final String newProjectName = "newProject";
     ProjectInput in = new ProjectInput();
     in.permissionsOnly = true;
@@ -186,7 +162,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 +171,7 @@
   }
 
   @Test
-  public void testCreateProjectWithBranches() throws IOException {
+  public void testCreateProjectWithBranches() throws Exception {
     final String newProjectName = "newProject";
     ProjectInput in = new ProjectInput();
     in.createEmptyCommit = true;
@@ -210,15 +186,14 @@
   }
 
   @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());
   }
 
   @Test
   public void testCreateProjectWhenProjectAlreadyExists_Conflict()
-      throws OrmException, JSchException, IOException {
+      throws Exception {
     RestResponse r = adminSession.put("/projects/All-Projects");
     assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
   }
@@ -229,7 +204,8 @@
 
   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());
@@ -240,7 +216,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 {
@@ -252,7 +229,6 @@
         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..261ed42 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
@@ -16,7 +16,6 @@
 
 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;
 
@@ -24,31 +23,14 @@
 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,7 +41,7 @@
   }
 
   @Test
-  public void deleteBranch_Forbidden() throws IOException {
+  public void deleteBranch_Forbidden() throws Exception {
     RestResponse r =
         userSession.delete("/projects/" + project.get()
             + "/branches/" + branch.getShortName());
@@ -68,7 +50,7 @@
   }
 
   @Test
-  public void deleteBranchByAdmin() throws IOException {
+  public void deleteBranchByAdmin() throws Exception {
     RestResponse r =
         adminSession.delete("/projects/" + project.get()
             + "/branches/" + branch.getShortName());
@@ -82,8 +64,7 @@
   }
 
   @Test
-  public void deleteBranchByProjectOwner() throws IOException,
-      ConfigInvalidException {
+  public void deleteBranchByProjectOwner() throws Exception {
     grantOwner();
 
     RestResponse r =
@@ -99,8 +80,7 @@
   }
 
   @Test
-  public void deleteBranchByAdminForcePushBlocked() throws IOException,
-      ConfigInvalidException {
+  public void deleteBranchByAdminForcePushBlocked() throws Exception {
     blockForcePush();
     RestResponse r =
         adminSession.delete("/projects/" + project.get()
@@ -116,7 +96,7 @@
 
   @Test
   public void deleteBranchByProjectOwnerForcePushBlocked_Forbidden()
-      throws IOException, ConfigInvalidException {
+      throws Exception {
     grantOwner();
     blockForcePush();
     RestResponse r =
@@ -126,26 +106,13 @@
     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..a3c1331 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
@@ -22,12 +22,8 @@
 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,31 +33,23 @@
 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 {
+  public void testGcNonExistingProject_NotFound() throws Exception {
     assertEquals(HttpStatus.SC_NOT_FOUND, POST("/projects/non-existing/gc"));
   }
 
   @Test
-  public void testGcNotAllowed_Forbidden() throws IOException, OrmException,
-      JSchException {
+  public void testGcNotAllowed_Forbidden() throws Exception {
     assertEquals(HttpStatus.SC_FORBIDDEN,
         userSession.post("/projects/" + allProjects.get() + "/gc")
             .getStatusCode());
@@ -69,10 +57,10 @@
 
   @Test
   @UseLocalDisk
-  public void testGcOneProject() throws JSchException, IOException {
+  public void testGcOneProject() throws Exception {
     assertEquals(HttpStatus.SC_OK, POST("/projects/" + allProjects.get() + "/gc"));
     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..bafd21a 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
@@ -23,11 +23,6 @@
 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,20 +31,14 @@
 
 public class GetChildProjectIT extends AbstractDaemonTest {
 
-  @Inject
-  private AllProjectsName allProjects;
-
-  @Inject
-  private ProjectCache projectCache;
-
   @Test
-  public void getNonExistingChildProject_NotFound() throws IOException {
+  public void getNonExistingChildProject_NotFound() throws Exception {
     assertEquals(HttpStatus.SC_NOT_FOUND,
         GET("/projects/" + allProjects.get() + "/children/non-existing").getStatusCode());
   }
 
   @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());
@@ -61,7 +50,7 @@
   }
 
   @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());
@@ -74,7 +63,7 @@
   }
 
   @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());
@@ -87,8 +76,7 @@
   }
 
   @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());
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..30610d2 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,134 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertNull;
 
+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);
+    assertEquals(commit.name(), info.commit);
+    assertEquals("Create", info.subject);
+    assertEquals("Create\n\nNew commit\n", info.message);
+    assertEquals("J. Author", info.author.name);
+    assertEquals("jauthor@example.com", info.author.email);
+    assertEquals("J. Committer", info.committer.name);
+    assertEquals("jcommitter@example.com", info.committer.email);
+
+    CommitInfo parent = Iterables.getOnlyElement(info.parents);
+    assertEquals(commit.getParent(0).name(), parent.commit);
+    assertEquals("Initial empty repository", parent.subject);
+    assertNull(parent.message);
+    assertNull(parent.author);
+    assertNull(parent.committer);
+  }
+
+  @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());
+    assertEquals(r.getCommitId().name(), info.commit);
+    assertEquals("test commit", info.subject);
+    assertEquals("test commit\n\nChange-Id: " + r.getChangeId() + "\n",
+        info.message);
+    assertEquals("admin", info.author.name);
+    assertEquals("admin@example.com", info.author.email);
+    assertEquals("admin", info.committer.name);
+    assertEquals("admin@example.com", info.committer.email);
+
+    CommitInfo parent = Iterables.getOnlyElement(info.parents);
+    assertEquals(r.getCommit().getParent(0).name(), parent.commit);
+    assertEquals("Initial empty repository", parent.subject);
+    assertNull(parent.message);
+    assertNull(parent.author);
+    assertNull(parent.committer);
+  }
+
+  @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());
+    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
+  }
+
+  private CommitInfo getCommit(ObjectId id) throws Exception {
+    RestResponse r = userSession.get(
+        "/projects/" + project.get() + "/commits/" + id.name());
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    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..32ac81d 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
@@ -16,30 +16,16 @@
 
 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,21 @@
 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 {
+  public void listBranchesOfNonExistingProject_NotFound() throws Exception {
     assertEquals(HttpStatus.SC_NOT_FOUND,
         GET("/projects/non-existing/branches").getStatusCode());
   }
 
   @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());
   }
 
   @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 +60,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");
@@ -106,8 +84,7 @@
   }
 
   @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 +100,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 +111,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);
+    assertEquals(4, result.size());
+    assertEquals("HEAD", result.get(0).ref);
+    assertEquals("refs/meta/config", result.get(1).ref);
+    assertEquals("refs/heads/master", result.get(2).ref);
+    assertEquals("refs/heads/someBranch1", result.get(3).ref);
+
+    // limit higher than total number of branches
+    r = adminSession.get("/projects/" + project.get() + "/branches?n=25");
+    result = toBranchInfoList(r);
+    assertEquals(6, result.size());
+    assertEquals("HEAD", result.get(0).ref);
+    assertEquals("refs/meta/config", result.get(1).ref);
+    assertEquals("refs/heads/master", result.get(2).ref);
+    assertEquals("refs/heads/someBranch1", result.get(3).ref);
+    assertEquals("refs/heads/someBranch2", result.get(4).ref);
+    assertEquals("refs/heads/someBranch3", result.get(5).ref);
+
+    // using skip only
+    r = adminSession.get("/projects/" + project.get() + "/branches?s=2");
+    result = toBranchInfoList(r);
+    assertEquals(4, result.size());
+    assertEquals("refs/heads/master", result.get(0).ref);
+    assertEquals("refs/heads/someBranch1", result.get(1).ref);
+    assertEquals("refs/heads/someBranch2", result.get(2).ref);
+    assertEquals("refs/heads/someBranch3", result.get(3).ref);
+
+    // skip more branches than the number of available branches
+    r = adminSession.get("/projects/" + project.get() + "/branches?s=7");
+    result = toBranchInfoList(r);
+    assertEquals(0, result.size());
+
+    // using skip and limit
+    r = adminSession.get("/projects/" + project.get() + "/branches?s=2&n=2");
+    result = toBranchInfoList(r);
+    assertEquals(2, result.size());
+    assertEquals("refs/heads/master", result.get(0).ref);
+    assertEquals("refs/heads/someBranch1", result.get(1).ref);
   }
 
-  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);
+    assertEquals(3, result.size());
+    assertEquals("refs/heads/someBranch1", result.get(0).ref);
+    assertEquals("refs/heads/someBranch2", result.get(1).ref);
+    assertEquals("refs/heads/someBranch3", result.get(2).ref);
+
+    r = adminSession.get("/projects/" + project.get() + "/branches?m=Branch");
+    result = toBranchInfoList(r);
+    assertEquals(3, result.size());
+    assertEquals("refs/heads/someBranch1", result.get(0).ref);
+    assertEquals("refs/heads/someBranch2", result.get(1).ref);
+    assertEquals("refs/heads/someBranch3", result.get(2).ref);
+
+    //using regex
+    r = adminSession.get("/projects/" + project.get() + "/branches?r=.*ast.*r");
+    result = toBranchInfoList(r);
+    assertEquals(1, result.size());
+    assertEquals("refs/heads/master", result.get(0).ref);
+  }
+
+  private RestResponse GET(String endpoint) throws IOException {
+    return adminSession.get(endpoint);
   }
 
   private static List<BranchInfo> toBranchInfoList(RestResponse r)
@@ -154,19 +202,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..d0e678f 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
@@ -23,11 +23,7 @@
 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,17 +34,14 @@
 
 public class ListChildProjectsIT extends AbstractDaemonTest {
 
-  @Inject
-  private AllProjectsName allProjects;
-
   @Test
-  public void listChildrenOfNonExistingProject_NotFound() throws IOException {
+  public void listChildrenOfNonExistingProject_NotFound() throws Exception {
     assertEquals(HttpStatus.SC_NOT_FOUND,
         GET("/projects/non-existing/children/").getStatusCode());
   }
 
   @Test
-  public void listNoChildren() throws IOException {
+  public void listNoChildren() throws Exception {
     RestResponse r = GET("/projects/" + allProjects.get() + "/children/");
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
     List<ProjectInfo> projectInfoList = toProjectInfoList(r);
@@ -57,7 +50,7 @@
   }
 
   @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());
@@ -75,7 +68,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");
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..5911492 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
@@ -24,15 +24,11 @@
 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,13 +39,10 @@
 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());
 
@@ -61,7 +54,7 @@
   }
 
   @Test
-  public void listProjectsWithBranch() throws IOException, JSchException {
+  public void listProjectsWithBranch() throws Exception {
     RestResponse r = GET("/projects/?b=master");
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
     Map<String, ProjectInfo> result = toProjectInfoMap(r);
@@ -72,7 +65,7 @@
   }
 
   @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";
@@ -94,7 +87,7 @@
   }
 
   @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());
     }
@@ -112,7 +105,7 @@
   }
 
   @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,6 +114,11 @@
     Project.NameKey projectAwesome = new Project.NameKey("project-awesome");
     createProject(sshSession, projectAwesome.get());
 
+    assertEquals(HttpStatus.SC_BAD_REQUEST,
+        GET("/projects/?p=some&r=.*").getStatusCode());
+    assertEquals(HttpStatus.SC_BAD_REQUEST,
+        GET("/projects/?p=some&m=some").getStatusCode());
+
     RestResponse r = GET("/projects/?p=some");
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
     Map<String, ProjectInfo> result = toProjectInfoMap(r);
@@ -129,7 +127,7 @@
   }
 
   @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,13 +136,22 @@
     Project.NameKey projectAwesome = new Project.NameKey("project-awesome");
     createProject(sshSession, projectAwesome.get());
 
+    assertEquals(HttpStatus.SC_BAD_REQUEST,
+        GET("/projects/?r=[.*some").getStatusCode());
+    assertEquals(HttpStatus.SC_BAD_REQUEST,
+        GET("/projects/?r=.*&p=s").getStatusCode());
+    assertEquals(HttpStatus.SC_BAD_REQUEST,
+        GET("/projects/?r=.*&m=s").getStatusCode());
+
     RestResponse r = GET("/projects/?r=.*some");
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
     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$");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    result = toProjectInfoMap(r);
+    assertProjects(Arrays.asList(someProject), result.values());
 
     r = GET("/projects/?r=.*");
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
@@ -154,7 +161,7 @@
   }
 
   @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());
     }
@@ -172,7 +179,7 @@
   }
 
   @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,6 +188,11 @@
     Project.NameKey projectAwesome = new Project.NameKey("project-awesome");
     createProject(sshSession, projectAwesome.get());
 
+    assertEquals(HttpStatus.SC_BAD_REQUEST,
+        GET("/projects/?m=some&r=.*").getStatusCode());
+    assertEquals(HttpStatus.SC_BAD_REQUEST,
+        GET("/projects/?m=some&p=some").getStatusCode());
+
     RestResponse r = GET("/projects/?m=some");
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
     Map<String, ProjectInfo> result = toProjectInfoMap(r);
@@ -190,7 +202,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());
@@ -207,7 +219,7 @@
   }
 
   @Test
-  public void listProjectWithType() throws RestApiException, IOException {
+  public void listProjectWithType() throws Exception {
     RestResponse r = GET("/projects/?type=PERMISSIONS");
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
     Map<String, ProjectInfo> result = toProjectInfoMap(r);
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..e4378b4 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
@@ -16,70 +16,28 @@
 
 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,18 @@
             configName, cfg.toText());
     push.to(git, RefNames.REFS_CONFIG);
 
-    ProjectState state = projectCache.get(new Project.NameKey(project));
+    ProjectState state = projectCache.get(project);
     assertEquals(cfg.toText(), state.getConfig(configName).get().toText());
   }
 
   @Test
   public void nonExistingConfig() {
-    ProjectState state = projectCache.get(new Project.NameKey(project));
+    ProjectState state = projectCache.get(project);
     assertEquals("", state.getConfig("test.config").get().toText());
   }
 
   @Test
-  public void withInheritance() throws GitAPIException, IOException {
+  public void withInheritance() throws Exception {
     String configName = "test.config";
 
     Config parentCfg = new Config();
@@ -110,7 +68,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 +83,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");
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..eda79a1 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
@@ -20,24 +20,14 @@
 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 =
@@ -48,7 +38,7 @@
   }
 
   @Test
-  public void setParent() throws IOException, JSchException {
+  public void setParent() throws Exception {
     String parent = "parent";
     createProject(sshSession, parent, null, true);
     RestResponse r =
@@ -66,7 +56,7 @@
   }
 
   @Test
-  public void setParentForAllProjects_Conflict() throws IOException {
+  public void setParentForAllProjects_Conflict() throws Exception {
     RestResponse r =
         adminSession.put("/projects/" + allProjects.get() + "/parent",
             newParentInput(project.get()));
@@ -75,7 +65,7 @@
   }
 
   @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()));
@@ -98,7 +88,7 @@
   }
 
   @Test
-  public void setNonExistingParent_UnprocessibleEntity() throws IOException {
+  public void setNonExistingParent_UnprocessibleEntity() throws Exception {
     RestResponse r =
         adminSession.put("/projects/" + project.get() + "/parent",
             newParentInput("non-existing"));
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..58dc065
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.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.acceptance.rest.project;
+
+import static org.junit.Assert.assertEquals;
+
+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 {
+    assertEquals(HttpStatus.SC_NOT_FOUND,
+        adminSession.get("/projects/non-existing/tags").getStatusCode());
+  }
+
+  @Test
+  public void listTagsOfNonVisibleProject_NotFound() throws Exception {
+    blockRead(project, "refs/*");
+    assertEquals(HttpStatus.SC_NOT_FOUND,
+        userSession.get("/projects/" + project.get() + "/tags").getStatusCode());
+  }
+
+  @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"));
+    assertEquals(2, result.size());
+
+    TagInfo t = result.get(0);
+    assertEquals("refs/tags/" + tag1.name, t.ref);
+    assertEquals(r1.getCommitId().getName(), t.revision);
+
+    t = result.get(1);
+    assertEquals("refs/tags/" + tag2.name, t.ref);
+    assertEquals(r2.getCommitId().getName(), t.object);
+    assertEquals(tag2.message, t.message);
+    assertEquals(tag2.tagger.getName(), t.tagger.name);
+    assertEquals(tag2.tagger.getEmailAddress(), t.tagger.email);
+  }
+
+  @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"));
+    assertEquals(2, result.size());
+    assertEquals("refs/tags/" + tag1.name, result.get(0).ref);
+    assertEquals(r1.getCommitId().getName(), result.get(0).revision);
+    assertEquals("refs/tags/" + tag2.name, result.get(1).ref);
+    assertEquals(r2.getCommitId().getName(), result.get(1).revision);
+
+    blockRead(project, "refs/heads/hidden");
+    result =
+        toTagInfoList(adminSession.get("/projects/" + project.get() + "/tags"));
+    assertEquals(1, result.size());
+    assertEquals("refs/tags/" + tag1.name, result.get(0).ref);
+    assertEquals(r1.getCommitId().getName(), result.get(0).revision);
+  }
+
+  @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);
+    assertEquals("refs/tags/" + tag1.name, tagInfo.ref);
+    assertEquals(r1.getCommitId().getName(), tagInfo.revision);
+  }
+
+  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..f5a765f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -0,0 +1,194 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+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);
+    assertEquals(1, result.size());
+    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);
+    assertTrue(!result.isEmpty());
+    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);
+    assertTrue(drafts.isEmpty());
+  }
+
+  private CommentInfo addDraft(String changeId, String revId,
+      ReviewInput.CommentInput c) throws IOException {
+    RestResponse r = userSession.put(
+        "/changes/" + changeId + "/revisions/" + revId + "/drafts", c);
+    assertEquals(HttpStatus.SC_CREATED, r.getStatusCode());
+    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);
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+  }
+
+  private void deleteDraft(String changeId, String revId, String uuid)
+      throws IOException {
+    RestResponse r = userSession.delete(
+        "/changes/" + changeId + "/revisions/" + revId + "/drafts/" + uuid);
+    assertEquals(HttpStatus.SC_NO_CONTENT, r.getStatusCode());
+  }
+
+  private Map<String, List<CommentInfo>> getPublishedComments(String changeId,
+      String revId) throws IOException {
+    RestResponse r = userSession.get(
+        "/changes/" + changeId + "/revisions/" + revId + "/comments/");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    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/");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    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);
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    return newGson().fromJson(r.getReader(), CommentInfo.class);
+  }
+
+  private static void assertCommentInfo(ReviewInput.CommentInput expected,
+      CommentInfo actual) {
+    assertEquals(expected.line, actual.line);
+    assertEquals(expected.message, actual.message);
+    assertEquals(expected.inReplyTo, actual.inReplyTo);
+    if (actual.side == null) {
+      assertEquals(expected.side, Comment.Side.REVISION);
+    }
+  }
+
+  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..b3bbc57 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
@@ -28,20 +28,26 @@
 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);
@@ -49,8 +55,7 @@
   }
 
   @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");
@@ -66,8 +71,7 @@
   }
 
   @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");
@@ -100,8 +104,7 @@
   }
 
   @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");
@@ -139,15 +142,51 @@
     }
   }
 
+  @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));
+    String editRev = editUtil.byChange(ch2).get().getRevision().get();
+
+    List<ChangeAndCommit> related = getRelated(ch2.getId(), 0);
+    assertEquals(3, related.size());
+    assertEquals("related to " + c2.getChangeId(), c3.getChangeId(), related.get(0).changeId);
+    assertEquals("related to " + c2.getChangeId(), c2.getChangeId(), related.get(1).changeId);
+    assertEquals("has edit revision number", 0, related.get(1)._revisionNumber.intValue());
+    assertEquals("has edit revision " + editRev, editRev, related.get(1).commit.commit);
+    assertEquals("related to " + c2.getChangeId(), c1.getChangeId(), related.get(2).changeId);
+  }
+
   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..52aa84e 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
@@ -24,6 +24,7 @@
 
 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 +33,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 +55,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);
@@ -90,9 +88,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);
@@ -127,9 +123,7 @@
   }
 
   @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);
@@ -157,9 +151,7 @@
   }
 
   @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);
@@ -212,16 +204,16 @@
   }
 
   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..f1d54cb 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
@@ -15,7 +15,6 @@
 package com.google.gerrit.acceptance.server.project;
 
 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;
@@ -32,12 +31,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 +42,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 +52,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);
   }
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..0249621 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
@@ -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
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..37b270c 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
@@ -22,27 +22,25 @@
 
 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(sshSession.getError(), sshSession.hasError());
     assertFalse(response, response.toLowerCase(Locale.US).contains("error"));
 
     PushResult pushResult = pushHead(git, "refs/heads/master", false);
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..d653b5f 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
@@ -21,31 +21,25 @@
 
 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 +48,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 +62,27 @@
 
   @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());
+    assertFalse(sshSession.getError(), sshSession.hasError());
     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());
+    assertFalse(sshSession.getError(), sshSession.hasError());
     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,15 +91,15 @@
 
   @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));
+        Arrays.asList(allProjects, project, project2, project3));
     assertTrue(result.hasErrors());
     assertEquals(1, result.getErrors().size());
     GarbageCollectionResult.Error error = result.getErrors().get(0);
     assertEquals(GarbageCollectionResult.Error.Type.GC_ALREADY_SCHEDULED, error.getType());
-    assertEquals(project1, error.getProjectName());
+    assertEquals(project, error.getProjectName());
   }
 
   private void assertError(String expectedError, String response) {
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..4be4ab6 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
      | '$'
      | '%'
      | '&'
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 85e4599..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);
@@ -161,8 +161,9 @@
       return defaultFactory.build(def);
     }
 
-    SqlStore<K, V> store = newSqlStore(def.name(), def.keyType(), limit);
-    H2CacheImpl<K, V> cache = new H2CacheImpl<K, V>(
+    SqlStore<K, V> store = newSqlStore(def.name(), def.keyType(), limit,
+        def.expireAfterWrite(TimeUnit.SECONDS));
+    H2CacheImpl<K, V> cache = new H2CacheImpl<>(
         executor, store, def.keyType(),
         (Cache<K, ValueHolder<V>>) defaultFactory.create(def, true).build());
     synchronized (caches) {
@@ -182,12 +183,13 @@
       return defaultFactory.build(def, loader);
     }
 
-    SqlStore<K, V> store = newSqlStore(def.name(), def.keyType(), limit);
+    SqlStore<K, V> store = newSqlStore(def.name(), def.keyType(), limit,
+        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;
@@ -209,9 +211,11 @@
   private <V, K> SqlStore<K, V> newSqlStore(
       String name,
       TypeLiteral<K> keyType,
-      long maxSize) {
+      long maxSize,
+      Long expireAfterWrite) {
     File db = new File(cacheDir, name).getAbsoluteFile();
     String url = "jdbc:h2:" + db.toURI().toString();
-    return new SqlStore<>(url, keyType, maxSize);
+    return new SqlStore<>(url, keyType, maxSize,
+        expireAfterWrite == null ? 0 : expireAfterWrite.longValue());
   }
 }
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 652ed30..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;
@@ -313,16 +313,19 @@
     private final String url;
     private final KeyType<K> keyType;
     private final long maxSize;
+    private final long expireAfterWrite;
     private final BlockingQueue<SqlHandle> handles;
     private final AtomicLong hitCount = new AtomicLong();
     private final AtomicLong missCount = new AtomicLong();
     private volatile BloomFilter<K> bloomFilter;
     private int estimatedSize;
 
-    SqlStore(String jdbcUrl, TypeLiteral<K> keyType, long maxSize) {
+    SqlStore(String jdbcUrl, TypeLiteral<K> keyType, long maxSize,
+        long expireAfterWrite) {
       this.url = jdbcUrl;
       this.keyType = KeyType.create(keyType);
       this.maxSize = maxSize;
+      this.expireAfterWrite = expireAfterWrite;
 
       int cores = Runtime.getRuntime().availableProcessors();
       int keep = Math.min(cores, 16);
@@ -408,7 +411,7 @@
       try {
         c = acquire();
         if (c.get == null) {
-          c.get = c.conn.prepareStatement("SELECT v FROM data WHERE k=?");
+          c.get = c.conn.prepareStatement("SELECT v, created FROM data WHERE k=?");
         }
         keyType.set(c.get, 1, key);
         ResultSet r = c.get.executeQuery();
@@ -418,6 +421,13 @@
             return null;
           }
 
+          Timestamp created = r.getTimestamp(2);
+          if (expired(created)) {
+            invalidate(key);
+            missCount.incrementAndGet();
+            return null;
+          }
+
           @SuppressWarnings("unchecked")
           V val = (V) r.getObject(1);
           ValueHolder<V> h = new ValueHolder<>(val);
@@ -438,6 +448,14 @@
       }
     }
 
+    private boolean expired(Timestamp created) {
+      if (expireAfterWrite == 0) {
+        return false;
+      }
+      long age = TimeUtil.nowMs() - created.getTime();
+      return 1000 * expireAfterWrite < age;
+    }
+
     private void touch(SqlHandle c, K key) throws SQLException {
       if (c.touch == null) {
         c.touch =c.conn.prepareStatement("UPDATE data SET accessed=? WHERE k=?");
@@ -552,12 +570,14 @@
           r = s.executeQuery("SELECT"
               + " k"
               + ",OCTET_LENGTH(k) + OCTET_LENGTH(v)"
+              + ",created"
               + " FROM data"
               + " ORDER BY accessed");
           try {
             while (maxSize < used && r.next()) {
               K key = keyType.get(r, 1);
-              if (mem.getIfPresent(key) != null) {
+              Timestamp created = r.getTimestamp(3);
+              if (mem.getIfPresent(key) != null && !expired(created)) {
                 touch(c, key);
               } else {
                 invalidate(c, key);
diff --git a/gerrit-common/BUCK b/gerrit-common/BUCK
index 3ba22c5..a719fef 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,6 +50,7 @@
     '//lib:gwtorm',
     '//lib:guava',
     '//lib/jgit:jgit',
+    '//lib/joda:joda-time',
   ],
   visibility = ['PUBLIC'],
 )
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/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/GlobalCapability.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
index f055136..e4b0381 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
@@ -38,6 +38,9 @@
   /** Can create any account on the server. */
   public static final String CREATE_ACCOUNT = "createAccount";
 
+  /** Can modify any account on the server. */
+  public static final String MODIFY_ACCOUNT = "modifyAccount";
+
   /** Can create any group on the server. */
   public static final String CREATE_GROUP = "createGroup";
 
@@ -58,9 +61,6 @@
   /** 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";
 
@@ -110,6 +110,7 @@
     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);
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/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/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..0c1b6a8 100644
--- a/gerrit-extension-api/BUCK
+++ b/gerrit-extension-api/BUCK
@@ -47,7 +47,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..6dd6b06 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,9 +59,23 @@
   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;
 
@@ -48,6 +87,18 @@
   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;
+
+  /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
    **/
@@ -103,6 +154,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 +187,15 @@
     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();
+    }
   }
 }
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/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/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/ChangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 653ec37..17517b8 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
@@ -39,5 +39,6 @@
   public Map<String, LabelInfo> labels;
   public Collection<ChangeMessageInfo> messages;
   public Map<String, RevisionInfo> revisions;
+  public String baseChange;
   public int _number;
 }
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/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/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/Response.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
index 314c898..385554c 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
@@ -52,6 +52,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 +101,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/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..3f04807 100644
--- a/gerrit-gwtdebug/BUCK
+++ b/gerrit-gwtdebug/BUCK
@@ -1,11 +1,17 @@
 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/gwt:codeserver',
     '//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..66fc127
--- /dev/null
+++ b/gerrit-gwtdebug/src/main/java/com/google/gwt/dev/codeserver/WebServer.java
@@ -0,0 +1,514 @@
+/*
+ * 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.UnableToCompleteException;
+import com.google.gwt.dev.json.JsonArray;
+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.io.PrintWriter;
+import java.util.EnumSet;
+import java.util.HashMap;
+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 + "$");
+
+  private static final Pattern SAFE_CALLBACK =
+      Pattern.compile("([a-zA-Z_][a-zA-Z0-9_]*\\.)*[a-zA-Z_][a-zA-Z0-9_]*");
+
+  private static final MimeTypes MIME_TYPES = new MimeTypes();
+
+  private final SourceHandler handler;
+
+  private final Modules modules;
+
+  private final String bindAddress;
+  private final int port;
+  private final TreeLogger logger;
+  private Server server;
+
+  WebServer(SourceHandler handler, Modules modules, String bindAddress, int port,
+      TreeLogger logger) {
+    this.handler = handler;
+    this.modules = modules;
+    this.bindAddress = bindAddress;
+    this.port = port;
+    this.logger = logger;
+  }
+
+  @SuppressWarnings("serial")
+  public void start() 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 handler = new ServletContextHandler(ServletContextHandler.SESSIONS);
+    handler.setContextPath("/");
+    handler.addServlet(new ServletHolder(new HttpServlet() {
+      @Override
+      protected void doGet(HttpServletRequest request, HttpServletResponse response)
+          throws ServletException, IOException {
+        handleRequest(request.getPathInfo(), request, response);
+      }
+    }), "/*");
+    handler.addFilter(GzipFilter.class, "/*", EnumSet.allOf(DispatcherType.class));
+    newServer.setHandler(handler);
+    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.)
+   */
+  public File getCurrentWarDir(String moduleName) {
+    return modules.get(moduleName).getWarDir();
+  }
+
+  private void handleRequest(String target, HttpServletRequest request,
+      HttpServletResponse response)
+      throws IOException {
+
+    if (request.getMethod().equalsIgnoreCase("get")) {
+      doGet(target, request, response);
+    }
+  }
+
+  private void doGet(String target, HttpServletRequest request, HttpServletResponse response)
+      throws IOException {
+
+    if (!target.endsWith(".cache.js")) {
+      // Make sure IE9 doesn't cache any pages.
+      // (Nearly all pages may change on server restart.)
+      PageUtil.setNoCacheHeaders(response);
+    }
+
+    if (target.equals("/")) {
+      setHandled(request);
+      JsonObject config = makeConfig();
+      PageUtil.sendJsonAndHtml("config", config, "frontpage.html", response, logger);
+      return;
+    }
+
+    if (target.equals("/dev_mode_on.js")) {
+      setHandled(request);
+      JsonObject config = makeConfig();
+      PageUtil
+          .sendJsonAndJavaScript("__gwt_codeserver_config", config, "dev_mode_on.js", response,
+              logger);
+      return;
+    }
+
+    // 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/")) {
+      setHandled(request);
+      String moduleName = target.substring("/recompile/".length());
+      ModuleState moduleState = modules.get(moduleName);
+      if (moduleState == null) {
+        response.sendError(HttpServletResponse.SC_NOT_FOUND);
+        logger.log(TreeLogger.WARN, "not found: " + target);
+        return;
+      }
+
+      // 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.
+      boolean ok = moduleState.recompile(getBindingProperties(request));
+
+      JsonObject config = makeConfig();
+      config.put("status", ok ? "ok" : "failed");
+      sendJsonpPage(config, request, response);
+      return;
+    }
+
+    if (target.startsWith("/log/")) {
+      setHandled(request);
+      String moduleName = target.substring("/log/".length());
+      File file = modules.get(moduleName).getCompileLog();
+      sendLogPage(moduleName, file, response);
+      return;
+    }
+
+    if (target.equals("/favicon.ico")) {
+      InputStream faviconStream = getClass().getResourceAsStream("favicon.ico");
+      if (faviconStream != null) {
+        setHandled(request);
+        // IE8 will not load the favicon in an img tag with the default MIME type,
+        // so use "image/x-icon" instead.
+        PageUtil.sendStream("image/x-icon", faviconStream, response);
+      }
+      return;
+    }
+
+    if (target.equals("/policies/")) {
+      setHandled(request);
+      sendPolicyIndex(response);
+      return;
+    }
+
+    Matcher matcher = SAFE_MODULE_PATH.matcher(target);
+    if (matcher.matches()) {
+      setHandled(request);
+      sendModulePage(matcher.group(1), response);
+      return;
+    }
+
+    matcher = SAFE_DIRECTORY_PATH.matcher(target);
+    if (matcher.matches() && handler.isSourceMapRequest(target)) {
+      setHandled(request);
+      handler.handle(target, request, response);
+      return;
+    }
+
+    matcher = SAFE_FILE_PATH.matcher(target);
+    if (matcher.matches()) {
+      setHandled(request);
+      if (handler.isSourceMapRequest(target)) {
+        handler.handle(target, request, response);
+        return;
+      }
+      if (target.startsWith("/policies/")) {
+        sendPolicyFile(target, response);
+        return;
+      }
+      sendOutputFile(target, request, response);
+      return;
+    }
+
+    logger.log(TreeLogger.WARN, "ignored get request: " + target);
+  }
+
+  private void sendOutputFile(String target, HttpServletRequest request,
+      HttpServletResponse response) throws IOException {
+
+    int secondSlash = target.indexOf('/', 1);
+    String moduleName = target.substring(1, secondSlash);
+    ModuleState moduleState = modules.get(moduleName);
+
+    File file = moduleState.getOutputFile(target);
+    if (!file.isFile()) {
+      // perhaps it's compressed
+      file = moduleState.getOutputFile(target + ".gz");
+      if (!file.isFile()) {
+        response.sendError(HttpServletResponse.SC_NOT_FOUND);
+        logger.log(TreeLogger.WARN, "not found: " + file.toString());
+        return;
+      }
+      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");
+    }
+
+    if (target.endsWith(".cache.js")) {
+      response.setHeader("X-SourceMap", sourceMapLocationForModule(moduleName));
+    }
+    response.setHeader("Access-Control-Allow-Origin", "*");
+    String mimeType = guessMimeType(target);
+    PageUtil.sendFile(mimeType, file, response);
+  }
+
+  private void sendModulePage(String moduleName, HttpServletResponse response) throws IOException {
+    ModuleState module = modules.get(moduleName);
+    if (module == null) {
+      response.sendError(HttpServletResponse.SC_NOT_FOUND);
+      logger.log(TreeLogger.WARN, "module not found: " + moduleName);
+      return;
+    }
+    PageUtil
+        .sendJsonAndHtml("config", module.getTemplateVariables(), "modulepage.html", response,
+            logger);
+  }
+
+  private void sendPolicyIndex(HttpServletResponse response) 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 (String moduleName : modules) {
+      ModuleState module = modules.get(moduleName);
+      File manifest = module.getExtraFile("rpcPolicyManifest/manifest.txt");
+      if (manifest.isFile()) {
+        out.startTag("h2").text(moduleName).endTag("h2").nl();
+
+        out.startTag("table").nl();
+        String text = PageUtil.loadFile(manifest);
+        for (String line : text.split("\n")) {
+          line = line.trim();
+          if (line.isEmpty() || line.startsWith("#")) {
+            continue;
+          }
+          String[] fields = line.split(", ");
+          if (fields.length < 2) {
+            continue;
+          }
+
+          String serviceName = fields[0];
+          String policyFileName = fields[1];
+
+          String serviceUrl = SourceHandler.SOURCEMAP_PATH + moduleName + "/" +
+              serviceName.replace('.', '/') + ".java";
+          String policyUrl = "/policies/" + policyFileName;
+
+          out.startTag("tr");
+
+          out.startTag("td");
+          out.startTag("a", "href=", serviceUrl).text(serviceName).endTag("a");
+          out.endTag("td");
+
+          out.startTag("td");
+          out.startTag("a", "href=", policyUrl).text(policyFileName).endTag("a");
+          out.endTag("td");
+
+          out.endTag("tr").nl();
+        }
+        out.endTag("table").nl();
+      }
+    }
+
+    out.endTag("body").nl();
+    out.endTag("html").nl();
+  }
+
+  private void sendPolicyFile(String target, HttpServletResponse response) throws IOException {
+    int secondSlash = target.indexOf('/', 1);
+    if (secondSlash < 1) {
+      response.sendError(HttpServletResponse.SC_NOT_FOUND);
+      return;
+    }
+    String rest = target.substring(secondSlash + 1);
+    if (rest.contains("/") || !rest.endsWith(".gwt.rpc")) {
+      response.sendError(HttpServletResponse.SC_NOT_FOUND);
+      return;
+    }
+
+    for (String moduleName : modules) {
+      ModuleState module = modules.get(moduleName);
+      File policy = module.getOutputFile(moduleName + "/" + rest);
+      if (policy.isFile()) {
+        PageUtil.sendFile("text/plain", policy, response);
+        return;
+      }
+    }
+
+    logger.log(TreeLogger.Type.WARN, "policy file not found: " + rest);
+    response.sendError(HttpServletResponse.SC_NOT_FOUND);
+  }
+
+  private JsonObject makeConfig() {
+    JsonArray moduleNames = new JsonArray();
+    for (String module : modules) {
+      moduleNames.add(module);
+    }
+    JsonObject config = JsonObject.create();
+    config.put("moduleNames", moduleNames);
+    return config;
+  }
+
+  private void sendJsonpPage(JsonObject json, HttpServletRequest request,
+      HttpServletResponse response) throws IOException {
+
+    response.setStatus(HttpServletResponse.SC_OK);
+    response.setContentType("application/javascript");
+    PrintWriter out = response.getWriter();
+
+    String callbackExpression = request.getParameter("_callback");
+    if (callbackExpression == null || !SAFE_CALLBACK.matcher(callbackExpression).matches()) {
+      logger.log(TreeLogger.ERROR, "invalid callback: " + callbackExpression);
+      out.print("/* invalid callback parameter */");
+      return;
+    }
+
+    out.print(callbackExpression + "(");
+    json.write(out);
+    out.println(");");
+  }
+
+  /**
+   * Sends the log file as html with errors highlighted in red.
+   */
+  private void sendLogPage(String moduleName, File file, HttpServletResponse response)
+       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(moduleName + " 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;
+  }
+
+  public static String sourceMapLocationForModule(String moduleName) {
+     return SourceHandler.SOURCEMAP_PATH + moduleName +
+         "/gwtSourceMap.json";
+  }
+
+  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/BUCK b/gerrit-gwtui/BUCK
index f1c9e6b..562f2d6 100644
--- a/gerrit-gwtui/BUCK
+++ b/gerrit-gwtui/BUCK
@@ -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',
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/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
index 2ad9a5e..cab7766 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;
@@ -148,7 +149,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);
     }
@@ -239,7 +240,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 +433,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 +536,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 +560,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 +592,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 +602,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 +644,7 @@
   private static void publish(final PatchSet.Id ps) {
     String token = toPublish(ps);
     new AsyncSplit(token) {
+      @Override
       public void onSuccess() {
         Gerrit.display(token, select());
       }
@@ -659,6 +671,7 @@
         Gerrit.getPatchScreenTopView() : topView;
 
     GWT.runAsync(new AsyncSplit(token) {
+      @Override
       public void onSuccess() {
         Gerrit.display(token, select());
       }
@@ -732,6 +745,7 @@
 
   private static void settings(String token) {
     GWT.runAsync(new AsyncSplit(token) {
+      @Override
       public void onSuccess() {
         Gerrit.display(token, select());
       }
@@ -799,6 +813,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 +987,7 @@
       this.token = token;
     }
 
+    @Override
     public final void onFailure(Throwable reason) {
       if (!isReloadUi
           && "HTTP download failed with status 404".equals(reason.getMessage())) {
@@ -988,6 +1004,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..8e5374c 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;
@@ -642,6 +650,7 @@
     final LinkMenuItem dashboardsMenuItem =
         new ProjectLinkMenuItem(C.menuProjectsDashboards(),
             ProjectScreen.DASHBOARDS) {
+      @Override
       protected boolean match(String token) {
         return super.match(token) ||
             (!getTargetHistoryToken().isEmpty() && ("/admin" + token).startsWith(getTargetHistoryToken()));
@@ -703,6 +712,7 @@
 
         case OPENID:
           menuRight.addItem(C.menuRegister(), new Command() {
+            @Override
             public void execute() {
               String t = History.getToken();
               if (t == null) {
@@ -712,6 +722,7 @@
             }
           });
           menuRight.addItem(C.menuSignIn(), new Command() {
+            @Override
             public void execute() {
               doSignIn(History.getToken());
             }
@@ -720,6 +731,7 @@
 
         case OPENID_SSO:
           menuRight.addItem(C.menuSignIn(), new Command() {
+            @Override
             public void execute() {
               doSignIn(History.getToken());
             }
@@ -742,6 +754,7 @@
             menuRight.add(anchor(registerText, cfg.getRegisterUrl()));
           }
           menuRight.addItem(C.menuSignIn(), new Command() {
+            @Override
             public void execute() {
               doSignIn(History.getToken());
             }
@@ -754,17 +767,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 +899,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 +964,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/GerritResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
index 2b78fa4..c45c797 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
@@ -37,6 +37,9 @@
   @Source("editText.png")
   public ImageResource edit();
 
+  @Source("mediaFloppy.png")
+  public ImageResource save();
+
   @Source("starOpen.gif")
   public ImageResource starOpen();
 
@@ -49,6 +52,9 @@
   @Source("redNot.png")
   public ImageResource redNot();
 
+  @Source("editUndo.png")
+  public ImageResource editUndo();
+
   @Source("downloadIcon.png")
   public ImageResource downloadIcon();
 
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/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/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..029e7c2 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
@@ -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..0de353d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
@@ -76,6 +76,7 @@
     super.onLoad();
     Util.ACCOUNT_SEC
         .myExternalIds(new ScreenLoadCallback<List<AccountExternalId>>(this) {
+          @Override
           public void preDisplay(final List<AccountExternalId> result) {
             identites.display(result);
           }
@@ -126,6 +127,7 @@
         deleteIdentity.setEnabled(false);
         Util.ACCOUNT_SEC.deleteExternalIds(keys,
             new GerritCallback<Set<AccountExternalId.Key>>() {
+              @Override
               public void onSuccess(final Set<AccountExternalId.Key> removed) {
                 for (int row = 1; row < table.getRowCount();) {
                   final AccountExternalId k = getRowItem(row);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
index a528912..20c0c38 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
@@ -213,6 +213,7 @@
 
     Util.ACCOUNT_SVC.addProjectWatch(projectName, filter,
         new GerritCallback<AccountProjectWatchInfo>() {
+          @Override
           public void onSuccess(final AccountProjectWatchInfo result) {
             addNew.setEnabled(true);
             nameBox.setEnabled(true);
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..f6c0002 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
@@ -96,6 +96,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));
@@ -135,6 +136,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 +167,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 +196,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);
               }
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..0562689 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
@@ -172,6 +172,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 +201,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 +250,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 +356,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/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/GroupListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
index 6579f83..5bf2649 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
@@ -90,7 +90,7 @@
     // 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 IgnoreOutdatedFilterResultsCallbackWrapper<>(this,
             new GerritCallback<GroupMap>() {
               @Override
               public void onSuccess(GroupMap result) {
@@ -148,10 +148,10 @@
     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);
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..a76638e 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
@@ -133,6 +133,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..163702c 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
@@ -176,14 +176,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..e28e465 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
@@ -104,7 +104,7 @@
     // 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 IgnoreOutdatedFilterResultsCallbackWrapper<>(this,
             new GerritCallback<ProjectMap>() {
               @Override
               public void onSuccess(ProjectMap result) {
@@ -161,10 +161,10 @@
     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() {
@@ -236,12 +236,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());
+            }
           }
         }
       }
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/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/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..6d68afd 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
@@ -92,6 +92,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 +108,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..9bcc24f 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,9 @@
   String nextChange();
   String openChange();
   String reviewedFileTitle();
+  String editFileInline();
+  String removeFileInline();
+  String restoreFileInline();
 
   String openLastFile();
   String openCommitMessage();
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..e16f5be 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,9 @@
 nextChange = Next related change
 openChange = Open related change
 reviewedFileTitle = Mark file as reviewed (Shortcut: r)
+editFileInline = Edit file inline
+removeFileInline = Remove file inline
+restoreFileInline = Restore file inline
 
 openLastFile = Open last file
 openCommitMessage = Open commit message
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..5c75c0b 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,7 @@
 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.MessageInfo;
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 import com.google.gerrit.client.changes.ChangeList;
@@ -107,6 +108,7 @@
     String label_need();
     String replyBox();
     String selected();
+    String hashtagName();
   }
 
   static ChangeScreen2 get(NativeEvent in) {
@@ -125,6 +127,7 @@
   private String revision;
   private ChangeInfo changeInfo;
   private CommentLinkProcessor commentLinkProcessor;
+  private EditInfo edit;
 
   private KeyCommandSet keysNavigation;
   private KeyCommandSet keysAction;
@@ -134,6 +137,7 @@
   private UpdateAvailableBar updateAvailable;
   private boolean openReplyBox;
   private boolean loaded;
+  private FileTable.Mode fileTableMode;
 
   @UiField HTMLPanel headerLine;
   @UiField Style style;
@@ -142,6 +146,8 @@
 
   @UiField Element ccText;
   @UiField Reviewers reviewers;
+  @UiField Hashtags hashtags;
+  @UiField Element hashtagTableRow;
   @UiField FlowPanel ownerPanel;
   @UiField InlineHyperlink ownerLink;
   @UiField Element statusText;
@@ -170,6 +176,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 +189,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,13 +208,24 @@
   @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) {
@@ -241,6 +264,7 @@
     star.setVisible(Gerrit.isSignedIn());
     labels.init(style, statusText);
     reviewers.init(style, ccText);
+    hashtags.init(style);
 
     keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
     keysNavigation.add(new KeyCommand(0, 'u', Util.C.upToChangeList()) {
@@ -350,7 +374,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,6 +424,35 @@
                 null)));
   }
 
+  private void initEditMode(ChangeInfo info) {
+    if (Gerrit.isSignedIn() && info.status() == Status.NEW) {
+      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")) {
@@ -477,22 +538,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 +572,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 +623,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 +633,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 +643,57 @@
 
   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, edit != null);
+      files.setValue(info.edit().files(), myLastReply(info),
+          emptyComment,
+          emptyComment,
+          fileTableMode);
+    } else {
+      loadDiff(b, rev, myLastReply(info), group);
+    }
     loadCommit(rev, group);
 
     if (loaded) {
@@ -600,10 +738,12 @@
       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()));
-          files.setValue(m, myLastReply, comments.get(0), drafts.get(0));
+              new PatchSet.Id(changeId, rev._number()),
+              style, editMessage, reply, edit != null);
+          files.setValue(m, myLastReply, comments.get(0),
+              drafts.get(0), fileTableMode);
         }
 
         @Override
@@ -611,7 +751,7 @@
         }
       }));
 
-    if (Gerrit.isSignedIn()) {
+    if (Gerrit.isSignedIn() && fileTableMode == FileTable.Mode.REVIEW) {
       ChangeApi.revision(changeId.get(), rev.name())
         .view("files")
         .addParameterTrue("reviewed")
@@ -671,6 +811,9 @@
   }
 
   private void loadCommit(final RevisionInfo rev, CallbackGroup group) {
+    if (rev.is_edit()) {
+      return;
+    }
     ChangeApi.revision(changeId.get(), rev.name())
       .view("commit")
       .get(group.add(new AsyncCallback<CommitInfo>() {
@@ -772,10 +915,14 @@
   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 {
@@ -791,6 +938,7 @@
     initDownloadAction(info, revision);
     initProjectLinks(info);
     initBranchLink(info);
+    initEditMode(info);
     actions.display(info, revision);
 
     star.setValue(info.starred());
@@ -800,6 +948,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);
@@ -882,7 +1035,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 +1077,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..b579b0f 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
@@ -236,6 +236,26 @@
     .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);
@@ -438,6 +458,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/>
@@ -462,6 +487,27 @@
         <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</ui:msg></div>
+        </g:Button>
+        <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>
     </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..5bb4639 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();
     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);
 
@@ -186,14 +185,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..a645cad 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
@@ -81,9 +81,12 @@
       right: -16px;
     }
     <!-- To make room for the copyableLabel from the adjacent column -->
-    .webLinkCell a:first-child {
+    .webLinkPanel a:first-child {
       margin-left:16px;
     }
+    .webLinkPanel>a {
+      margin-left:2px;
+    }
     .parentWebLink {
       margin-left:16px;
       display: block;
@@ -152,7 +155,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>
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..96cbc28 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) {
+          }
+        });
+      }
     }
   }
 
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..9bf961e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileBox.java
@@ -0,0 +1,123 @@
+//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.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.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.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 AsyncCallback<VoidResult>() {
+          @Override
+          public void onSuccess(VoidResult result) {
+            Gerrit.display(PageLinks.toChangeInEditMode(id.getParentKey()));
+            hide();
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+          }
+        });
+  }
+
+  @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/EditMessageBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java
index 700638a..5bb8148 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
@@ -55,8 +55,10 @@
     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));
@@ -78,7 +80,7 @@
   }
 
   @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>() {
@@ -91,12 +93,12 @@
   }
 
   @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..1e3dc2b 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,17 +16,21 @@
 
 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;
 import com.google.gerrit.client.diff.FileInfo;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.NativeMap;
 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 +50,11 @@
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.EventListener;
 import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.ImageResourceRenderer;
+import com.google.gwt.user.client.ui.Widget;
 import com.google.gwt.user.client.ui.impl.HyperlinkImpl;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.progress.client.ProgressBar;
@@ -55,7 +62,7 @@
 
 import java.sql.Timestamp;
 
-class FileTable extends FlowPanel {
+public class FileTable extends FlowPanel {
   static final FileTableResources R = GWT
       .create(FileTableResources.class);
 
@@ -80,20 +87,43 @@
     String deltaColumn2();
     String inserted();
     String deleted();
+    String editButton();
+    String removeButton();
   }
 
+  public static enum Mode {
+    REVIEW,
+    @SuppressWarnings("hiding")
+    EDIT
+  }
+
+  private static final String DELETE;
+  private static final String EDIT;
+  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('-', '_');
+    EDIT = DOM.createUniqueId().replace('-', '_');
+    RESTORE = DOM.createUniqueId().replace('-', '_');
     REVIEWED = DOM.createUniqueId().replace('-', '_');
     OPEN = DOM.createUniqueId().replace('-', '_');
-    init(REVIEWED, OPEN);
+    init(DELETE, EDIT, RESTORE, REVIEWED, OPEN);
   }
 
-  private static final native void init(String r, String o) /*-{
+  private static final native void init(String d, String e, 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[e] = $entry(function(e,i) {
+      @com.google.gerrit.client.change.FileTable::onEdit(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 +132,27 @@
     });
   }-*/;
 
+  private static void onEdit(NativeEvent e, int idx) {
+    MyTable t = getMyTable(e);
+    if (t != null) {
+      t.onEdit(idx);
+    }
+  }
+
+  private static void onDelete(NativeEvent e, int idx) {
+    MyTable t = getMyTable(e);
+    if (t != null) {
+      t.onDelete(idx);
+    }
+  }
+
+  private static void onRestore(NativeEvent e, int idx) {
+    MyTable t = getMyTable(e);
+    if (t != null) {
+      t.onRestore(idx);
+    }
+  }
+
   private static void onReviewed(NativeEvent e, int idx) {
     MyTable t = getMyTable(e);
     if (t != null) {
@@ -139,6 +190,10 @@
   private boolean register;
   private JsArrayString reviewed;
   private String scrollToPath;
+  private ChangeScreen2.Style style;
+  private Widget editButton;
+  private Widget replyButton;
+  private boolean editExists;
 
   @Override
   protected void onLoad() {
@@ -146,20 +201,26 @@
     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, boolean editExists) {
     this.base = base;
     this.curr = curr;
+    this.style = style;
+    this.editButton = editButton;
+    this.replyButton = replyButton;
+    this.editExists = editExists;
   }
 
   void setValue(NativeMap<FileInfo> fileMap,
       Timestamp myLastReply,
       NativeMap<JsArray<CommentInfo>> comments,
-      NativeMap<JsArray<CommentInfo>> drafts) {
+      NativeMap<JsArray<CommentInfo>> drafts,
+      Mode mode) {
     JsArray<FileInfo> list = fileMap.values();
     FileInfo.sortFileInfoByPath(list);
 
     DisplayCommand cmd = new DisplayCommand(fileMap, list,
-        myLastReply, comments, drafts);
+        myLastReply, comments, drafts, mode);
     if (cmd.execute()) {
       cmd.showProgressBar();
       Scheduler.get().scheduleIncremental(cmd);
@@ -262,6 +323,54 @@
           + curr.toString());
     }
 
+    void onEdit(int idx) {
+      final String path = list.get(idx).path();
+      final PatchSet.Id id = editExists && curr.get() != 0
+          ? new PatchSet.Id(curr.getParentKey(), 0)
+          : curr;
+      ChangeFileApi.getContent(id, path,
+          new GerritCallback<String>() {
+            @Override
+            public void onSuccess(String result) {
+              EditFileAction edit = new EditFileAction(
+                  id, result, path, style, editButton, replyButton);
+              edit.onEdit();
+            }
+          });
+    }
+
+    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,12 +461,13 @@
 
   private final class DisplayCommand implements RepeatingCommand {
     private final SafeHtmlBuilder sb = new SafeHtmlBuilder();
-    private final MyTable table;
+    private final MyTable myTable;
     private final JsArray<FileInfo> list;
     private final Timestamp myLastReply;
     private final NativeMap<JsArray<CommentInfo>> comments;
     private final NativeMap<JsArray<CommentInfo>> drafts;
     private final boolean hasUser;
+    private final Mode mode;
     private boolean attached;
     private int row;
     private double start;
@@ -371,16 +481,19 @@
         JsArray<FileInfo> list,
         Timestamp myLastReply,
         NativeMap<JsArray<CommentInfo>> comments,
-        NativeMap<JsArray<CommentInfo>> drafts) {
-      this.table = new MyTable(map, list);
+        NativeMap<JsArray<CommentInfo>> drafts,
+        Mode mode) {
+      this.myTable = new MyTable(map, list);
       this.list = list;
       this.myLastReply = myLastReply;
       this.comments = comments;
       this.drafts = drafts;
       this.hasUser = Gerrit.isSignedIn();
-      table.addStyleName(R.css().table());
+      this.mode = mode;
+      myTable.addStyleName(R.css().table());
     }
 
+    @Override
     public boolean execute() {
       boolean attachedNow = isAttached();
       if (!attached && attachedNow) {
@@ -408,9 +521,9 @@
         }
       }
       footer(sb);
-      table.resetHtml(sb);
-      table.finishDisplay();
-      setTable(table);
+      myTable.resetHtml(sb);
+      myTable.finishDisplay();
+      setTable(myTable);
       return false;
     }
 
@@ -449,7 +562,12 @@
     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().editButton()).closeTh();
+        sb.openTh().setStyleName(R.css().removeButton()).closeTh();
+      }
       sb.openTh().setStyleName(R.css().status()).closeTh();
       sb.openTh().append(Util.C.patchTableColumnName()).closeTh();
       sb.openTh()
@@ -466,9 +584,18 @@
     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 {
+        columnEdit(sb, info);
+        columnDeleteRestore(sb, info);
+      }
       columnStatus(sb, info);
-      columnPath(sb, info);
+      if (mode == Mode.REVIEW) {
+        columnPath(sb, info);
+      } else {
+        columnPathEdit(sb, info);
+      }
       columnComments(sb, info);
       columnDelta1(sb, info);
       columnDelta2(sb, info);
@@ -487,6 +614,67 @@
       sb.closeTd();
     }
 
+    private void columnEdit(SafeHtmlBuilder sb, FileInfo info) {
+      sb.openTd().setStyleName(R.css().editButton());
+      if (hasUser && isEditable(info)) {
+        if (!Patch.COMMIT_MSG.equals(info.path())) {
+          sb.openElement("button")
+            .setAttribute("title", Resources.C.editFileInline())
+            .setAttribute("onclick", EDIT + "(event," + info._row() + ")")
+            .append(new ImageResourceRenderer().render(Gerrit.RESOURCES.edit()))
+            .closeElement("button");
+        }
+      }
+      sb.closeTd();
+    }
+
+    private void columnPathEdit(SafeHtmlBuilder sb, FileInfo info) {
+      sb.openTd().setStyleName(R.css().pathColumn());
+      String path = info.path();
+      if (!Patch.COMMIT_MSG.equals(path)) {
+        sb.openAnchor()
+          .setAttribute("onclick", (isEditable(info) ? EDIT : RESTORE)
+              + "(event," + info._row() + ")");
+        int commonPrefixLen = commonPrefix(path);
+        if (commonPrefixLen > 0) {
+          sb.openSpan().setStyleName(R.css().commonPrefix())
+            .append(path.substring(0, commonPrefixLen))
+            .closeSpan();
+        }
+        sb.append(path.substring(commonPrefixLen));
+        sb.closeAnchor();
+      } else {
+        sb.append(Util.C.commitMessage());
+      }
+      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())
@@ -629,7 +817,12 @@
     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().editButton()).closeTh();
+        sb.openTh().setStyleName(R.css().editButton()).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..1e6192a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
@@ -0,0 +1,273 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.ImageResourceRenderer;
+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(
+              new ImageResourceRenderer().render(Resources.I.remove_reviewer()))
+          .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..7bc3edd
--- /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: #444;
+      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/Message.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
index e3799ca..8c23458 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,17 @@
     }
   }
 
+  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;
+    }
+    return s.substring(0, len - 10) + "..." + s.substring(s.length() - 10);
+  }
+
   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 5c8b638..2a6fec0 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;
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..165ea68 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);
   }
 
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/RelatedChangesTab.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
index e1ce99d..591acc8 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
@@ -200,6 +200,7 @@
       return false;
     }
 
+    @Override
     public boolean execute() {
       if (navList != view || !panel.isAttached()) {
         // If the user navigated away, we aren't in the DOM anymore.
@@ -311,7 +312,7 @@
         PatchSet.Id id = info.patch_set_id();
         return "#" + PageLinks.toChange(
             id.getParentKey(),
-            String.valueOf(id.get()));
+            id.getId());
       }
 
       GitwebLink gw = Gerrit.getGitwebLink();
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/RestReviewerSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestReviewerSuggestOracle.java
index b5a3023..3c544cf 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/RestReviewerSuggestOracle.java
@@ -62,6 +62,7 @@
       this.reviewer = reviewer;
     }
 
+    @Override
     public String getDisplayString() {
       if (reviewer.account() != null) {
         return FormatUtil.nameEmail(reviewer.account());
@@ -72,6 +73,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/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/Reviewers.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
index 10a140d..ae4834c 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
@@ -127,7 +127,7 @@
   }
 
   @UiHandler("openForm")
-  void onOpenForm(ClickEvent e) {
+  void onOpenForm(@SuppressWarnings("unused") ClickEvent e) {
     onOpenForm();
   }
 
@@ -139,7 +139,7 @@
   }
 
   @UiHandler("add")
-  void onAdd(ClickEvent e) {
+  void onAdd(@SuppressWarnings("unused") ClickEvent e) {
     String reviewer = suggestBox.getText();
     if (!reviewer.isEmpty()) {
       addReviewer(reviewer, false);
@@ -147,13 +147,13 @@
   }
 
   @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);
@@ -163,6 +163,7 @@
     ChangeApi.reviewers(changeId.get()).post(
         PostInput.create(reviewer, confirmed),
         new GerritCallback<PostResult>() {
+          @Override
           public void onSuccess(PostResult result) {
             nameTxtBox.setEnabled(true);
 
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..21016e4 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
@@ -105,7 +105,7 @@
   }
 
   @UiHandler("cancel")
-  void onCancel(ClickEvent e) {
+  void onCancel(@SuppressWarnings("unused") ClickEvent e) {
     input.setFocus(false);
     UIObject.setVisible(form, false);
   }
@@ -121,7 +121,7 @@
   }
 
   @UiHandler("save")
-  void onSave(ClickEvent e) {
+  void onSave(@SuppressWarnings("unused") ClickEvent e) {
     ChangeApi.topic(
         psId.getParentKey().get(),
         input.getValue().trim(),
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/file_table.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
index 4b695d2..2802f39 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
@@ -40,6 +40,7 @@
 }
 .pathColumn a {
   color: #000;
+  cursor: pointer;
 }
 .commonPrefix {
   color: #888;
@@ -85,3 +86,21 @@
   background-color: #d44;
 }
 
+.editButton button {
+  cursor: pointer;
+  padding: 0;
+  margin: 0 0 0 5px;
+  border: 0;
+  background-color: transparent;
+  white-space: nowrap;
+}
+
+.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/changes/ApprovalTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
index b41a082..bdc3142 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
@@ -254,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..7c498c2 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,6 +14,7 @@
 
 package com.google.gerrit.client.changes;
 
+import com.google.gerrit.client.changes.ChangeInfo.EditInfo;
 import com.google.gerrit.client.changes.ChangeInfo.IncludedInInfo;
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.RestApi;
@@ -33,6 +34,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 +81,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).addParameter("list", true).get(cb);
+  }
+
+  public static RestApi edit(int id) {
+    return change(id).view("edit");
+  }
+
+  public static RestApi editWithCommands(int id) {
+    return edit(id).addParameter("download-commands", true);
+  }
+
   public static void includedIn(int id, AsyncCallback<IncludedInInfo> cb) {
     call(id, "in").get(cb);
   }
@@ -103,6 +132,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 +178,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("publish_edit").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("rebase_edit").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 +213,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();
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..854dc61
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeFileApi.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.client.changes;
+
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+/**
+ * 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);
+    }
+  }
+
+  /** Get the contents of a File in a PatchSet or cange edit. */
+  public static void getContent(PatchSet.Id id, String filename,
+      AsyncCallback<String> cb) {
+    contentEditOrPs(id, filename).get(
+        new CallbackWrapper<NativeString, String>(cb) {
+            @Override
+            public void onSuccess(NativeString b64) {
+              if (b64 != null) {
+                wrapped.onSuccess(b64decode(b64.asString()));
+              }
+            }
+          });
+  }
+
+  /** Put contents into a File in a change edit. */
+  public static void putContent(PatchSet.Id id, String filename,
+      String content, AsyncCallback<VoidResult> result) {
+    contentEdit(id.getParentKey(), filename).put(content, result);
+  }
+
+  /** 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.path(filename);
+    in.restore(true);
+    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 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 path(String p) /*-{ if(p)this.path=p; }-*/;
+    final native void restore(boolean r) /*-{ if(r)this.restore=r; }-*/;
+
+    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..62c6e06 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; }-*/;
@@ -99,9 +100,14 @@
   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 +210,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; }-*/;
@@ -223,14 +261,43 @@
     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 () {
     }
   }
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 425309d..2666827 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)) {
@@ -456,6 +455,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()));
@@ -743,6 +743,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 dcea18c..bb3d3ed 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
@@ -65,7 +65,7 @@
         event.stopPropagation();
       }
     }
-  };
+  }
 
   static void focusOnClick(Element e, DisplaySide side) {
     onClick(e, side == A ? focusA : focusB);
@@ -165,10 +165,10 @@
     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);
     }
   }
 
@@ -264,8 +264,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));
   }
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..77c93a7 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
@@ -295,6 +295,7 @@
 
   Runnable toggleOpenBox(final CodeMirror cm) {
     return new Runnable() {
+      @Override
       public void run() {
         if (cm.hasActiveLine()) {
           CommentGroup w = map(cm.side()).get(
@@ -339,6 +340,7 @@
     }
 
     return new Runnable() {
+      @Override
       public void run() {
         if (cm.hasActiveLine()) {
           newDraft(cm);
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..ff99d7b 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,6 +14,7 @@
 
 package com.google.gerrit.client.diff;
 
+import com.google.gerrit.client.WebLinkInfo;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
@@ -100,6 +101,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..fa47954 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
@@ -79,6 +79,7 @@
   private SideBySide2 parent;
   private boolean header;
   private boolean headerVisible;
+  private boolean autoHideHeader;
   private boolean visibleA;
   private ChangeType changeType;
 
@@ -133,7 +134,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 +149,13 @@
     parent.resizeCodeMirror();
   }
 
+  void setAutoHideDiffHeader(boolean hide) {
+    autoHideHeader = hide;
+    if (!hide) {
+      showHeader(true);
+    }
+  }
+
   int getHeaderHeight() {
     int h = patchSetSelectBoxA.getOffsetHeight();
     if (header) {
@@ -156,10 +168,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) {
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..2793d01 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
@@ -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/Header.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
index 2b3e2be..3772b01 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
@@ -101,8 +101,7 @@
     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) {
@@ -191,7 +190,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()
@@ -243,7 +242,7 @@
   }
 
   @UiHandler("preferences")
-  void onPreferences(ClickEvent e) {
+  void onPreferences(@SuppressWarnings("unused") ClickEvent e) {
     prefsAction.show();
   }
 
@@ -282,6 +281,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/LineMapper.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
index 6c7423c..3afa208 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(this);
+    }
+
+    @Override
     public String toString() {
       return line + " " + aligned;
     }
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..4e336e1 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,13 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.WebLinkInfo;
+import com.google.gerrit.client.changes.ChangeFileApi;
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
 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.InlineHyperlink;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
@@ -25,6 +30,7 @@
 import com.google.gwt.core.client.GWT;
 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.resources.client.CssResource;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
@@ -34,8 +40,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 +85,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 +95,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 +107,62 @@
     } 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()
+        && !Patch.COMMIT_MSG.equals(path)) {
+      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() {
+    final Anchor anchor = new Anchor(
+        new ImageResourceRenderer().render(Gerrit.RESOURCES.edit()));
+    anchor.addClickHandler(new ClickHandler() {
+      boolean editing = false;
+      @Override
+      public void onClick(ClickEvent event) {
+        final PatchSet.Id id = (idActive == null) ? other.idActive : idActive;
+        editing = !editing;
+        parent.editSideB(editing);
+
+        if (editing) {
+          ChangeFileApi.getContent(id, path,
+              new GerritCallback<String>() {
+            @Override
+            public void onSuccess(String content) {
+              parent.setSideBContent(content);
+            }
+          });
+          anchor.setHTML(new ImageResourceRenderer().render(Gerrit.RESOURCES.save()));
+        } else {
+          anchor.setHTML(new ImageResourceRenderer().render(Gerrit.RESOURCES.edit()));
+          String siteBContent = parent.getSideBContent();
+          ChangeFileApi.putContent(id, path, siteBContent,
+              new GerritCallback<VoidResult>() {
+                @Override
+                public void onSuccess(VoidResult result) {
+                }
+              });
+        }
+      }
+    });
+    anchor.setTitle(PatchUtil.C.edit());
+    return anchor;
   }
 
   static void link(PatchSetSelectBox2 a, PatchSetSelectBox2 b) {
@@ -130,7 +193,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..9ea5c8d 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
@@ -90,6 +90,7 @@
   @UiField ToggleButton leftSide;
   @UiField ToggleButton emptyPane;
   @UiField ToggleButton topMenu;
+  @UiField ToggleButton autoHideDiffTableHeader;
   @UiField ToggleButton manualReview;
   @UiField ToggleButton expandAllComments;
   @UiField ToggleButton renderEntireFile;
@@ -157,6 +158,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 +194,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 +324,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,7 +347,7 @@
   }
 
   @UiHandler("mode")
-  void onMode(ChangeEvent e) {
+  void onMode(@SuppressWarnings("unused") ChangeEvent e) {
     final String m = mode.getValue(mode.getSelectedIndex());
     prefs.syntaxHighlighting(true);
     syntaxHighlighting.setValue(true, false);
@@ -380,7 +389,7 @@
   }
 
   @UiHandler("theme")
-  void onTheme(ChangeEvent e) {
+  void onTheme(@SuppressWarnings("unused") ChangeEvent e) {
     prefs.theme(Theme.valueOf(theme.getValue(theme.getSelectedIndex())));
     view.setThemeStyles(prefs.theme().isDark());
     view.operation(new Runnable() {
@@ -394,12 +403,12 @@
   }
 
   @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) {
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/SideBySide2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
index 22df01f..afcdf1f 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;
@@ -119,6 +121,7 @@
   private ScrollSynchronizer scrollSynchronizer;
   private DiffInfo diff;
   private FileSize fileSize;
+  private EditInfo edit;
   private ChunkManager chunkManager;
   private CommentManager commentManager;
   private SkipManager skipManager;
@@ -129,6 +132,8 @@
   private List<HandlerRegistration> handlers;
   private PreferencesAction prefsAction;
   private int reloadVersionId;
+  private KeyMap sbsKeyMap;
+  private boolean isEdited = false;
 
   public SideBySide2(
       PatchSet.Id base,
@@ -190,6 +195,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);
 
@@ -200,9 +215,19 @@
       @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);
+        // TODO(davido): Change edit feature doesn't support drafts atm,
+        // so we cannot use info.status().isOpen()
+        boolean renderChangeEditIcon = info.status() == Change.Status.NEW;
+        diffTable.set(prefs, list, diff, edit != null, currentPatchSet,
+            renderChangeEditIcon);
         header.setChangeInfo(info);
       }}));
 
@@ -319,7 +344,7 @@
     cm.on("cursorActivity", updateActiveLine(cm));
     cm.on("gutterClick", onGutterClick(cm));
     cm.on("focus", updateActiveLine(cm));
-    cm.addKeyMap(KeyMap.create()
+    sbsKeyMap = KeyMap.create()
         .on("A", upToChange(true))
         .on("U", upToChange(false))
         .on("[", header.navigate(Direction.PREV))
@@ -338,6 +363,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:
@@ -384,7 +410,8 @@
           public void run() {
             cm.execCommand("selectAll");
           }
-        }));
+        });
+    cm.addKeyMap(sbsKeyMap);
     if (prefs.renderEntireFile()) {
       cm.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
     }
@@ -396,6 +423,9 @@
 
       @Override
       public void handle(CodeMirror cm, LineCharacter anchor, LineCharacter head) {
+        if (isEdited) {
+          return;
+        }
         if (anchor == head
             || (anchor.getLine() == head.getLine()
              && anchor.getCh() == head.getCh())) {
@@ -514,6 +544,30 @@
     }));
   }
 
+  public void editSideB(boolean state) {
+    isEdited = state;
+    cmB.setOption("readOnly", !state);
+    JumpKeys.enable(!state);
+    if (state) {
+      removeKeyHandlerRegistrations();
+      cmB.removeKeyMap(sbsKeyMap);
+      cmB.setOption("keyMap", "default");
+      cmB.focus();
+    } else {
+      cmB.setOption("keyMap", "vim_ro");
+      cmB.addKeyMap(sbsKeyMap);
+      registerKeys();
+    }
+  }
+
+  public String getSideBContent() {
+    return cmB.getValue();
+  }
+
+  public void setSideBContent(String content) {
+    cmB.setValue(content);
+  }
+
   private void display(final CommentsCollections comments) {
     setThemeStyles(prefs.theme().isDark());
     setShowTabs(prefs.showTabs());
@@ -541,6 +595,7 @@
     }
 
     operation(new Runnable() {
+      @Override
       public void run() {
         // Estimate initial CM3 height, fixed up in onShowView.
         int height = Window.getClientHeight()
@@ -697,6 +752,10 @@
     });
   }
 
+  void setAutoHideDiffHeader(boolean hide) {
+    diffTable.setAutoHideDiffHeader(hide);
+  }
+
   private void render(DiffInfo diff) {
     header.setNoDiff(diff);
     chunkManager.render(diff);
@@ -726,6 +785,7 @@
   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
@@ -736,6 +796,7 @@
           @Override
           public void execute() {
             operation(new Runnable() {
+              @Override
               public void run() {
                 LineHandle handle = cm.getLineHandleVisualStart(
                     cm.getCursor("end").getLine());
@@ -791,6 +852,7 @@
 
   private Runnable upToChange(final boolean openReplyBox) {
     return new Runnable() {
+      @Override
       public void run() {
         CallbackGroup group = new CallbackGroup();
         commentManager.saveAllDrafts(group);
@@ -798,11 +860,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));
           }
         });
       }
@@ -821,6 +884,7 @@
 
     final DisplaySide sideSrc = cmSrc.side();
     return new Runnable() {
+      @Override
       public void run() {
         if (cmSrc.hasActiveLine()) {
           cmDst.setCursor(LineCharacter.create(lineOnOther(
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..c69bc07 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
@@ -174,7 +174,7 @@
   }
 
   @UiHandler("skipNum")
-  void onExpandAll(ClickEvent e) {
+  void onExpandAll(@SuppressWarnings("unused") ClickEvent e) {
     expandAll();
     updateSelection();
     otherBar.updateSelection();
@@ -189,13 +189,13 @@
   }
 
   @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();
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/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/editUndo.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editUndo.png
new file mode 100644
index 0000000..4790e10
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editUndo.png
Binary files differ
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..ec2b3bb 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;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/mediaFloppy.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/mediaFloppy.png
new file mode 100644
index 0000000..f1d7a19
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/mediaFloppy.png
Binary files differ
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/PatchConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
index 9ff9893..82d664d 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();
 
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..84ae02c 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
 
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..1024a98 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
@@ -564,6 +564,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/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/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..ac73dd2 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
@@ -28,10 +28,12 @@
   @Override
   public void _onRequestSuggestions(final Request req, final Callback callback) {
     RpcStatus.hide(new Runnable() {
+      @Override
       public void run() {
         SuggestUtil.SVC.suggestAccount(req.getQuery(), Boolean.TRUE,
             req.getLimit(),
             new GerritCallback<List<AccountInfo>>() {
+              @Override
               public void onSuccess(final List<AccountInfo> result) {
                 final ArrayList<AccountSuggestion> r =
                     new ArrayList<>(result.size());
@@ -52,10 +54,12 @@
       info = k;
     }
 
+    @Override
     public String getDisplayString() {
       return FormatUtil.nameEmail(FormatUtil.asInfo(info));
     }
 
+    @Override
     public String getReplacementString() {
       return FormatUtil.nameEmail(FormatUtil.asInfo(info));
     }
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/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/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..e11d11c 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
@@ -82,6 +82,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..a7928d7 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
@@ -88,9 +88,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) {
   }
 
@@ -175,7 +185,7 @@
 
   protected void populateProjects() {
     ProjectMap.match(subname,
-        new IgnoreOutdatedFilterResultsCallbackWrapper<ProjectMap>(this,
+        new IgnoreOutdatedFilterResultsCallbackWrapper<>(this,
             new GerritCallback<ProjectMap>() {
               @Override
               public void onSuccess(final ProjectMap result) {
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
index 1068c3d..be58080 100644
--- 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
@@ -28,6 +28,7 @@
   private SuggestOracle.Request request;
   private SuggestOracle.Callback callback;
   private SuggestOracle.Callback myCallback = new SuggestOracle.Callback() {
+      @Override
       public void onSuggestionsReady(SuggestOracle.Request req,
             SuggestOracle.Response response) {
           if (request == req) {
@@ -43,6 +44,7 @@
     oracle = ora;
   }
 
+  @Override
   public void requestSuggestions(SuggestOracle.Request req,
       SuggestOracle.Callback cb) {
     request = req;
@@ -50,6 +52,7 @@
     oracle.requestSuggestions(req, myCallback);
   }
 
+  @Override
   public boolean isDisplayStringHTML() {
     return oracle.isDisplayStringHTML();
   }
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
index 12506da..23c8b8f 100644
--- 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
@@ -34,9 +34,11 @@
   @Override
   protected void _onRequestSuggestions(final Request req, final Callback callback) {
     RpcStatus.hide(new Runnable() {
+      @Override
       public void run() {
         SuggestUtil.SVC.suggestChangeReviewer(changeId, req.getQuery(),
             req.getLimit(), new GerritCallback<List<ReviewerInfo>>() {
+              @Override
               public void onSuccess(final List<ReviewerInfo> result) {
                 final List<ReviewerSuggestion> r =
                     new ArrayList<>(result.size());
@@ -61,6 +63,7 @@
       this.reviewerInfo = reviewerInfo;
     }
 
+    @Override
     public String getDisplayString() {
       final AccountInfo accountInfo = reviewerInfo.getAccountInfo();
       if (accountInfo != null) {
@@ -70,6 +73,7 @@
           + Util.C.suggestedGroupLabel() + ")";
     }
 
+    @Override
     public String getReplacementString() {
       final AccountInfo accountInfo = reviewerInfo.getAccountInfo();
       if (accountInfo != null) {
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/lib/CodeMirror.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
index 941ef7a..4b8970a 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
@@ -96,7 +96,15 @@
 
   private final native void addLineClassNative(LineHandle line, String where,
       String lineClass) /*-{
-    this.addLineClass(line, where, lineClass);
+    try {
+      this.addLineClass(line, where, lineClass);
+    } catch (err) {
+      if ("TypeError: Cannot read property 'parrent' of undefinded" == err.toString()) {
+        // ignore CodeMirror bug after going to new line
+        return;
+      }
+      throw err;
+    }
   }-*/;
 
   public final void removeLineClass(int line, LineClassWhere where,
@@ -362,4 +370,8 @@
   public interface BeforeSelectionChangeHandler {
     public void handle(CodeMirror instance, LineCharacter anchor, LineCharacter head);
   }
+
+  public final native String getValue() /*-{
+    return this.getValue();
+  }-*/;
 }
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java
index 3098c43..cfb098e 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java
@@ -53,7 +53,6 @@
       Modes.I.gas(),
       Modes.I.gerrit_commit(),
       Modes.I.gfm(),
-      Modes.I.go(),
       Modes.I.groovy(),
       Modes.I.haskell(),
       Modes.I.htmlmixed(),
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..9b56c38 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
@@ -36,7 +36,6 @@
   @Source("gas/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();
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-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/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..e76759d 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
@@ -50,18 +50,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 +82,36 @@
     }
     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(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);
+
+    bind(ProxyProperties.class).toProvider(ProxyPropertiesProvider.class);
+
+    listener().toInstance(registerInParentInjectors());
+  }
+
+  private void installAuthModule() {
     switch (authConfig.getAuthType()) {
       case HTTP:
       case HTTP_LDAP:
@@ -109,28 +139,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..785b9d4 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,13 @@
 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.MoreObjects;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMultimap;
@@ -47,9 +48,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 +81,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 +224,7 @@
         try {
           rsrc = rc.parse(rsrc, id);
           if (path.isEmpty()) {
-            checkPreconditions(req, rsrc);
+            checkPreconditions(req);
           }
         } catch (ResourceNotFoundException e) {
           if (rc instanceof AcceptsCreate
@@ -254,6 +257,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 +269,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 +280,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;
             }
@@ -331,27 +345,27 @@
         }
       }
     } catch (AuthException e) {
-      replyError(req, res, status = SC_FORBIDDEN, e.getMessage(), e.caching());
+      replyError(req, res, status = SC_FORBIDDEN, e.getMessage(), e.caching(), e);
     } catch (BadRequestException e) {
-      replyError(req, res, status = SC_BAD_REQUEST, e.getMessage(), e.caching());
+      replyError(req, res, status = SC_BAD_REQUEST, e.getMessage(), 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, "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, e.getMessage(), e.caching(), e);
     } catch (PreconditionFailedException e) {
       replyError(req, res, status = SC_PRECONDITION_FAILED,
-          Objects.firstNonNull(e.getMessage(), "Precondition failed"), e.caching());
+          MoreObjects.firstNonNull(e.getMessage(), "Precondition failed"), e.caching(), e);
     } catch (ResourceNotFoundException e) {
-      replyError(req, res, status = SC_NOT_FOUND, "Not found", e.caching());
+      replyError(req, res, status = SC_NOT_FOUND, "Not found", e.caching(), e);
     } catch (UnprocessableEntityException e) {
       replyError(req, res, status = 422,
-          Objects.firstNonNull(e.getMessage(), "Unprocessable Entity"), e.caching());
+          MoreObjects.firstNonNull(e.getMessage(), "Unprocessable Entity"), e.caching(), e);
     } catch (AmbiguousViewException e) {
-      replyError(req, res, status = SC_NOT_FOUND, e.getMessage());
+      replyError(req, res, status = SC_NOT_FOUND, e.getMessage(), e);
     } catch (MalformedJsonException e) {
-      replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
+      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");
+      replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
     } catch (Exception e) {
       status = SC_INTERNAL_SERVER_ERROR;
       handleException(e, req, res);
@@ -425,7 +439,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 +872,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,21 +928,24 @@
 
     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 {
+      CacheControl c, @Nullable Throwable err) throws IOException {
     res.setStatus(statusCode);
     configureCaching(req, res, null, c);
     replyText(req, res, msg);
+    if (err != null) {
+      RequestUtil.setErrorTraceAttribute(req, err);
+    }
   }
 
   static void replyText(@Nullable HttpServletRequest req,
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..ff64c3c 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
@@ -110,9 +110,11 @@
     boolean isVisible(Account account) throws OrmException;
   }
 
+  @Override
   public void suggestAccount(final String query, final Boolean active,
       final int limit, final AsyncCallback<List<AccountInfo>> callback) {
     run(callback, new Action<List<AccountInfo>>() {
+      @Override
       public List<AccountInfo> run(final ReviewDb db) throws OrmException {
         return suggestAccount(db, query, active, limit, new VisibilityControl() {
           @Override
@@ -175,15 +177,18 @@
     }
   }
 
+  @Override
   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) {
@@ -217,6 +222,7 @@
       final String query, final int limit,
       final AsyncCallback<List<ReviewerInfo>> callback) {
     run(callback, new Action<List<ReviewerInfo>>() {
+      @Override
       public List<ReviewerInfo> run(final ReviewDb db)
           throws OrmException, Failure {
         final ChangeControl changeControl;
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..0ff12f0 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());
@@ -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..9616d25 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
@@ -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);
@@ -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/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 4a684f9..f2345fe 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;
@@ -58,7 +59,6 @@
 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 +121,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 +140,10 @@
     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;
     for (Map.Entry<Integer, Schema<ChangeData>> e
         : ChangeSchemas.ALL.entrySet()) {
       if (e.getKey() <= 3) {
@@ -146,8 +154,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(), Version.LUCENE_4_10_1);
       }
     }
     LUCENE_VERSIONS = versions.build();
@@ -174,8 +186,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 +231,7 @@
   LuceneChangeIndex(
       @GerritServerConfig Config cfg,
       SitePaths sitePaths,
-      @IndexExecutor ListeningExecutorService executor,
+      @IndexExecutor(INTERACTIVE)  ListeningExecutorService executor,
       Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       FillArgs fillArgs,
@@ -238,9 +252,9 @@
     Version luceneVersion = checkNotNull(
         LUCENE_VERSIONS.get(schema),
         "unknown Lucene version for index schema: %s", schema);
-
-    Analyzer analyzer =
-        new StandardAnalyzer(luceneVersion, CharArraySet.EMPTY_SET);
+    CustomMappingAnalyzer analyzer =
+        new CustomMappingAnalyzer(new StandardAnalyzer(CharArraySet.EMPTY_SET),
+            CUSTOM_CHAR_MAPPING);
     queryBuilder = new QueryBuilder(schema, analyzer);
 
     BooleanQuery.setMaxClauseCount(cfg.getInt("index", "defaultMaxClauseCount",
@@ -285,26 +299,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);
@@ -497,25 +491,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());
@@ -534,7 +531,7 @@
       if (legacy) {
         for (Object value : values.getValues()) {
           int t = queryBuilder.toIndexTimeInMinutes((Timestamp) value);
-          doc.add(new IntField(name, (int) t, store));
+          doc.add(new IntField(name, t, store));
         }
       } else {
         for (Object value : values.getValues()) {
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 dfe3f2d..fecd77a 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
@@ -43,7 +43,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;
@@ -154,9 +154,9 @@
   }
 
   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)
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..395b9f2 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;
@@ -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-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..6b46415 100644
--- a/gerrit-pgm/BUCK
+++ b/gerrit-pgm/BUCK
@@ -1,15 +1,7 @@
 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'])
 
 java_library(
   name = 'init-api',
@@ -24,6 +16,9 @@
     '//lib/guice:guice-assistedinject',
     '//lib/jgit:jgit',
   ],
+  provided_deps = [
+    '//lib/log:api',
+  ],
   visibility = ['PUBLIC'],
 )
 
@@ -33,65 +28,75 @@
   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,
+  name = 'init',
+  srcs = glob([SRCS + 'init/*.java']),
+  resources = glob([RSRCS + 'init/*']),
   deps = [
     ':init-api',
+    ':util',
     '//gerrit-common:server',
     '//gerrit-extension-api:api',
     '//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:h2',
+    '//lib/guice:guice',
+    '//lib/jgit:jgit',
+    '//lib/mina:sshd',
     '//lib/log:api',
   ],
   provided_deps = ['//gerrit-launcher:launcher'],
   visibility = [
-    '//gerrit-war:',
     '//gerrit-acceptance-tests/...',
+    '//gerrit-war:',
+  ],
+)
+
+java_library(
+  name = 'util',
+  srcs = glob([SRCS + 'util/*.java']),
+  deps = [
+    '//gerrit-cache-h2:cache-h2',
+    '//gerrit-common:server',
+    '//gerrit-extension-api:api',
+    '//gerrit-reviewdb:server',
+    '//gerrit-server:server',
+    '//gerrit-util-cli:cli',
+    '//lib:args4j',
+    '//lib:guava',
+    '//lib:gwtorm',
+    '//lib/commons:dbcp',
+    '//lib/guice:guice',
+    '//lib/jgit:jgit',
+    '//lib/log:api',
+    '//lib/log:log4j',
+  ],
+  visibility = [
+    '//gerrit-acceptance-tests/...',
+    '//gerrit-gwtdebug:gwtdebug',
+    '//gerrit-war:',
   ],
 )
 
 java_library(
   name = 'pgm',
   srcs = glob(
-    ['src/main/java/**/*.java'],
-    excludes = INIT_API_SRCS + INIT_BASE_SRCS
+    [SRCS + n for n in [
+      '*.java',
+      # TODO(dborowitz): Split these into separate rules.
+      'http/**/*.java',
+      'shell/**/*.java',
+    ]],
   ),
-  resources = glob(
-    ['src/main/resources/**/*'],
-    excludes = INIT_BASE_RSRCS),
+  resources = glob([RSRCS + '*']),
   deps = [
+    ':init',
     ':init-api',
-    ':init-base',
+    ':util',
     '//gerrit-cache-h2:cache-h2',
     '//gerrit-common:server',
     '//gerrit-extension-api:api',
@@ -102,14 +107,11 @@
     '//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',
@@ -120,13 +122,13 @@
     '//lib/jgit:jgit',
     '//lib/log:api',
     '//lib/log:log4j',
-    '//lib/lucene:core',
     '//lib/prolog:prolog-cafe',
   ],
   provided_deps = ['//gerrit-launcher:launcher'],
   visibility = [
     '//:',
     '//gerrit-acceptance-tests/...',
+    '//gerrit-gwtdebug:gwtdebug',
     '//tools/eclipse:classpath',
     '//Documentation:licenses.txt',
   ],
@@ -136,8 +138,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..361fe98 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,7 +32,6 @@
 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;
@@ -44,7 +44,6 @@
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
-import com.google.gerrit.server.change.MergeabilityChecksExecutorModule;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.AuthConfigModule;
 import com.google.gerrit.server.config.CanonicalWebUrlModule;
@@ -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());
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/RebuildNotedb.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNotedb.java
new file mode 100644
index 0000000..4593aef
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNotedb.java
@@ -0,0 +1,292 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.lifecycle.LifecycleManager;
+import com.google.gerrit.pgm.util.BatchGitModule;
+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.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 BatchGitModule());
+        install(new NoteDbModule());
+      }
+    });
+  }
+
+  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 f8d8630..67f4cb2 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,98 +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.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.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.AccessControlModule;
-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.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();
 
@@ -117,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;
 
@@ -127,7 +67,6 @@
   private boolean dryRun;
 
   private Injector dbInjector;
-  private Config cfg;
   private Injector sysInjector;
   private ChangeIndex index;
 
@@ -135,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();
@@ -168,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);
@@ -200,48 +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);
-        install(new AccessControlModule());
-        install(new DefaultCacheFactory.Module());
-        install(new GroupModule());
-        install(new PrologModule());
-        install(AccountByEmailCacheImpl.module());
-        install(AccountCacheImpl.module());
-        install(GroupCacheImpl.module());
-        install(GroupIncludeCacheImpl.module());
-        install(ProjectCacheImpl.module());
-        install(SectionSortCache.module());
-        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);
   }
 
@@ -254,81 +139,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();
@@ -348,11 +158,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..a553363 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,11 +18,12 @@
 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;
@@ -30,7 +31,6 @@
 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;
@@ -648,7 +648,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 bacdd2d..7459bdc 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;
@@ -133,10 +130,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 {
   }
 
@@ -166,8 +175,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;
@@ -194,7 +203,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);
@@ -228,10 +238,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;
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..88bc790 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,7 @@
 
 package com.google.gerrit.pgm.init;
 
+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/InitAuth.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
index a2037b5..0f4db71 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
@@ -16,7 +16,9 @@
 
 import static com.google.gerrit.pgm.init.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..f655b6a 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
@@ -16,6 +16,8 @@
 
 import static com.google.gerrit.pgm.init.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..7a66b3f 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
@@ -18,7 +18,9 @@
 import static com.google.gerrit.pgm.init.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..88b2d4f 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
@@ -16,7 +16,9 @@
 
 import static com.google.gerrit.pgm.init.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..9c25538 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
@@ -20,7 +20,10 @@
 import static com.google.gerrit.pgm.init.InitUtil.isAnyAddress;
 import static com.google.gerrit.pgm.init.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 4ce9a24..1dd9df2 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;
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 c737c39..c30d3f5 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..673ff6e 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
@@ -17,7 +17,9 @@
 import static com.google.gerrit.pgm.init.InitUtil.isLocal;
 import static com.google.gerrit.pgm.init.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..98fd2b5 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
@@ -18,7 +18,9 @@
 import static com.google.gerrit.pgm.init.InitUtil.die;
 import static com.google.gerrit.pgm.init.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..73b6396 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
@@ -17,6 +17,7 @@
 import static com.google.gerrit.pgm.init.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..c3111554 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
@@ -16,6 +16,8 @@
 
 import static com.google.gerrit.pgm.init.InitUtil.username;
 
+import com.google.gerrit.pgm.init.api.Section;
+
 public class MaxDbInitializer implements DatabaseConfigInitializer {
 
   @Override
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..50d71f3 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
@@ -16,6 +16,8 @@
 
 import static com.google.gerrit.pgm.init.InitUtil.username;
 
+import com.google.gerrit.pgm.init.api.Section;
+
 class MySqlInitializer implements DatabaseConfigInitializer {
 
   @Override
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..e7bf99b 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
@@ -16,6 +16,8 @@
 
 import static com.google.gerrit.pgm.init.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..4f2b802 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
@@ -16,6 +16,8 @@
 
 import static com.google.gerrit.pgm.init.InitUtil.username;
 
+import com.google.gerrit.pgm.init.api.Section;
+
 class PostgreSQLInitializer implements DatabaseConfigInitializer {
 
   @Override
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 892a8a5..c172e7b 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
@@ -22,8 +22,9 @@
 import static com.google.gerrit.pgm.init.InitUtil.saveSecure;
 import static com.google.gerrit.pgm.init.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..6abb375 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
@@ -18,7 +18,10 @@
 import static com.google.gerrit.pgm.init.InitUtil.savePublic;
 import static com.google.gerrit.pgm.init.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/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 97%
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..a376bb7 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,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.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;
@@ -50,7 +49,7 @@
     this.subsection = subsection;
   }
 
-  String get(String name) {
+  public String get(String name) {
     return flags.cfg.getString(section, subsection, name);
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java
new file mode 100644
index 0000000..11ab073
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.util;
+
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.DisabledChangeHooks;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.git.GitModule;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.CommitValidators;
+
+/** Module for batch programs that need git access. */
+public class BatchGitModule extends FactoryModule {
+  @Override
+  protected void configure() {
+    bind(ChangeHooks.class).to(DisabledChangeHooks.class);
+    DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
+    DynamicSet.setOf(binder(), CommitValidationListener.class);
+    factory(CommitValidators.Factory.class);
+    install(new GitModule());
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
new file mode 100644
index 0000000..2ee2450
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -0,0 +1,120 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.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.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.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.AccessControlModule;
+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.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.List;
+
+/**
+ * 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(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);
+    install(new AccessControlModule());
+    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/gerrit.sh b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh
similarity index 100%
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
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..575f4ac 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
@@ -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..1d7b2f7 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
@@ -41,6 +41,7 @@
       code = c;
     }
 
+    @Override
     public char getCode() {
       return code;
     }
@@ -92,6 +93,7 @@
     p.setContext(DEFAULT_CONTEXT);
     p.setManualReview(false);
     p.setHideEmptyPane(false);
+    p.setAutoHideDiffTableHeader(true);
     return p;
   }
 
@@ -156,6 +158,9 @@
   @Column(id = 20)
   protected boolean hideEmptyPane;
 
+  @Column(id = 21)
+  protected boolean autoHideDiffTableHeader;
+
   protected AccountDiffPreference() {
   }
 
@@ -183,6 +188,7 @@
     this.hideLineNumbers = p.hideLineNumbers;
     this.renderEntireFile = p.renderEntireFile;
     this.hideEmptyPane = p.hideEmptyPane;
+    this.autoHideDiffTableHeader = p.autoHideDiffTableHeader;
   }
 
   public Account.Id getAccountId() {
@@ -343,4 +349,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..d5236f8 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,6 +14,8 @@
 
 package com.google.gerrit.reviewdb.client;
 
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
+
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
 import com.google.gwtorm.client.RowVersion;
@@ -128,8 +130,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;
     }
   }
 
@@ -362,6 +420,8 @@
   @Column(id = 10)
   protected char status;
 
+  // DELETED: id = 11 (nbrPatchSets)
+
   /** The current patch set. */
   @Column(id = 12)
   protected int currentPatchSetId;
@@ -374,16 +434,8 @@
   @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;
-
-  @Column(id = 16)
-  protected boolean mergeable;
+  // DELETED: id = 15 (lastSha1MergeTested)
+  // DELETED: id = 16 (mergeable)
 
   protected Change() {
   }
@@ -397,7 +449,6 @@
     owner = ownedBy;
     dest = forBranch;
     setStatus(Status.NEW);
-    setLastSha1MergeTested(null);
   }
 
   public Change(Change other) {
@@ -414,8 +465,6 @@
     currentPatchSetId = other.currentPatchSetId;
     subject = other.subject;
     topic = other.topic;
-    mergeable = other.mergeable;
-    lastSha1MergeTested = other.lastSha1MergeTested;
   }
 
   /** Legacy 32 bit integer identity for a change. */
@@ -506,20 +555,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..2b33a1e 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;
 
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/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..11f054c 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -130,6 +130,7 @@
   srcs = PROLOG_TEST_CASE,
   deps = [
     ':server',
+    '//gerrit-common:server',
     '//lib:junit',
     '//lib/guice:guice',
     '//lib/prolog:prolog-cafe',
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..0a086f4 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;
@@ -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;
@@ -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());
     }
 
+    @Override
     public void addChangeListener(ChangeListener listener, IdentifiedUser 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<>();
@@ -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..d9d76e3 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
@@ -30,6 +30,7 @@
 import org.eclipse.jgit.lib.RefUpdate;
 
 import java.util.Map;
+import java.util.Set;
 
 /** Invokes hooks on server actions. */
 public interface ChangeHooks {
@@ -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..a97c4e6 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,7 +17,6 @@
 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;
@@ -30,6 +29,7 @@
 import org.eclipse.jgit.lib.RefUpdate;
 
 import java.util.Map;
+import java.util.Set;
 
 /** Does not invoke hooks. */
 public final class DisabledChangeHooks implements ChangeHooks {
@@ -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/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/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..c7c94f8 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);
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..19231cf 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
@@ -20,8 +20,11 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.primitives.Ints;
-import com.google.gerrit.common.errors.EmailException;
+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 +33,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 +42,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 +49,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;
@@ -247,7 +249,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 +373,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 +395,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 +403,6 @@
         }
 
         Date now = myIdent.getWhen();
-        Change change = db.get().changes().get(changeId);
         PersonIdent authorIdent =
             user().newCommitterIdent(now, myIdent.getTimeZone());
 
@@ -441,7 +438,7 @@
             .setMessage(msg)
             .setCopyLabels(true)
             .setValidatePolicy(RECEIVE_COMMITS)
-            .setDraft(originalPS.isDraft())
+            .setDraft(ps.isDraft())
             .insert();
 
         return change.getId();
@@ -453,19 +450,14 @@
     }
   }
 
-  public void deleteDraftChange(PatchSet.Id patchSetId)
+  public void deleteDraftChange(Change change)
       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) {
+    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);
@@ -508,19 +500,52 @@
     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));
   }
 
+  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
+    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();
+  }
+
   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);
   }
 
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..bb16710 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
@@ -33,6 +33,7 @@
 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;
@@ -70,6 +71,7 @@
     private final Provider<String> canonicalUrl;
     private final AccountCache accountCache;
     private final GroupBackend groupBackend;
+    private final Boolean disableReverseDnsLookup;
 
     @Inject
     public GenericFactory(
@@ -77,6 +79,7 @@
         AuthConfig authConfig,
         @AnonymousCowardName String anonymousCowardName,
         @CanonicalWebUrl Provider<String> canonicalUrl,
+        @DisableReverseDnsLookup Boolean disableReverseDnsLookup,
         AccountCache accountCache,
         GroupBackend groupBackend) {
       this.capabilityControlFactory = capabilityControlFactory;
@@ -85,6 +88,7 @@
       this.canonicalUrl = canonicalUrl;
       this.accountCache = accountCache;
       this.groupBackend = groupBackend;
+      this.disableReverseDnsLookup = disableReverseDnsLookup;
     }
 
     public IdentifiedUser create(final Account.Id id) {
@@ -92,22 +96,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,
+          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,
+          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,
+          anonymousCowardName, canonicalUrl, accountCache, groupBackend,
+          disableReverseDnsLookup, Providers.of(remotePeer), null, id, caller);
     }
   }
 
@@ -125,6 +129,7 @@
     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;
@@ -137,6 +142,7 @@
         final @CanonicalWebUrl Provider<String> canonicalUrl,
         final AccountCache accountCache,
         final GroupBackend groupBackend,
+        final @DisableReverseDnsLookup Boolean disableReverseDnsLookup,
 
         final @RemotePeer Provider<SocketAddress> remotePeerProvider,
         final Provider<ReviewDb> dbProvider) {
@@ -146,21 +152,22 @@
       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,
+          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,
+          anonymousCowardName, canonicalUrl, accountCache, groupBackend,
+          disableReverseDnsLookup, remotePeerProvider, dbProvider, id, caller);
     }
   }
 
@@ -177,6 +184,7 @@
   private final AuthConfig authConfig;
   private final GroupBackend groupBackend;
   private final String anonymousCowardName;
+  private final Boolean disableReverseDnsLookup;
 
   @Nullable
   private final Provider<SocketAddress> remotePeerProvider;
@@ -194,6 +202,7 @@
   private Collection<AccountProjectWatch> notificationFilters;
   private CurrentUser realUser;
 
+
   private IdentifiedUser(
       CapabilityControl.Factory capabilityControlFactory,
       final AuthConfig authConfig,
@@ -201,6 +210,7 @@
       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,
@@ -211,6 +221,7 @@
     this.groupBackend = groupBackend;
     this.authConfig = authConfig;
     this.anonymousCowardName = anonymousCowardName;
+    this.disableReverseDnsLookup = disableReverseDnsLookup;
     this.remotePeerProvider = remotePeerProvider;
     this.dbProvider = dbProvider;
     this.accountId = id;
@@ -383,7 +394,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 +455,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..96298eb 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,130 @@
 
 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.WebLinkInfo;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.webui.BranchWebLink;
+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.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<ProjectWebLink> projectLinks;
+  private final DynamicSet<BranchWebLink> branchLinks;
 
+  @Inject
   public WebLinks(DynamicSet<PatchSetWebLink> patchSetLinks,
-      DynamicSet<ProjectWebLink> projectLinks) {
+      DynamicSet<FileWebLink> fileLinks,
+      DynamicSet<ProjectWebLink> projectLinks,
+      DynamicSet<BranchWebLink> branchLinks) {
     this.patchSetLinks = patchSetLinks;
+    this.fileLinks = fileLinks;
     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 String project,
+      final String commit) {
+    return filterLinks(patchSetLinks, new Function<WebLink, WebLinkInfo>() {
+
+      @Override
+      public WebLinkInfo apply(WebLink webLink) {
+        return ((PatchSetWebLink)webLink).getPathSetWebLink(project, 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.
+   * @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/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..6394a5b 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);
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/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..53dec1b 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,6 +15,8 @@
 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;
@@ -30,14 +32,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;
@@ -74,13 +74,14 @@
   private final AccountByEmailCache byEmailCache;
   private final AccountInfo.Loader.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) {
+      @Assisted String username, AuditService auditService) {
     this.db = db;
     this.currentUser = currentUser;
     this.groupsCollection = groupsCollection;
@@ -89,6 +90,7 @@
     this.byEmailCache = byEmailCache;
     this.infoLoader = infoLoader;
     this.username = username;
+    this.auditService = auditService;
   }
 
   @Override
@@ -169,9 +171,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));
     }
 
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/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/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..7169e91 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
@@ -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/PerformCreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
index a54a97b..bd533aa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
@@ -14,19 +14,17 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupById;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
 import com.google.gerrit.reviewdb.client.AccountGroupName;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -52,12 +50,13 @@
   private final PersonIdent serverIdent;
   private final GroupCache groupCache;
   private final CreateGroupArgs createGroupArgs;
+  private final AuditService auditService;
 
   @Inject
   PerformCreateGroup(ReviewDb db, AccountCache accountCache,
       GroupIncludeCache groupIncludeCache, IdentifiedUser currentUser,
       @GerritPersonIdent PersonIdent serverIdent, GroupCache groupCache,
-      @Assisted CreateGroupArgs createGroupArgs) {
+      @Assisted CreateGroupArgs createGroupArgs, AuditService auditService) {
     this.db = db;
     this.accountCache = accountCache;
     this.groupIncludeCache = groupIncludeCache;
@@ -65,6 +64,7 @@
     this.serverIdent = serverIdent;
     this.groupCache = groupCache;
     this.createGroupArgs = createGroupArgs;
+    this.auditService = auditService;
   }
 
   /**
@@ -127,18 +127,13 @@
   private void addMembers(final AccountGroup.Id groupId,
       final Collection<? extends Account.Id> members) throws OrmException {
     List<AccountGroupMember> memberships = new ArrayList<>();
-    List<AccountGroupMemberAudit> membershipsAudit = new ArrayList<>();
     for (Account.Id accountId : members) {
       final AccountGroupMember membership =
           new AccountGroupMember(new AccountGroupMember.Key(accountId, groupId));
       memberships.add(membership);
-
-      final AccountGroupMemberAudit audit = new AccountGroupMemberAudit(
-          membership, currentUser.getAccountId(), TimeUtil.nowTs());
-      membershipsAudit.add(audit);
     }
     db.accountGroupMembers().insert(memberships);
-    db.accountGroupMembersAudit().insert(membershipsAudit);
+    auditService.dispatchAddAccountsToGroup(currentUser.getAccountId(), memberships);
 
     for (Account.Id accountId : members) {
       accountCache.evict(accountId);
@@ -148,18 +143,13 @@
   private void addGroups(final AccountGroup.Id groupId,
       final Collection<? extends AccountGroup.UUID> groups) throws OrmException {
     List<AccountGroupById> includeList = new ArrayList<>();
-    List<AccountGroupByIdAud> includesAudit = new ArrayList<>();
     for (AccountGroup.UUID includeUUID : groups) {
       final AccountGroupById groupInclude =
         new AccountGroupById(new AccountGroupById.Key(groupId, includeUUID));
       includeList.add(groupInclude);
-
-      final AccountGroupByIdAud audit = new AccountGroupByIdAud(
-          groupInclude, currentUser.getAccountId(), TimeUtil.nowTs());
-      includesAudit.add(audit);
     }
     db.accountGroupById().insert(includeList);
-    db.accountGroupByIdAud().insert(includesAudit);
+    auditService.dispatchAddGroupsToGroup(currentUser.getAccountId(), includeList);
 
     for (AccountGroup.UUID uuid : groups) {
       groupIncludeCache.evictMemberIn(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 3903050..c6b6282 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/SetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
index 9b971e4..d922faf 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
@@ -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/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 5f90d34..462300c 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,7 @@
 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.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 +30,11 @@
 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.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 +45,7 @@
 
 import java.io.IOException;
 import java.util.EnumSet;
+import java.util.Set;
 
 class ChangeApiImpl extends ChangeApi.NotImplemented implements ChangeApi {
   interface Factory {
@@ -53,8 +59,12 @@
   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;
 
   @Inject
   ChangeApiImpl(Changes changeApi,
@@ -63,8 +73,12 @@
       Abandon abandon,
       Revert revert,
       Restore restore,
-      Provider<PostReviewers> postReviewers,
+      GetTopic getTopic,
+      PutTopic putTopic,
+      PostReviewers postReviewers,
       Provider<ChangeJson> changeJson,
+      PostHashtags postHashtags,
+      GetHashtags getHashtags,
       @Assisted ChangeResource change) {
     this.changeApi = changeApi;
     this.revert = revert;
@@ -72,8 +86,12 @@
     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.change = change;
   }
 
@@ -145,6 +163,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 +188,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);
     }
@@ -180,4 +214,22 @@
   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);
+    }
+  }
 }
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..d45b851 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,6 +60,42 @@
 @Singleton class Helper {
   static final String LDAP_UUID = "ldap:";
 
+  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>> groupsByInclude;
   private final Config config;
   private final String server;
@@ -68,6 +106,7 @@
   private final String authentication;
   private volatile LdapSchema ldapSchema;
   private final String readTimeOutMillis;
+  private final Map<String, String> connectionPoolConfig;
 
   @Inject
   Helper(@GerritServerConfig final Config config,
@@ -76,10 +115,11 @@
     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 =
@@ -89,6 +129,7 @@
       readTimeOutMillis = null;
     }
     this.groupsByInclude = groupsByInclude;
+    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<>();
 
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..22c60b4 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
@@ -106,6 +106,22 @@
     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/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..7d823c3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.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.change;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.common.io.ByteStreams;
+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.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.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", metaVar = "LIST")
+    boolean list;
+
+    @Option(name = "--download-commands", metaVar = "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 {
+      try {
+          editModifier.modifyFile(rsrc.getChangeEdit(), rsrc.getPath(),
+              ByteStreams.toByteArray(input.content.getInputStream()));
+      } 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();
+      }
+    }
+  }
+}
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..28e7a34 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,16 @@
 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.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.validators.ValidationException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -64,8 +69,10 @@
   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 RefControl refControl;
   private final Change change;
@@ -77,6 +84,7 @@
   private Set<Account.Id> reviewers;
   private Set<Account.Id> extraCC;
   private Map<String, Short> approvals;
+  private Set<String> hashtags;
   private boolean runHooks;
   private boolean sendMail;
 
@@ -88,8 +96,10 @@
       ChangeHooks hooks,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
-      MergeabilityChecker mergeabilityChecker,
+      ChangeIndexer indexer,
       CreateChangeSender.Factory createChangeSenderFactory,
+      HashtagsUtil hashtagsUtil,
+      AccountCache accountCache,
       @Assisted RefControl refControl,
       @Assisted Change change,
       @Assisted RevCommit commit) {
@@ -99,14 +109,17 @@
     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.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;
 
@@ -145,6 +158,11 @@
     return this;
   }
 
+  public ChangeInserter setHashtags(Set<String> hashtags) {
+    this.hashtags = hashtags;
+    return this;
+  }
+
   public ChangeInserter setRunHooks(boolean runHooks) {
     this.runHooks = runHooks;
     return this;
@@ -183,7 +201,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,11 +209,20 @@
     } finally {
       db.rollback();
     }
+
     update.commit();
-    CheckedFuture<?, IOException> f = mergeabilityChecker.newCheck()
-        .addChange(change)
-        .reindex()
-        .runAsync();
+
+    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();
@@ -221,6 +248,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..55f65d5 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
@@ -31,8 +31,9 @@
 import static com.google.gerrit.extensions.common.ListChangesOption.WEB_LINKS;
 
 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;
@@ -62,6 +63,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;
@@ -77,6 +79,7 @@
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.account.AccountInfo;
 import com.google.gerrit.server.extensions.webui.UiActions;
@@ -86,7 +89,7 @@
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.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.gwtorm.server.OrmException;
@@ -124,9 +127,10 @@
   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 AccountInfo.Loader accountLoader;
 
@@ -137,7 +141,6 @@
       Provider<CurrentUser> user,
       AnonymousUser au,
       IdentifiedUser.GenericFactory uf,
-      ProjectControl.GenericFactory pcf,
       ChangeData.Factory cdf,
       PatchSetInfoFactory psi,
       FileInfoJson fileInfoJson,
@@ -146,8 +149,9 @@
       DynamicMap<DownloadCommand> downloadCommands,
       DynamicMap<RestView<ChangeResource>> changeViews,
       Revisions revisions,
-      Provider<WebLinks> webLinks,
-      ChangeMessagesUtil cmUtil) {
+      WebLinks webLinks,
+      ChangeMessagesUtil cmUtil,
+      PatchLineCommentsUtil plcUtil) {
     this.db = db;
     this.labelNormalizer = ln;
     this.userProvider = user;
@@ -163,6 +167,7 @@
     this.revisions = revisions;
     this.webLinks = webLinks;
     this.cmUtil = cmUtil;
+    this.plcUtil = plcUtil;
     options = EnumSet.noneOf(ListChangesOption.class);
   }
 
@@ -239,7 +244,7 @@
   }
 
   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());
@@ -266,8 +271,12 @@
     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;
@@ -295,7 +304,7 @@
           || 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);
@@ -307,7 +316,7 @@
     if (has(ALL_REVISIONS)
         || has(CURRENT_REVISION)
         || limitToPsId.isPresent()) {
-      out.revisions = revisions(ctl, cd, limitToPsId, out.project, src);
+      out.revisions = revisions(ctl, cd, out.project, src);
       if (out.revisions != null) {
         for (Map.Entry<String, RevisionInfo> entry : out.revisions.entrySet()) {
           if (entry.getValue().isCurrent) {
@@ -326,31 +335,30 @@
           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();
   }
 
@@ -375,7 +383,7 @@
   private Map<String, LabelInfo> labelsForOpenChange(ChangeControl ctl,
       ChangeData cd, LabelTypes labelTypes, boolean standard, boolean detailed)
       throws OrmException {
-    Map<String, LabelInfo> labels = initLabels(ctl, cd, labelTypes, standard);
+    Map<String, LabelInfo> labels = initLabels(cd, labelTypes, standard);
     if (detailed) {
       setAllApprovals(ctl, cd, labels);
     }
@@ -400,11 +408,11 @@
     return labels;
   }
 
-  private Map<String, LabelInfo> initLabels(ChangeControl ctl, ChangeData cd,
+  private Map<String, LabelInfo> 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)) {
+    for (SubmitRecord rec : submitRecords(cd)) {
       if (rec.labels == null) {
         continue;
       }
@@ -436,8 +444,7 @@
   }
 
   private void setLabelScores(LabelType type,
-      LabelInfo label, short score, Account.Id accountId)
-      throws OrmException {
+      LabelInfo label, short score, Account.Id accountId) {
     if (label.approved != null || label.rejected != null) {
       return;
     }
@@ -605,7 +612,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 +677,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,7 +686,8 @@
         continue;
       }
       for (ApprovalInfo ai : label.all) {
-        if (ctl.canRemoveReviewer(ai._id, Objects.firstNonNull(ai.value, 0))) {
+        if (ctl.canRemoveReviewer(ai._id,
+            MoreObjects.firstNonNull(ai.value, 0))) {
           removable.add(ai._id);
         } else {
           fixed.add(ai._id);
@@ -739,8 +747,7 @@
   }
 
   private Map<String, RevisionInfo> revisions(ChangeControl ctl, ChangeData cd,
-      Optional<PatchSet.Id> limitToPsId, String project,
-      Map<PatchSet.Id, PatchSet> map) throws OrmException {
+      String project, Map<PatchSet.Id, PatchSet> map) throws OrmException {
     Map<String, RevisionInfo> res = Maps.newLinkedHashMap();
     for (PatchSet in : map.values()) {
       if ((has(ALL_REVISIONS)
@@ -786,7 +793,7 @@
     out.isCurrent = in.getId().equals(cd.change().currentPatchSetId());
     out._number = in.getId().get();
     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 {
@@ -821,19 +828,16 @@
         && 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);
-      }
+      FluentIterable<WebLinkInfo> links =
+          webLinks.getPatchSetLinks(project, in.getRevision().get());
+      out.webLinks = links.isEmpty() ? null : links.toList();
     }
     return out;
   }
@@ -856,7 +860,7 @@
     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 +884,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();
     }
@@ -915,6 +927,7 @@
     public String project;
     public String branch;
     public String topic;
+    public Collection<String> hashtags;
     public String changeId;
     public String subject;
     public Change.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/ChangesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
index b9b92cb..3c61b86 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.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AcceptsPost;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -24,7 +23,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.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -34,33 +33,32 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-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;
 
   @Inject
   ChangesCollection(
-      Provider<ReviewDb> dbProvider,
       Provider<CurrentUser> user,
       ChangeControl.GenericFactory changeControlFactory,
       Provider<QueryChanges> queryFactory,
       DynamicMap<RestView<ChangeResource>> views,
+      ChangeUtil changeUtil,
       CreateChange createChange) {
-    this.db = dbProvider;
     this.user = user;
     this.changeControlFactory = changeControlFactory;
     this.queryFactory = queryFactory;
     this.views = views;
+    this.changeUtil = changeUtil;
     this.createChange = createChange;
   }
 
@@ -77,7 +75,7 @@
   @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.size() != 1) {
       throw new ResourceNotFoundException(id);
     }
@@ -97,43 +95,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/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
index d034a10..889ae1f 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
@@ -91,7 +91,7 @@
       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..b97cf05 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,7 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.gerrit.common.errors.EmailException;
+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 +27,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 +39,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;
@@ -98,7 +99,7 @@
   public Change.Id cherryPick(final PatchSet.Id patchSetId,
       final String message, final String destinationBranch,
       final RefControl refControl) throws NoSuchChangeException,
-      EmailException, OrmException, MissingObjectException,
+      OrmException, MissingObjectException,
       IncorrectObjectTypeException, IOException,
       InvalidChangeOperationException, MergeException {
 
@@ -112,7 +113,8 @@
           "Cherry Pick: Destination branch cannot be null or empty");
     }
 
-    Project.NameKey project = db.get().changes().get(changeId).getProject();
+    Change change = db.get().changes().get(changeId);
+    Project.NameKey project = change.getProject();
     IdentifiedUser identifiedUser = (IdentifiedUser) currentUser.get();
     final Repository git;
     try {
@@ -153,14 +155,12 @@
           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);
         if (!idList.isEmpty()) {
@@ -183,13 +183,13 @@
         } else if (destChanges.size() == 1) {
           // The change key exists on the destination branch. The cherry pick
           // will be added as a new patch set.
-          return insertPatchSet(git, revWalk, destChanges.get(0), patchSetId,
-              cherryPickCommit, refControl, identifiedUser);
+          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);
+              cherryPickCommit, refControl, identifiedUser, change.getTopic());
         }
       } finally {
         revWalk.release();
@@ -200,8 +200,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 =
@@ -222,12 +222,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..4c5d848 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
@@ -17,8 +17,8 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.common.changes.Side;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.CommentRange;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.server.account.AccountInfo;
 
 import java.sql.Timestamp;
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..3f527c9 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,14 @@
 package com.google.gerrit.server.change;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+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,6 +32,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.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
@@ -43,7 +47,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 +83,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 +93,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 +103,14 @@
     this.commitValidatorsFactory = commitValidatorsFactory;
     this.changeInserterFactory = changeInserterFactory;
     this.json = json;
+    this.changeUtil = changeUtil;
   }
 
   @Override
   public Response<ChangeJson.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 +154,32 @@
     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.isEmpty()) {
+            throw new InvalidChangeOperationException(
+                "Base change not found: " + input.baseChange);
+          }
+          Change change = Iterables.getOnlyElement(changes);
+          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 +190,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 =
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..a863552 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;
@@ -73,7 +71,7 @@
     }
 
     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..fc6dbb9 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
@@ -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..3942898 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,6 +15,7 @@
 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.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
@@ -27,10 +28,8 @@
 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..9a817e3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+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.git.GitRepositoryManager;
+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 FileContentUtil {
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  FileContentUtil(GitRepositoryManager repoManager) {
+    this.repoManager = repoManager;
+  }
+
+  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();
+    }
+  }
+}
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/GetContent.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
index bfc1df9..5a73e86 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,26 @@
 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.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;
 
   @Inject
-  GetContent(GitRepositoryManager repoManager) {
-    this.repoManager = repoManager;
+  GetContent(FileContentUtil fileContentUtil) {
+    this.fileContentUtil = fileContentUtil;
   }
 
   @Override
   public BinaryResult apply(FileResource rsrc)
       throws ResourceNotFoundException, IOException {
-    return apply(rsrc.getRevision().getControl().getProject().getNameKey(),
+    return fileContentUtil.getContent(
+        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();
-    }
-  }
 }
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..bcda2b0 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,15 @@
 
 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.Lists;
 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.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;
@@ -34,8 +37,11 @@
 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,6 +59,7 @@
 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;
 
@@ -60,6 +67,7 @@
   private final ProjectCache projectCache;
   private final PatchScriptFactory.Factory patchScriptFactoryFactory;
   private final Revisions revisions;
+  private final WebLinks webLinks;
 
   @Option(name = "--base", metaVar = "REVISION")
   String base;
@@ -76,20 +84,23 @@
   @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;
+      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 +111,7 @@
       PatchScriptFactory psf = patchScriptFactoryFactory.create(
           resource.getRevision().getControl(),
           resource.getPatchKey().getFileName(),
-          basePatchSet,
+          basePatchSet != null ? basePatchSet.getId() : null,
           resource.getPatchKey().getParentKey(),
           prefs);
       psf.setLoadHistory(false);
@@ -139,9 +150,18 @@
       Result result = new Result();
       if (ps.getDisplayMethodA() != DisplayMethod.NONE) {
         result.metaA = new FileMeta();
-        result.metaA.name = Objects.firstNonNull(ps.getOldName(), ps.getNewName());
+        result.metaA.name = MoreObjects.firstNonNull(ps.getOldName(),
+            ps.getNewName());
         setContentType(result.metaA, state, ps.getFileModeA(), ps.getMimeTypeA());
         result.metaA.lines = ps.getA().size();
+
+        // TODO referring to the parent commit by refs/changes/12/60012/1^1
+        // will likely not work for inline edits
+        String rev = basePatchSet != null
+            ? basePatchSet.getRefName()
+            : resource.getRevision().getPatchSet().getRefName() + "^1";
+        result.metaA.webLinks =
+            getFileWebLinks(state.getProject(), rev, result.metaA.name);
       }
 
       if (ps.getDisplayMethodB() != DisplayMethod.NONE) {
@@ -149,6 +169,11 @@
         result.metaB.name = ps.getNewName();
         setContentType(result.metaB, state, ps.getFileModeB(), ps.getMimeTypeB());
         result.metaB.lines = ps.getB().size();
+        String rev = resource.getRevision().getEdit().isPresent()
+            ? resource.getRevision().getEdit().get().getRefName()
+            : resource.getRevision().getPatchSet().getRefName();
+        result.metaB.webLinks =
+            getFileWebLinks(state.getProject(), rev, result.metaB.name);
       }
 
       if (intraline) {
@@ -178,6 +203,13 @@
     }
   }
 
+  private List<WebLinkInfo> getFileWebLinks(Project project, String rev,
+      String file) {
+    FluentIterable<WebLinkInfo> links =
+        webLinks.getFileLinks(project.getName(), rev, file);
+    return links.isEmpty() ? null : links.toList();
+  }
+
   static class Result {
     FileMeta metaA;
     FileMeta metaB;
@@ -191,6 +223,7 @@
     String name;
     String contentType;
     Integer lines;
+    List<WebLinkInfo> webLinks;
   }
 
   private void setContentType(FileMeta meta, ProjectState project,
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..077ec6f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
@@ -21,12 +21,12 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.common.CommitInfo;
-import com.google.gerrit.extensions.common.GitPerson;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetAncestor;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CommonConverters;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectControl;
@@ -39,7 +39,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 +49,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 +91,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()) {
@@ -129,9 +127,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 +141,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 +156,10 @@
         r.put(p.getId(), p);
       }
     }
+
+    if (rsrc.getEdit().isPresent()) {
+      r.put(rsrc.getPatchSet().getId(), rsrc.getPatchSet());
+    }
     return r;
   }
 
@@ -272,15 +272,6 @@
     return r;
   }
 
-  private static GitPerson toGitPerson(PersonIdent id) {
-    GitPerson p = new GitPerson();
-    p.name = id.getName();
-    p.email = id.getEmailAddress();
-    p.date = new Timestamp(id.getWhen().getTime());
-    p.tz = id.getTimeZoneOffset();
-    return p;
-  }
-
   public static class RelatedInfo {
     public List<ChangeAndCommit> changes;
   }
@@ -309,7 +300,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/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..9c4372b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/HashtagsUtil.java
@@ -0,0 +1,140 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.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;
+
+@Singleton
+public class HashtagsUtil {
+  private static final CharMatcher LEADER = WHITESPACE.or(CharMatcher.is('#'));
+
+  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.toLowerCase();
+  }
+
+  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/ListComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java
index f4d7b49..7896403 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
@@ -26,13 +26,10 @@
 
 @Singleton
 class ListComments extends ListDrafts {
-  private final PatchLineCommentsUtil plcUtil;
-
   @Inject
   ListComments(Provider<ReviewDb> db, AccountInfo.Loader.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..b3c36fd 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,6 +22,7 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.account.AccountInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -36,20 +37,21 @@
 @Singleton
 class ListDrafts implements RestReadView<RevisionResource> {
   protected final Provider<ReviewDb> db;
+  protected final PatchLineCommentsUtil plcUtil;
   private final AccountInfo.Loader.Factory accountLoaderFactory;
 
   @Inject
-  ListDrafts(Provider<ReviewDb> db, AccountInfo.Loader.Factory alf) {
+  ListDrafts(Provider<ReviewDb> db, AccountInfo.Loader.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() {
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..31b2aa0
--- /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, null, null, null));
+  }
+}
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..0f20e27 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,7 @@
 
 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.SubmitType;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -22,43 +22,32 @@
 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);
@@ -73,34 +62,29 @@
       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;
   }
 
   @Override
@@ -117,30 +101,47 @@
       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);
       }
 
       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..fc13786 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;
@@ -44,15 +45,18 @@
     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);
     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);
@@ -99,6 +103,14 @@
     get(FILE_KIND, "content").to(GetContent.class);
     get(FILE_KIND, "diff").to(GetDiff.class);
 
+    child(CHANGE_KIND, "edit").to(ChangeEdits.class);
+    delete(CHANGE_KIND, "edit").to(DeleteChangeEdit.class);
+    child(CHANGE_KIND, "publish_edit").to(PublishChangeEdit.class);
+    child(CHANGE_KIND, "rebase_edit").to(RebaseChangeEdit.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);
+
     install(new FactoryModule() {
       @Override
       protected void configure() {
@@ -107,6 +119,8 @@
         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..bc2a46e 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;
@@ -120,7 +123,7 @@
       PatchSetInfoFactory patchSetInfoFactory,
       GitReferenceUpdated gitRefUpdated,
       CommitValidators.Factory commitValidatorsFactory,
-      MergeabilityChecker mergeabilityChecker,
+      ChangeIndexer indexer,
       ReplacePatchSetSender.Factory replacePatchSetFactory,
       @Assisted Repository git,
       @Assisted RevWalk revWalk,
@@ -139,7 +142,7 @@
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.gitRefUpdated = gitRefUpdated;
     this.commitValidatorsFactory = commitValidatorsFactory;
-    this.mergeabilityChecker = mergeabilityChecker;
+    this.indexer = indexer;
     this.replacePatchSetFactory = replacePatchSetFactory;
 
     this.git = git;
@@ -177,8 +180,7 @@
     return this;
   }
 
-  public PatchSetInserter setMessage(ChangeMessage changeMessage)
-      throws OrmException {
+  public PatchSetInserter setMessage(ChangeMessage changeMessage) {
     this.changeMessage = changeMessage;
     return this;
   }
@@ -267,7 +269,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);
@@ -315,10 +316,7 @@
     } finally {
       db.rollback();
     }
-    mergeabilityChecker.newCheck()
-        .addChange(updatedChange)
-        .reindex()
-        .run();
+    indexer.index(db, c);
     if (runHooks) {
       hooks.doPatchsetCreatedHook(updatedChange, patchSet, db);
     }
@@ -356,7 +354,7 @@
     }
   }
 
-  private void validate() throws InvalidChangeOperationException {
+  private void validate() throws InvalidChangeOperationException, IOException {
     CommitValidators cv =
         commitValidatorsFactory.create(ctl.getRefControl(), sshInfo, git);
 
@@ -372,7 +370,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 48b8855..5a223f7 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
@@ -150,7 +150,7 @@
   }
 
   private PostResult putAccount(ReviewerResource rsrc) throws OrmException,
-      EmailException, IOException {
+      IOException {
     Account.Id id = rsrc.getUser().getAccountId();
     ChangeControl control = rsrc.getControl().forUser(
         identifiedUserFactory.create(id));
@@ -161,7 +161,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())) {
@@ -222,7 +222,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;
@@ -260,8 +260,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..428f48fe 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;
@@ -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..295189b 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
@@ -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..1e0fb2a 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
@@ -30,6 +30,7 @@
 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..75b80e7 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,6 +15,7 @@
 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.restapi.AuthException;
@@ -32,7 +33,6 @@
 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..804af4e 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
@@ -29,8 +29,8 @@
 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.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;
@@ -67,8 +67,7 @@
     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,8 +79,7 @@
     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));
@@ -109,8 +107,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;
         }
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..e458145
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+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.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(dbProvider.get().accounts().all());
+              }
+            });
+  }
+
+  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..0d90cc2 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,6 +27,7 @@
 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;
@@ -59,7 +60,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 +104,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 +121,7 @@
       Provider<ReviewDb> dbProvider,
       GitRepositoryManager repoManager,
       IdentifiedUser.GenericFactory userFactory,
+      ChangeData.Factory changeDataFactory,
       ChangeUpdate.Factory updateFactory,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
@@ -131,6 +135,7 @@
     this.dbProvider = dbProvider;
     this.repoManager = repoManager;
     this.userFactory = userFactory;
+    this.changeDataFactory = changeDataFactory;
     this.updateFactory = updateFactory;
     this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
@@ -139,10 +144,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));
   }
@@ -374,10 +379,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..ff54e09 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,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.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
@@ -57,7 +57,8 @@
 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;
@@ -72,11 +73,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",
@@ -95,7 +102,8 @@
       Provider<CurrentUser> currentUser,
       Provider<ReviewDb> dbProvider,
       @GerritServerConfig Config cfg,
-      GroupBackend groupBackend) {
+      GroupBackend groupBackend,
+      ReviewerSuggestionCache reviewerSuggestionCache) {
     this.accountLoaderFactory = accountLoaderFactory;
     this.accountControlFactory = accountControlFactory;
     this.accountCache = accountCache;
@@ -104,12 +112,19 @@
     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);
     }
 
@@ -134,7 +149,12 @@
     }
 
     VisibilityControl visibilityControl = getVisibility(rsrc);
-    List<AccountInfo> suggestedAccounts = suggestAccount(visibilityControl);
+    List<AccountInfo> suggestedAccounts;
+    if (useFullTextSearch) {
+      suggestedAccounts = suggestAccountFullTextSearch(visibilityControl);
+    } else {
+      suggestedAccounts = suggestAccount(visibilityControl);
+    }
     accountLoaderFactory.create(true).fill(suggestedAccounts);
 
     List<SuggestedReviewerInfo> reviewer = Lists.newArrayList();
@@ -220,6 +240,42 @@
     return Lists.newArrayList(r.values());
   }
 
+  private List<AccountInfo> suggestAccountFullTextSearch(
+      VisibilityControl visibilityControl) throws OrmException {
+    String str = query.toLowerCase();
+    LinkedHashMap<Account.Id, AccountInfo> accountMap = Maps.newLinkedHashMap();
+    List<Account> fullNameMatches = Lists.newArrayListWithCapacity(fullTextMaxMatches);
+    List<Account> emailMatches = Lists.newArrayListWithCapacity(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, new AccountInfo(a.getId()), visibilityControl);
+      if (accountMap.size() >= limit) {
+        break;
+      }
+    }
+    if (accountMap.size() < limit) {
+      for (Account a : emailMatches) {
+        addSuggestion(accountMap, a, new AccountInfo(a.getId()), visibilityControl);
+        if (accountMap.size() >= limit) {
+          break;
+        }
+      }
+    }
+    return Lists.newArrayList(accountMap.values());
+  }
+
   private void addSuggestion(Map<Account.Id, AccountInfo> map, Account account,
       AccountInfo info, VisibilityControl visibilityControl)
       throws OrmException {
@@ -287,7 +343,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..fd7ecad 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,13 +14,7 @@
 
 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;
@@ -32,18 +26,14 @@
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.account.AccountInfo;
 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;
 
@@ -86,46 +76,15 @@
     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);
     for (SubmitRecord r : records) {
@@ -135,11 +94,6 @@
     return out;
   }
 
-  private static List<Term> eval(SubmitRuleEvaluator evaluator)
-      throws RuleEvalException {
-    return evaluator.evaluate();
-  }
-
   static class Record {
     SubmitRecord.Status status;
     String 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..d3869ad 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;
@@ -34,7 +35,6 @@
 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 +95,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 +106,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;
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/CapabilityConstants.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java
index 289173b..4c93ce0 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
@@ -30,6 +30,7 @@
   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..c54e193 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;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InstallPlugins.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/DisableReverseDnsLookup.java
similarity index 74%
copy from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InstallPlugins.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/config/DisableReverseDnsLookup.java
index 73db6f5..04712f9 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InstallPlugins.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/DisableReverseDnsLookup.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,14 +12,15 @@
 // 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.config;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
 import com.google.inject.BindingAnnotation;
 
 import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
 
+@Retention(RUNTIME)
 @BindingAnnotation
-@Retention(RetentionPolicy.RUNTIME)
-public @interface InstallPlugins {
+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..02ade49 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,8 @@
 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.FileWebLink;
 import com.google.gerrit.extensions.webui.PatchSetWebLink;
 import com.google.gerrit.extensions.webui.ProjectWebLink;
 import com.google.gerrit.extensions.webui.TopMenu;
@@ -45,8 +47,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 +71,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 +89,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;
@@ -126,6 +129,7 @@
 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());
@@ -232,7 +237,8 @@
         .in(SINGLETON);
     bind(FromAddressGenerator.class).toProvider(
         FromAddressGeneratorProvider.class).in(SINGLETON);
-    bind(WebLinks.class).toProvider(WebLinksProvider.class).in(SINGLETON);
+    bind(Boolean.class).annotatedWith(DisableReverseDnsLookup.class)
+        .toProvider(DisableReverseDnsLookupProvider.class).in(SINGLETON);
 
     bind(PatchSetInfoFactory.class);
     bind(IdentifiedUser.GenericFactory.class).in(SINGLETON);
@@ -262,14 +268,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 +286,9 @@
     DynamicMap.mapOf(binder(), DownloadCommand.class);
     DynamicMap.mapOf(binder(), ProjectConfigEntry.class);
     DynamicSet.setOf(binder(), PatchSetWebLink.class);
+    DynamicSet.setOf(binder(), FileWebLink.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/EncryptedContactStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java
index 8c1fdb6..e3ec745 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;
@@ -38,6 +38,8 @@
 import org.bouncycastle.openpgp.PGPPublicKeyRing;
 import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
 import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder;
+import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -49,7 +51,6 @@
 import java.io.OutputStream;
 import java.net.URL;
 import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
 import java.security.SecureRandom;
 import java.sql.Timestamp;
 import java.text.SimpleDateFormat;
@@ -94,11 +95,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 (PGPException | IOException e) {
       throw new ProvisionException("PGP encryption not available", e);
     }
   }
@@ -132,6 +129,7 @@
     return null;
   }
 
+  @Override
   public void store(final Account account, final ContactInformation info)
       throws ContactInformationStoreException {
     try {
@@ -155,29 +153,26 @@
       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 e) {
       log.error("Cannot store encrypted contact information", e);
       throw new ContactInformationStoreException(e);
     }
   }
 
-  @SuppressWarnings("deprecation")
-  private final PGPEncryptedDataGenerator cpk()
-      throws NoSuchProviderException, PGPException {
+  private final PGPEncryptedDataGenerator cpk() {
+    final BcPGPDataEncryptorBuilder builder =
+        new BcPGPDataEncryptorBuilder(PGPEncryptedData.CAST5)
+            .setSecureRandom(prng);
     PGPEncryptedDataGenerator cpk =
-        new PGPEncryptedDataGenerator(PGPEncryptedData.CAST5, true, prng, "BC");
-    cpk.addMethod(dest);
+        new PGPEncryptedDataGenerator(builder, true);
+    final BcPublicKeyKeyEncryptionMethodGenerator methodGenerator =
+        new BcPublicKeyKeyEncryptionMethodGenerator(dest);
+    cpk.addMethod(methodGenerator);
     return cpk;
   }
 
   private byte[] encrypt(final String name, final Date date,
-      final byte[] rawText) throws NoSuchProviderException, PGPException,
+      final byte[] rawText) throws PGPException,
       IOException {
     final byte[] zText = compress(name, date, rawText);
 
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..40a0bef
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditJson.java
@@ -0,0 +1,139 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.Lists;
+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.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.parents = Lists.newArrayListWithCapacity(1);
+    commit.author = CommonConverters.toGitPerson(editCommit.getAuthorIdent());
+    commit.committer = CommonConverters.toGitPerson(
+        editCommit.getCommitterIdent());
+    commit.subject = editCommit.getShortMessage();
+    commit.message = editCommit.getFullMessage();
+
+    CommitInfo i = new CommitInfo();
+    i.commit = editCommit.getParent(0).toObjectId().getName();
+    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..ad59093
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -0,0 +1,413 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.gerrit.server.edit.ChangeEditUtil.editRefName;
+import static com.google.gerrit.server.edit.ChangeEditUtil.editRefPrefix;
+
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.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.Constants;
+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.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);
+      ObjectInserter inserter = repo.newObjectInserter();
+      try {
+        RevCommit base = rw.parseCommit(ObjectId.fromString(
+            ps.getRevision().get()));
+        RevCommit changeBase = base.getParent(0);
+        ObjectId commit = createCommit(me, inserter, base, changeBase, base.getTree());
+        inserter.flush();
+        String editRefName = editRefName(me.getAccountId(), change.getId(),
+            ps.getId());
+        return update(repo, me, editRefName, rw, ObjectId.zeroId(), commit);
+      } finally {
+        rw.release();
+        inserter.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 implemented");
+        }
+        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 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, byte[] 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, byte[] 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();
+        if (prevEdit.getParentCount() == 0) {
+          throw new InvalidChangeOperationException(
+              "Modify edit against root commit not implemented");
+        }
+
+        RevCommit base = prevEdit.getParent(0);
+        base = rw.parseCommit(base);
+        ObjectId newTree = writeNewTree(op, rw, inserter,
+            prevEdit, reader, file, content, base);
+        if (ObjectId.equals(newTree, prevEdit.getTree())) {
+          throw new InvalidChangeOperationException("no changes were made");
+        }
+
+        ObjectId commit = createCommit(me, inserter, prevEdit, base, newTree);
+        inserter.flush();
+        return update(repo, me, refName, rw, prevEdit, commit);
+      } finally {
+        rw.release();
+        inserter.release();
+        reader.release();
+      }
+    } finally {
+      repo.close();
+    }
+  }
+
+  private ObjectId createCommit(IdentifiedUser me, ObjectInserter inserter,
+      RevCommit prevEdit, RevCommit base, ObjectId tree) throws IOException {
+    CommitBuilder builder = new CommitBuilder();
+    builder.setTreeId(tree);
+    builder.setParentIds(base);
+    builder.setAuthor(prevEdit.getAuthorIdent());
+    builder.setCommitter(getCommitterIdent(me));
+    builder.setMessage(prevEdit.getFullMessage());
+    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.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, byte[] content, RevCommit base)
+      throws IOException, InvalidChangeOperationException {
+    DirCache newTree = createTree(reader, prevEdit);
+    editTree(
+        op,
+        rw,
+        base,
+        newTree.editor(),
+        ins,
+        fileName,
+        content);
+    return newTree.writeTree(ins);
+  }
+
+  private static void editTree(TreeOperation op, RevWalk rw, RevCommit base,
+      DirCacheEditor dce, ObjectInserter ins, String path, byte[] content)
+      throws IOException, InvalidChangeOperationException {
+    switch (op) {
+      case DELETE_ENTRY:
+        dce.add(new DeletePath(path));
+        break;
+      case CHANGE_ENTRY:
+      case RESTORE_ENTRY:
+        dce.add(getPathEdit(op, rw, base, path, ins, content));
+        break;
+    }
+    dce.finish();
+  }
+
+  private static PathEdit getPathEdit(TreeOperation op, RevWalk rw,
+      RevCommit base, String path, ObjectInserter ins, byte[] content)
+      throws IOException, InvalidChangeOperationException {
+    final ObjectId oid = op == TreeOperation.CHANGE_ENTRY
+        ? ins.insert(Constants.OBJ_BLOB, content)
+        : getObjectIdForRestoreOperation(rw, base, path);
+    return new PathEdit(path) {
+      @Override
+      public void apply(DirCacheEntry ent) {
+        ent.setFileMode(FileMode.REGULAR_FILE);
+        ent.setObjectId(oid);
+      }
+    };
+  }
+
+  private static ObjectId getObjectIdForRestoreOperation(RevWalk rw,
+      RevCommit base, String path)
+      throws IOException, InvalidChangeOperationException {
+    TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), path,
+        base.getTree().getId());
+    // If the file does not exist in the base commit, try to restore it
+    // from the base's parent commit.
+    if (tw == null && base.getParentCount() == 1) {
+      tw = TreeWalk.forPath(rw.getObjectReader(), path,
+          rw.parseCommit(base.getParent(0)).getTree().getId());
+    }
+    if (tw == null) {
+      throw new InvalidChangeOperationException(String.format(
+          "cannot restore path %s: missing in base revision %s",
+          path, base.abbreviate(8).name()));
+    }
+    return tw.getObjectId(0);
+  }
+
+  private static DirCache createTree(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()
+        .getId());
+    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..2c83121
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -0,0 +1,294 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.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())) {
+      throw new ResourceConflictException("identical tree");
+    }
+    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)
+        .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(parent.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/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..77ba756 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,15 @@
 
 package com.google.gerrit.server.events;
 
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+
 public abstract class ChangeEvent {
+  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/HashtagsChangedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/HashtagsChangedEvent.java
new file mode 100644
index 0000000..91c14b1
--- /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-edited";
+  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/RefOperationReceivedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefOperationReceivedEvent.java
new file mode 100644
index 0000000..da3a215
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefOperationReceivedEvent.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 RefOperationReceivedEvent 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..f421dcb 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"),
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-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeConflictException.java
similarity index 62%
copy from gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/git/MergeConflictException.java
index cd07320..02bc8dc 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeConflictException.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.server.git;
 
-public class GerritUiOptions {
-  private final boolean headless;
-
-  public GerritUiOptions(boolean headless) {
-    this.headless = headless;
-  }
-
-  public boolean enableDefaultUi() {
-    return !headless;
+/** Indicates that the commit cannot be merged without conflicts. */
+public class MergeConflictException extends Exception {
+  private static final long serialVersionUID = 1L;
+  public MergeConflictException(String msg) {
+    super(msg, null);
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InstallPlugins.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java
similarity index 60%
copy from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InstallPlugins.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java
index 73db6f5..109fa76 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InstallPlugins.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIdenticalTreeException.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,14 +12,12 @@
 // 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.git;
 
-import com.google.inject.BindingAnnotation;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-@BindingAnnotation
-@Retention(RetentionPolicy.RUNTIME)
-public @interface InstallPlugins {
+/** Indicates that the commit is already contained in destination banch. */
+public class MergeIdenticalTreeException extends Exception {
+  private static final long serialVersionUID = 1L;
+  public MergeIdenticalTreeException(String msg) {
+    super(msg, null);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index e7ff0220..7d603b1 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,7 +918,8 @@
   }
 
   private void setMerged(Change c, ChangeMessage msg)
-      throws OrmException, IOException, NoSuchChangeException {
+      throws OrmException, IOException {
+    logDebug("Setting change {} merged", c.getId());
     ChangeUpdate update = null;
     try {
       db.changes().beginTransaction(c.getId());
@@ -861,7 +950,7 @@
               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);
+          logError("Cannot run hook for submitted patch set " + c.getId(), ex);
         }
       }
     } finally {
@@ -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..9eb9945 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
@@ -52,6 +52,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;
@@ -89,6 +90,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 +180,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 +189,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 +200,7 @@
       mergeCommit.setMessage(commitMsg);
       return rw.parseCommit(commit(inserter, mergeCommit));
     } else {
-      return null;
+      throw new MergeConflictException("merge conflict");
     }
   }
 
@@ -605,9 +613,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 +634,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..4d5db88 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));
@@ -524,7 +522,7 @@
             n.addEmail(ref);
           } else {
             error(new ValidationError(PROJECT_CONFIG,
-                "group \"" + ref.getName() + "\" not in " + GROUP_LIST));
+                "group \"" + ref.getName() + "\" not in " + GroupList.FILE_NAME));
           }
         } else if (dst.startsWith("user ")) {
           error(new ValidationError(PROJECT_CONFIG, dst + " not supported"));
@@ -620,7 +618,7 @@
         ref = rule.getGroup();
         groupsByName.put(ref.getName(), ref);
         error(new ValidationError(PROJECT_CONFIG,
-            "group \"" + ref.getName() + "\" not in " + GROUP_LIST));
+            "group \"" + ref.getName() + "\" not in " + GroupList.FILE_NAME));
       }
 
       rule.setGroup(ref);
@@ -641,7 +639,7 @@
         valueText);
   }
 
-  private void loadLabelSections(Config rc) throws IOException {
+  private void loadLabelSections(Config rc) {
     Map<String, String> lowerNames = Maps.newHashMapWithExpectedSize(2);
     labelSections = Maps.newLinkedHashMap();
     for (String name : rc.getSubsections(LABEL)) {
@@ -673,7 +671,7 @@
         continue;
       }
 
-      String functionName = Objects.firstNonNull(
+      String functionName = MoreObjects.firstNonNull(
           rc.getString(LABEL, name, KEY_FUNCTION), "MaxWithBlock");
       if (LABEL_FUNCTIONS.contains(functionName)) {
         label.setFunctionName(functionName);
@@ -767,31 +765,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 +799,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 +815,7 @@
     saveContributorAgreements(rc, keepGroups);
     saveAccessSections(rc, keepGroups);
     saveNotifySections(rc, keepGroups);
-    groupsByUUID.keySet().retainAll(keepGroups);
+    groupList.retainUUIDs(keepGroups);
     saveLabelSections(rc);
     savePluginSections(rc);
 
@@ -1102,30 +1088,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 +1101,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 fc1ddfc..b7c7679 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git;
 
 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 +34,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 +42,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 +52,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;
@@ -71,7 +75,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;
@@ -79,7 +82,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;
@@ -97,7 +99,9 @@
 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;
@@ -110,7 +114,6 @@
 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;
@@ -296,7 +299,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;
@@ -309,6 +311,7 @@
   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 =
@@ -325,6 +328,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();
@@ -360,12 +364,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,
@@ -375,7 +377,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;
@@ -404,7 +407,6 @@
     this.changeUpdateExector = changeUpdateExector;
     this.requestScopePropagator = requestScopePropagator;
     this.indexer = indexer;
-    this.mergeabilityChecker = mergeabilityChecker;
     this.sshInfo = sshInfo;
     this.allProjectsName = allProjectsName;
     this.receiveConfig = config;
@@ -415,17 +417,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);
@@ -587,63 +591,54 @@
 
     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:
                 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
@@ -660,15 +655,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) {
@@ -991,7 +993,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 {
@@ -1095,6 +1098,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;
@@ -1108,7 +1113,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);
     }
@@ -1123,7 +1129,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);
@@ -1136,10 +1142,26 @@
       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;
+      this.notesMigration = notesMigration;
     }
 
     boolean isDraft() {
@@ -1154,6 +1176,10 @@
       return new MailRecipients(reviewer, cc);
     }
 
+    Set<String> getHashtags() {
+      return hashtags;
+    }
+
     Map<String, Short> getLabels() {
       return labels;
     }
@@ -1213,7 +1239,7 @@
       return;
     }
 
-    magicBranch = new MagicBranchInput(cmd, labelTypes);
+    magicBranch = new MagicBranchInput(cmd, labelTypes, notesMigration);
     magicBranch.reviewer.addAll(reviewersFromCommandLine);
     magicBranch.cc.addAll(ccFromCommandLine);
 
@@ -1280,6 +1306,23 @@
     }
 
     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());
@@ -1300,6 +1343,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
@@ -1308,7 +1363,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
@@ -1431,6 +1485,7 @@
 
       List<ChangeLookup> pending = Lists.newArrayList();
       final Set<Change.Key> newChangeIds = new HashSet<>();
+      final int maxBatchChanges = receiveConfig.maxBatchChanges;
       for (;;) {
         final RevCommit c = walk.next();
         if (c == null) {
@@ -1448,6 +1503,13 @@
           return Collections.emptyList();
         }
 
+        // 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());
         final List<String> idList = c.getFooterLines(CHANGE_ID);
         if (idList.isEmpty()) {
@@ -1464,6 +1526,13 @@
 
         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);
+          return Collections.emptyList();
+        }
       }
 
       for (ChangeLookup p : pending) {
@@ -1472,7 +1541,7 @@
           return Collections.emptyList();
         }
 
-        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
@@ -1555,12 +1624,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);
     }
   }
 
@@ -1622,6 +1691,7 @@
       if (magicBranch != null) {
         recipients.add(magicBranch.getMailRecipients());
         approvals = magicBranch.getLabels();
+        ins.setHashtags(magicBranch.getHashtags());
       }
       recipients.add(getRecipientsFromFooters(accountResolver, ps, footerLines));
       recipients.remove(me);
@@ -1980,14 +2050,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();
+        Set<String> hashtags = magicBranch.getHashtags();
+        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());
@@ -2011,7 +2090,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));
@@ -2039,7 +2118,6 @@
                   } else {
                     change.setStatus(Change.Status.NEW);
                   }
-                  change.setLastSha1MergeTested(null);
                   change.setCurrentPatchSet(info);
 
                   final List<String> idList = newCommit.getFooterLines(CHANGE_ID);
@@ -2076,10 +2154,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
@@ -2125,18 +2200,25 @@
   }
 
   private List<Ref> refs(Change.Id changeId) {
+    return refsByChange().get(changeId);
+  }
+
+  private ListMultimap<Change.Id, Ref> refsByChange() {
     if (refsByChange == null) {
       int estRefsPerChange = 4;
       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);
+        if (ref.getObjectId() != null) {
+          PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
+          if (psId != null) {
+            refsByChange.put(psId.getParentKey(), ref);
+          }
         }
       }
     }
-    return refsByChange.get(changeId);
+    return refsByChange;
   }
 
   static boolean parentsEqual(RevCommit a, RevCommit b) {
@@ -2256,7 +2338,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;
@@ -2268,7 +2350,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());
@@ -2278,40 +2361,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;
           }
@@ -2328,18 +2418,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) {
@@ -2385,23 +2469,21 @@
     return change.getKey();
   }
 
-  private SetMultimap<ObjectId, Ref> changeRefsById() throws IOException {
+  private SetMultimap<ObjectId, Ref> changeRefsById() {
     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);
-        }
+      for (Ref r : refsByChange().values()) {
+        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/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..6d37ae0 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
@@ -25,6 +25,7 @@
   final boolean checkMagicRefs;
   final boolean checkReferencedObjectsAreReachable;
   final boolean allowDrafts;
+  final int maxBatchChanges;
 
   @Inject
   ReceiveConfig(@GerritServerConfig Config config) {
@@ -37,5 +38,6 @@
     allowDrafts = config.getBoolean(
         "change", null, "allowDrafts",
         true);
+    maxBatchChanges = config.getInt("receive", "maxBatchChanges", 0);
   }
 }
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 5a1e50d..2e41894 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
@@ -318,6 +318,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 3ff36ab..d34d1e3 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,22 +134,24 @@
 
   private CodeReviewCommit writeCherryPickCommit(CodeReviewCommit mergeTip,
       CodeReviewCommit n) throws IOException, OrmException,
-      NoSuchChangeException {
+      NoSuchChangeException, MergeConflictException,
+      MergeIdenticalTreeException {
 
     args.rw.parseBody(n);
 
     final PatchSetApproval submitAudit = args.mergeUtil.getSubmitter(n);
 
     IdentifiedUser cherryPickUser;
+    PersonIdent serverNow = args.serverIdent.get();
     PersonIdent cherryPickCommitterIdent;
     if (submitAudit != null) {
       cherryPickUser =
           args.identifiedUserFactory.create(submitAudit.getAccountId());
       cherryPickCommitterIdent = cherryPickUser.newCommitterIdent(
-          submitAudit.getGranted(), args.serverIdent.get().getTimeZone());
+          serverNow.getWhen(), serverNow.getTimeZone());
     } else {
       cherryPickUser = args.identifiedUserFactory.create(n.change().getOwner());
-      cherryPickCommitterIdent = args.serverIdent.get();
+      cherryPickCommitterIdent = serverNow;
     }
 
     final String cherryPickCmtMsg = args.mergeUtil.createCherryPickCommitMessage(n);
@@ -156,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);
@@ -234,4 +235,4 @@
     return args.mergeUtil.canCherryPick(args.mergeSorter, args.repo,
         mergeTip, args.rw, toMerge);
   }
-}
+}
\ No newline at end of file
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/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..4336e12 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
@@ -24,7 +24,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;
@@ -93,7 +92,8 @@
   }
 
   public List<CommitValidationMessage> validateForReceiveCommits(
-      CommitReceivedEvent receiveEvent) throws CommitValidationException {
+      CommitReceivedEvent receiveEvent, NoteMap rejectCommits)
+      throws CommitValidationException {
 
     List<CommitValidationListener> validators = new LinkedList<>();
 
@@ -102,7 +102,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 +110,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 +136,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()) {
@@ -396,7 +396,7 @@
   public static class SignedOffByValidator implements CommitValidationListener {
     private final RefControl refControl;
 
-    public SignedOffByValidator(RefControl refControl, String canonicalWebUrl) {
+    public SignedOffByValidator(RefControl refControl) {
       this.refControl = refControl;
     }
 
@@ -522,24 +522,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..c33ecfc
--- /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.RefOperationReceivedEvent;
+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(RefOperationReceivedEvent refEvent)
+      throws ValidationException;
+}
\ No newline at end of file
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..f265d3f
--- /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.RefOperationReceivedEvent;
+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 RefOperationReceivedEvent 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 RefOperationReceivedEvent();
+    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,
+      RefOperationReceivedEvent 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..24bb4e3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
@@ -27,14 +28,12 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupById;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.account.GroupIncludeCache;
 import com.google.gerrit.server.group.AddIncludedGroups.Input;
 import com.google.gerrit.server.group.GroupJson.GroupInfo;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -75,15 +74,17 @@
   private final GroupIncludeCache groupIncludeCache;
   private final Provider<ReviewDb> db;
   private final GroupJson json;
+  private final AuditService auditService;
 
   @Inject
   public AddIncludedGroups(GroupsCollection groupsCollection,
-      GroupIncludeCache groupIncludeCache,
-      Provider<ReviewDb> db, GroupJson json) {
+      GroupIncludeCache groupIncludeCache, Provider<ReviewDb> db,
+      GroupJson json, AuditService auditService) {
     this.groupsCollection = groupsCollection;
     this.groupIncludeCache = groupIncludeCache;
     this.db = db;
     this.json = json;
+    this.auditService = auditService;
   }
 
   @Override
@@ -98,13 +99,12 @@
 
     GroupControl control = resource.getControl();
     Map<AccountGroup.UUID, AccountGroupById> newIncludedGroups = Maps.newHashMap();
-    List<AccountGroupByIdAud> newIncludedGroupsAudits = Lists.newLinkedList();
     List<GroupInfo> result = Lists.newLinkedList();
     Account.Id me = ((IdentifiedUser) control.getCurrentUser()).getAccountId();
 
     for (String includedGroup : input.groups) {
       GroupDescription.Basic d = groupsCollection.parse(includedGroup);
-      if (!control.canAddGroup(d.getGroupUUID())) {
+      if (!control.canAddGroup()) {
         throw new AuthException(String.format("Cannot add group: %s",
             d.getName()));
       }
@@ -117,15 +117,13 @@
         if (agi == null) {
           agi = new AccountGroupById(agiKey);
           newIncludedGroups.put(d.getGroupUUID(), agi);
-          newIncludedGroupsAudits.add(
-              new AccountGroupByIdAud(agi, me, TimeUtil.nowTs()));
         }
       }
       result.add(json.format(d));
     }
 
     if (!newIncludedGroups.isEmpty()) {
-      db.get().accountGroupByIdAud().insert(newIncludedGroupsAudits);
+      auditService.dispatchAddGroupsToGroup(me, newIncludedGroups.values());
       db.get().accountGroupById().insert(newIncludedGroups.values());
       for (AccountGroupById agi : newIncludedGroups.values()) {
         groupIncludeCache.evictMemberIn(agi.getIncludeUUID());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
index df58c8f..6002088 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,7 @@
 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.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -25,7 +26,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.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
@@ -39,7 +39,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;
@@ -82,6 +81,7 @@
   private final AccountCache accountCache;
   private final AccountInfo.Loader.Factory infoFactory;
   private final Provider<ReviewDb> db;
+  private final AuditService auditService;
 
   @Inject
   AddMembers(AccountManager accountManager,
@@ -90,8 +90,10 @@
       AccountResolver accountResolver,
       AccountCache accountCache,
       AccountInfo.Loader.Factory infoFactory,
-      Provider<ReviewDb> db) {
+      Provider<ReviewDb> db,
+      AuditService auditService) {
     this.accountManager = accountManager;
+    this.auditService = auditService;
     this.authType = authConfig.getAuthType();
     this.accounts = accounts;
     this.accountResolver = accountResolver;
@@ -112,7 +114,6 @@
 
     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);
@@ -124,7 +125,7 @@
             "Account Inactive: %s", nameOrEmail));
       }
 
-      if (!control.canAddMember(a.getId())) {
+      if (!control.canAddMember()) {
         throw new AuthException("Cannot add member: " + a.getFullName());
       }
 
@@ -135,14 +136,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..afc0d84
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java
@@ -0,0 +1,201 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.inject.Inject;
+import com.google.inject.Provider;
+
+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 Provider<ReviewDb> db;
+  private final AccountCache accountCache;
+  private final GroupCache groupCache;
+  private final UniversalGroupBackend groupBackend;
+
+  @Inject
+  public DbGroupMemberAuditListener(Provider<ReviewDb> db,
+      AccountCache accountCache, GroupCache groupCache,
+      UniversalGroupBackend groupBackend) {
+    this.db = db;
+    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 {
+      db.get().accountGroupMembersAudit().insert(auditInserts);
+    } 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();
+    ReviewDb reviewDB = db.get();
+    try {
+      for (AccountGroupMember m : removed) {
+        AccountGroupMemberAudit audit = null;
+        for (AccountGroupMemberAudit a : reviewDB.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);
+        }
+      }
+      reviewDB.accountGroupMembersAudit().update(auditUpdates);
+      reviewDB.accountGroupMembersAudit().insert(auditInserts);
+    } 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 {
+      db.get().accountGroupByIdAud().insert(includesAudit);
+    } 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 {
+      for (final AccountGroupById g : removed) {
+        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);
+    } 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..977c0aa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -26,14 +27,12 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupById;
-import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.account.GroupIncludeCache;
 import com.google.gerrit.server.group.AddIncludedGroups.Input;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -48,16 +47,17 @@
   private final GroupIncludeCache groupIncludeCache;
   private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> self;
+  private final AuditService auditService;
 
   @Inject
   DeleteIncludedGroups(GroupsCollection groupsCollection,
-      GroupIncludeCache groupIncludeCache,
-      Provider<ReviewDb> db,
-      Provider<CurrentUser> self) {
+      GroupIncludeCache groupIncludeCache, Provider<ReviewDb> db,
+      Provider<CurrentUser> self, AuditService auditService) {
     this.groupsCollection = groupsCollection;
     this.groupIncludeCache = groupIncludeCache;
     this.db = db;
     this.self = self;
+    this.auditService = auditService;
   }
 
   @Override
@@ -76,7 +76,7 @@
 
     for (final String includedGroup : input.groups) {
       GroupDescription.Basic d = groupsCollection.parse(includedGroup);
-      if (!control.canRemoveGroup(d.getGroupUUID())) {
+      if (!control.canRemoveGroup()) {
         throw new AuthException(String.format("Cannot delete group: %s",
             d.getName()));
       }
@@ -109,27 +109,9 @@
     return groups;
   }
 
-  private void writeAudits(final List<AccountGroupById> toBeRemoved)
-      throws OrmException {
+  private void writeAudits(final List<AccountGroupById> toRemoved) {
     final Account.Id me = ((IdentifiedUser) self.get()).getAccountId();
-    final List<AccountGroupByIdAud> auditUpdates = Lists.newLinkedList();
-    for (final AccountGroupById g : toBeRemoved) {
-      AccountGroupByIdAud audit = null;
-      for (AccountGroupByIdAud a : db.get()
-          .accountGroupByIdAud().byGroupInclude(g.getGroupId(),
-              g.getIncludeUUID())) {
-        if (a.isActive()) {
-          audit = a;
-          break;
-        }
-      }
-
-      if (audit != null) {
-        audit.removed(me, TimeUtil.nowTs());
-        auditUpdates.add(audit);
-      }
-    }
-    db.get().accountGroupByIdAud().update(auditUpdates);
+    auditService.dispatchDeleteGroupsFromGroup(me, toRemoved);
   }
 
   @Singleton
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
index 654ad88..3047994 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -24,7 +25,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -32,7 +32,6 @@
 import com.google.gerrit.server.account.AccountsCollection;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.group.AddMembers.Input;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -47,15 +46,18 @@
   private final AccountCache accountCache;
   private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> self;
+  private final AuditService auditService;
 
   @Inject
   DeleteMembers(AccountsCollection accounts,
       AccountCache accountCache, Provider<ReviewDb> db,
-      Provider<CurrentUser> self) {
+      Provider<CurrentUser> self,
+      AuditService auditService) {
     this.accounts = accounts;
     this.accountCache = accountCache;
     this.db = db;
     this.self = self;
+    this.auditService = auditService;
   }
 
   @Override
@@ -75,7 +77,7 @@
     for (final String nameOrEmail : input.members) {
       Account a = accounts.parse(nameOrEmail).getAccount();
 
-      if (!control.canRemoveMember(a.getId())) {
+      if (!control.canRemoveMember()) {
         throw new AuthException("Cannot delete member: " + a.getFullName());
       }
 
@@ -94,32 +96,9 @@
     return Response.none();
   }
 
-  private void writeAudits(final List<AccountGroupMember> toBeRemoved)
-      throws OrmException {
+  private void writeAudits(final List<AccountGroupMember> toRemove) {
     final Account.Id me = ((IdentifiedUser) self.get()).getAccountId();
-    final List<AccountGroupMemberAudit> auditUpdates = Lists.newLinkedList();
-    final List<AccountGroupMemberAudit> auditInserts = Lists.newLinkedList();
-    for (final AccountGroupMember m : toBeRemoved) {
-      AccountGroupMemberAudit audit = null;
-      for (AccountGroupMemberAudit a : db.get().accountGroupMembersAudit()
-          .byGroupAccount(m.getAccountGroupId(), m.getAccountId())) {
-        if (a.isActive()) {
-          audit = a;
-          break;
-        }
-      }
-
-      if (audit != null) {
-        audit.removed(me, TimeUtil.nowTs());
-        auditUpdates.add(audit);
-      } else {
-        audit = new AccountGroupMemberAudit(m, me, TimeUtil.nowTs());
-        audit.removedLegacy();
-        auditInserts.add(audit);
-      }
-    }
-    db.get().accountGroupMembersAudit().update(auditUpdates);
-    db.get().accountGroupMembersAudit().insert(auditInserts);
+    auditService.dispatchDeleteAccountsFromGroup(me, toRemove);
   }
 
   private Map<Account.Id, AccountGroupMember> getMembers(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/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..ad16a7e 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
@@ -72,12 +72,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..2e2959c 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;
@@ -27,9 +30,9 @@
 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 +82,7 @@
         @Override
         public String get(ChangeData input, FillArgs args)
             throws OrmException {
-          return ChangeStatusPredicate.VALUES.get(
+          return ChangeStatusPredicate.canonicalize(
               input.change().getStatus());
         }
       };
@@ -136,7 +139,7 @@
         @Override
         public String get(ChangeData input, FillArgs args)
             throws OrmException {
-          return Objects.firstNonNull(input.change().getTopic(), "");
+          return MoreObjects.firstNonNull(input.change().getTopic(), "");
         }
       };
 
@@ -225,6 +228,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 +430,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 +441,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 a710a10..eb74928 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
@@ -39,23 +39,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
    *
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 437f559..e235379 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;
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..4b7850a 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
@@ -118,7 +118,7 @@
         ChangeField.COMMENT,
         ChangeField.CHANGE,
         ChangeField.APPROVAL,
-        ChangeField.MERGEABLE);
+        ChangeField.LEGACY_MERGEABLE);
 
   // For upgrade to Lucene 4.6.0 index format only.
   static final Schema<ChangeData> V6 = release(V5.getFields().values());
@@ -145,7 +145,7 @@
         ChangeField.COMMENT,
         ChangeField.CHANGE,
         ChangeField.APPROVAL,
-        ChangeField.MERGEABLE);
+        ChangeField.LEGACY_MERGEABLE);
 
   @SuppressWarnings("deprecation")
   static final Schema<ChangeData> V8 = release(
@@ -168,7 +168,7 @@
         ChangeField.COMMENT,
         ChangeField.CHANGE,
         ChangeField.APPROVAL,
-        ChangeField.MERGEABLE);
+        ChangeField.LEGACY_MERGEABLE);
 
   @SuppressWarnings("deprecation")
   static final Schema<ChangeData> V9 = release(
@@ -192,8 +192,9 @@
         ChangeField.COMMENT,
         ChangeField.CHANGE,
         ChangeField.APPROVAL,
-        ChangeField.MERGEABLE);
+        ChangeField.LEGACY_MERGEABLE);
 
+  @SuppressWarnings("deprecation")
   static final Schema<ChangeData> V10 = release(
         ChangeField.LEGACY_ID,
         ChangeField.ID,
@@ -215,8 +216,9 @@
         ChangeField.COMMENT,
         ChangeField.CHANGE,
         ChangeField.APPROVAL,
-        ChangeField.MERGEABLE);
+        ChangeField.LEGACY_MERGEABLE);
 
+  @SuppressWarnings("deprecation")
   static final Schema<ChangeData> V11 = release(
         ChangeField.LEGACY_ID,
         ChangeField.ID,
@@ -238,12 +240,68 @@
         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 = release(V11.getFields().values());
 
+  @SuppressWarnings("deprecation")
+  static final Schema<ChangeData> V13 = 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.LEGACY_MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG);
+
+  static final Schema<ChangeData> V14 = 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.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG);
 
   private static Schema<ChangeData> release(Collection<FieldDef<ChangeData, ?>> fields) {
     return new Schema<>(true, fields);
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 9f1c353..47ea8c2 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
@@ -33,10 +33,6 @@
   }
 
   @Override
-  public void insert(ChangeData cd) throws IOException {
-  }
-
-  @Override
   public void replace(ChangeData cd) 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..40ac213 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
@@ -16,7 +16,7 @@
 
 import static com.google.gerrit.common.data.GlobalCapability.DEFAULT_MAX_QUERY_LIMIT;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Change;
@@ -133,7 +133,7 @@
       throws QueryParseException {
     ChangeIndex index = indexes.getSearchIndex();
     in = basicRewrites.rewrite(in);
-    int limit = Objects.firstNonNull(
+    int limit = MoreObjects.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.
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..1bdcf79 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
@@ -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.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
@@ -225,7 +225,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..13d37fc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexAfterUpdate.java
@@ -0,0 +1,164 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.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)));
+            }
+            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 Change change;
+
+    Index(Event event, Change change) {
+      super(event);
+      this.change = change;
+    }
+
+    @Override
+    protected Void impl() throws IOException {
+      RequestContext context = new RequestContext() {
+        @Override
+        public CurrentUser getCurrentUser() {
+          return userFactory.create(change.getOwner());
+        }
+
+        @Override
+        public Provider<ReviewDb> getReviewDbProvider() {
+          return Providers.of(db);
+        }
+      };
+      RequestContext old = tl.setContext(context);
+      try {
+        indexerFactory.create(executor, indexes).index(db, change);
+        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..b7688ff 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;
@@ -123,7 +123,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/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/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..fe69d0b 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
@@ -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,
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..c54ca4c 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;
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..b2d6443 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();
 
@@ -381,17 +387,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 37aafe0..13ad817 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..039ae65 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
@@ -109,7 +109,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..25c9dbf 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
@@ -157,6 +157,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/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index 53b7368..2205b52 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
@@ -16,6 +16,7 @@
 
 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 +31,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;
@@ -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;
@@ -263,12 +273,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 +289,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 +437,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 +448,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 +524,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 715419d..3a5f55d 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
@@ -17,7 +17,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;
@@ -37,7 +36,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;
@@ -313,16 +311,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) {
@@ -404,6 +406,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..d3a8b69 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 {
@@ -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..54a95f1 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.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,467 @@
  * 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 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;
+  }
+
+  /**
+   * 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 (RuntimeException err) {
-        throw new RuleEvalException("Exception calling " + submitRule
-            + " on change " + change.getId() + " of " + getProjectName(),
+        throw new RuleEvalException("Exception calling " + sr
+            + " on change " + cd.getId() + " of " + getProjectName(),
             err);
       }
 
       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;
@@ -215,11 +541,11 @@
         results = template[2];
       } catch (PrologException err) {
         throw new RuleEvalException("Exception calling " + filterRule
-            + " on change " + change.getId() + " of "
+            + " on change " + cd.getId() + " of "
             + parentState.getProject().getName(), err);
       } catch (RuntimeException err) {
         throw new RuleEvalException("Exception calling " + filterRule
-            + " on change " + change.getId() + " of "
+            + " on change " + cd.getId() + " of "
             + parentState.getProject().getName(), err);
       }
       childEnv = parentEnv;
@@ -235,14 +561,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/change/AgePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
index 1894596..cbce335 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.TimestampRangePredicate;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 
 import java.sql.Timestamp;
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..87afc33 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
@@ -26,50 +26,47 @@
 
 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),
+          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));
+    return or(ChangeStatusPredicate.open(),
+        ChangeStatusPredicate.forStatus(Change.Status.MERGED));
   }
 
   @NoCostComputation
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..d45a58e 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,48 @@
     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();
+        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..5c39df8 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
@@ -25,6 +25,7 @@
 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;
@@ -36,6 +37,7 @@
 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;
@@ -90,6 +92,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,13 +118,11 @@
 
 
   private static final QueryBuilder.Definition<ChangeData, ChangeQueryBuilder> mydef =
-      new QueryBuilder.Definition<ChangeData, ChangeQueryBuilder>(
-          ChangeQueryBuilder.class);
+      new QueryBuilder.Definition<>(ChangeQueryBuilder.class);
 
   @SuppressWarnings("unchecked")
   public static Integer getLimit(Predicate<ChangeData> p) {
-    IntPredicate<?> ip =
-        (IntPredicate<?>) find(p, IntPredicate.class, FIELD_LIMIT);
+    IntPredicate<?> ip = find(p, IntPredicate.class, FIELD_LIMIT);
     return ip != null ? ip.intValue() : null;
   }
 
@@ -146,6 +147,8 @@
     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;
@@ -168,6 +171,8 @@
         CapabilityControl.Factory capabilityControlFactory,
         ChangeControl.GenericFactory changeControlGenericFactory,
         ChangeData.Factory changeDataFactory,
+        FieldDef.FillArgs fillArgs,
+        PatchLineCommentsUtil plcUtil,
         AccountResolver accountResolver,
         GroupBackend groupBackend,
         AllProjectsName allProjectsName,
@@ -187,6 +192,8 @@
       this.capabilityControlFactory = capabilityControlFactory;
       this.changeControlGenericFactory = changeControlGenericFactory;
       this.changeDataFactory = changeDataFactory;
+      this.fillArgs = fillArgs;
+      this.plcUtil = plcUtil;
       this.accountResolver = accountResolver;
       this.groupBackend = groupBackend;
       this.allProjectsName = allProjectsName;
@@ -265,29 +272,22 @@
   }
 
   @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
@@ -304,7 +304,7 @@
   }
 
   @Operator
-  public Predicate<ChangeData> is(String value) throws QueryParseException {
+  public Predicate<ChangeData> is(String value) {
     if ("starred".equalsIgnoreCase(value)) {
       return new IsStarredByPredicate(args, currentUser);
     }
@@ -330,7 +330,7 @@
     }
 
     if ("mergeable".equalsIgnoreCase(value)) {
-      return new IsMergeablePredicate();
+      return new IsMergeablePredicate(schema(args.indexes), args.fillArgs);
     }
 
     try {
@@ -375,8 +375,8 @@
 
   @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,6 +393,11 @@
   }
 
   @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);
@@ -407,23 +412,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 +489,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);
   }
@@ -727,26 +732,14 @@
     } 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.
-    }
+    predicates.add(message(query));
+    predicates.add(comment(query));
     try {
       predicates.add(projects(query));
     } catch (QueryParseException e) {
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/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/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/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
index 7927cbb..d44de07 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,14 +14,13 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.TimeUtil;
 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;
@@ -30,14 +29,13 @@
 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.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;
@@ -97,7 +95,6 @@
   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;
@@ -124,12 +121,11 @@
   @Inject
   QueryProcessor(EventFactory eventFactory,
       ChangeQueryBuilder.Factory queryBuilder, CurrentUser currentUser,
-      ChangeQueryRewriter queryRewriter, Provider<ReviewDb> db,
+      ChangeQueryRewriter queryRewriter,
       TrackingFooters trackingFooters) {
     this.eventFactory = eventFactory;
     this.queryBuilder = queryBuilder.create(currentUser);
     this.queryRewriter = queryRewriter;
-    this.db = db;
     this.trackingFooters = trackingFooters;
     this.user = currentUser;
     this.maxLimit = currentUser.getCapabilities()
@@ -325,11 +321,10 @@
           }
 
           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);
+            eventFactory.addSubmitRecords(c, new SubmitRuleEvaluator(d)
+                .setAllowClosed(true)
+                .setAllowDraft(true)
+                .canSubmit());
           }
 
           if (includeCommitMessage) {
@@ -366,7 +361,7 @@
             eventFactory.addComments(c, d.messages());
             if (includePatchSets) {
               for (PatchSetAttribute attribute : c.patchSets) {
-                eventFactory.addPatchSetComments(attribute,  d.comments());
+                eventFactory.addPatchSetComments(attribute,  d.publishedComments());
               }
             }
           }
@@ -416,7 +411,7 @@
   }
 
   private int limit(Predicate<ChangeData> s) {
-    int n = Objects.firstNonNull(ChangeQueryBuilder.getLimit(s), maxLimit);
+    int n = MoreObjects.firstNonNull(ChangeQueryBuilder.getLimit(s), maxLimit);
     return limit > 0 ? Math.min(n, limit) + 1 : n + 1;
   }
 
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/schema/AllProjectsCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index 1d8c126..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();
@@ -170,7 +180,7 @@
     grant(config, meta, Permission.PUSH, admin, owners);
     grant(config, meta, Permission.SUBMIT, admin, owners);
 
-    config.commit(md);
+    config.commitToNewRef(md, RefNames.REFS_CONFIG);
   }
 
   private void grant(ProjectConfig config, AccessSection section,
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/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index 11479cc..470fb14 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -32,7 +32,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_98> C = Schema_98.class;
+  public static final Class<Schema_100> C = Schema_100.class;
 
   public static class Module extends AbstractModule {
     @Override
@@ -92,6 +92,7 @@
     try {
       final List<String> pruneList = Lists.newArrayList();
       s.pruneSchema(new StatementExecutor() {
+        @Override
         public void execute(String sql) {
           pruneList.add(sql);
         }
@@ -130,7 +131,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 +155,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..30ae4ab 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
@@ -52,6 +52,7 @@
     this.version = version;
   }
 
+  @Override
   public void start() {
     try {
       final ReviewDb db = schema.open();
@@ -84,6 +85,7 @@
     }
   }
 
+  @Override
   public void stop() {
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InstallPlugins.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_100.java
similarity index 61%
copy from gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InstallPlugins.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_100.java
index 73db6f5..0902194 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InstallPlugins.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_100.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,14 +12,16 @@
 // 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.inject.BindingAnnotation;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
+public class Schema_100 extends SchemaVersion {
+  @Inject
+  Schema_100(Provider<Schema_99> prior) {
+    super(prior);
+  }
 
-@BindingAnnotation
-@Retention(RetentionPolicy.RUNTIME)
-public @interface InstallPlugins {
+  // No database migration; merges are rechecked on reindex.
 }
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/MostSpecificComparator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/MostSpecificComparator.java
index 804a7ec..1cb180b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/MostSpecificComparator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/MostSpecificComparator.java
@@ -28,7 +28,7 @@
  * name and the regex string shortest example. A shorter distance is a more
  * specific match.
  * <li>2 - Finites first, infinities after.
- * <li>3 - Number of transitions.
+ * <li>3 - Number of transitions.  More transitions is more specific.
  * <li>4 - Length of the expression text.
  * </ul>
  *
@@ -72,7 +72,7 @@
       }
     }
     if (cmp == 0) {
-      cmp = transitions(pattern1) - transitions(pattern2);
+      cmp = transitions(pattern2) - transitions(pattern1);
     }
     if (cmp == 0) {
       cmp = pattern2.length() - pattern1.length();
@@ -86,7 +86,7 @@
       example = RefControl.shortestExample(pattern);
 
     } else if (pattern.endsWith("/*")) {
-      example = pattern.substring(0, pattern.length() - 1) + '1';
+      example = pattern;
 
     } else if (pattern.equals(refName)) {
       return 0;
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_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/resources/com/google/gerrit/server/config/CapabilityConstants.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties
index 9eb7d9b..c8e95bc 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
@@ -6,6 +6,7 @@
 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/test/java/com/google/gerrit/rules/GerritCommonTest.java b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
index 1ed35be..8e1ab8f 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
@@ -19,6 +19,7 @@
 import static com.google.gerrit.server.project.Util.category;
 import static com.google.gerrit.server.project.Util.value;
 
+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,7 +28,6 @@
 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;
 
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..0f00afb 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 {
@@ -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..0f0cc97 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,9 +23,10 @@
 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.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -37,12 +38,13 @@
 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;
@@ -50,9 +52,11 @@
 import com.google.gerrit.server.account.AccountInfo;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupBackend;
+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 +66,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 +106,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>>>() {};
+    @SuppressWarnings("unchecked")
+    final DynamicMap<RestView<DraftResource>> draftViews =
+        createMock(DynamicMap.class);
+    final TypeLiteral<DynamicMap<RestView<DraftResource>>> draftViewsType =
+        new TypeLiteral<DynamicMap<RestView<DraftResource>>>() {};
+
     final AccountInfo.Loader.Factory alf =
         createMock(AccountInfo.Loader.Factory.class);
-    final ReviewDb db = createMock(ReviewDb.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,8 +163,46 @@
     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(AccountInfo.Loader.Factory.class).toInstance(alf);
+        bind(ReviewDb.class).toInstance(db);
+        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);
 
@@ -194,6 +216,8 @@
     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 +227,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 +240,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,7 +298,8 @@
   }
 
   private ChangeControl stubChangeControl(Change c) throws OrmException {
-    return TestChanges.stubChangeControl(repoManager, c, changeOwner);
+    return TestChanges.stubChangeControl(
+        repoManager, migration, c, allUsers, changeOwner);
   }
 
   private Change newChange() {
@@ -258,27 +307,55 @@
   }
 
   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 +367,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 +383,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 +399,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(), ci.author._id);
+    }
     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 +453,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/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..821ca33 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;
@@ -41,7 +42,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;
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 0fe246e..b5c03e7 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
@@ -24,6 +24,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;
@@ -32,7 +33,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;
@@ -746,7 +746,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 1b6ae4e..9c1dd38 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
@@ -68,11 +68,6 @@
   }
 
   @Override
-  public void insert(ChangeData cd) {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
   public void replace(ChangeData cd) {
     throw new UnsupportedOperationException();
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
index 50e5764..c6425f5 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,8 +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, indexes,
+          null, null, null, null),
         null);
   }
 
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..1fa946f 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
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 02ebf51..d8ff543 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..f73c1c2
--- /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.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.FakeRealm;
+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);
+  }
+
+  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..eef6456
--- /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, 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, 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 48301ca4..bbb9be4 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
@@ -127,8 +134,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");
@@ -145,8 +152,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",
@@ -157,8 +164,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")
@@ -170,8 +177,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);
@@ -186,20 +193,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);
@@ -208,7 +215,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);
@@ -220,7 +227,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/*");
 
@@ -233,7 +240,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/*");
 
@@ -245,7 +252,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/*");
 
@@ -295,7 +302,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());
@@ -305,7 +312,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());
   }
@@ -314,7 +321,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());
@@ -323,7 +330,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);
 
@@ -338,7 +345,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);
@@ -400,7 +407,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);
@@ -409,8 +416,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);
@@ -418,7 +425,7 @@
   }
 
   @Test
-  public void testUnblockVisibilityByREGISTEREDUsers() {
+  public void testUnblockVisibilityByRegisteredUsers() {
     block(local, READ, ANONYMOUS_USERS, "refs/heads/*");
     allow(local, READ, REGISTERED_USERS, "refs/heads/*");
 
@@ -428,7 +435,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);
@@ -447,7 +454,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);
@@ -490,7 +497,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..2f48c02 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,7 +29,6 @@
 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;
@@ -41,15 +40,18 @@
 import com.google.gerrit.server.account.ListGroupMembership;
 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 +60,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 +182,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 +245,43 @@
       }
 
       @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);
 
         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 +294,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 +326,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..5ba1a9e 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
@@ -20,15 +20,22 @@
 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 static org.junit.Assume.assumeTrue;
 
-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.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.Account;
@@ -46,12 +53,13 @@
 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 +69,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 +78,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;
@@ -200,6 +219,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"));
@@ -226,6 +246,17 @@
     assertEquals(2, results.size());
     assertResultEquals(change2, results.get(0));
     assertResultEquals(change1, results.get(1));
+
+    assertEquals(2, query("status:OPEN").size());
+    assertEquals(2, query("status:o").size());
+    assertEquals(2, query("status:op").size());
+    assertEquals(2, query("status:ope").size());
+    assertEquals(2, query("status:pending").size());
+    assertEquals(2, query("status:PENDING").size());
+    assertEquals(2, query("status:p").size());
+    assertEquals(2, query("status:pe").size());
+    assertEquals(2, query("status:pen").size());
+
     results = query("is:open");
     assertEquals(2, results.size());
     assertResultEquals(change2, results.get(0));
@@ -253,6 +284,15 @@
     assertEquals(2, results.size());
     assertResultEquals(change2, results.get(0));
     assertResultEquals(change1, results.get(1));
+
+    assertEquals(2, query("status:CLOSED").size());
+    assertEquals(2, query("status:c").size());
+    assertEquals(2, query("status:cl").size());
+    assertEquals(2, query("status:clo").size());
+    assertEquals(2, query("status:clos").size());
+    assertEquals(2, query("status:close").size());
+    assertEquals(2, query("status:closed").size());
+
     results = query("is:closed");
     assertEquals(2, results.size());
     assertResultEquals(change2, results.get(0));
@@ -260,6 +300,28 @@
   }
 
   @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);
@@ -851,6 +913,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 {
+    assumeTrue(notesMigration.enabled());
+    List<Change> changes = setUpHashtagChanges();
+    List<ChangeInfo> results = query("hashtag:foo");
+    assertEquals(2, results.size());
+    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 {
+    assumeTrue(!notesMigration.enabled());
+    setUpHashtagChanges();
+    assertTrue(query("hashtag:foo").isEmpty());
+    assertTrue(query("hashtag:bar").isEmpty());
+    assertTrue(query("hashtag:\" bar \"").isEmpty());
+    assertTrue(query("hashtag:\"a tag\"").isEmpty());
+    assertTrue(query("hashtag:\" a tag \"").isEmpty());
+    assertTrue(query("hashtag:#foo").isEmpty());
+    assertTrue(query("hashtag:\"# #foo\"").isEmpty());
+  }
+
   @Test
   public void byDefault() throws Exception {
     TestRepository<InMemoryRepository> repo = createProject("repo");
@@ -904,7 +1010,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;
     }
@@ -941,11 +1047,20 @@
     assertEquals(message, expected.getId().get(), actual._number);
   }
 
+  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)));
   }
 
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..868a0e3 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;
 
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/MostSpecificComparatorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/MostSpecificComparatorTest.java
new file mode 100644
index 0000000..e974f1f
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/MostSpecificComparatorTest.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.server.util;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class MostSpecificComparatorTest {
+
+  private MostSpecificComparator cmp;
+
+  @Test
+  public void shorterDistanceWins() {
+    cmp = new MostSpecificComparator("refs/heads/master");
+    moreSpecificFirst("refs/heads/master", "refs/heads/master2");
+    moreSpecificFirst("refs/heads/master", "refs/heads/maste");
+    moreSpecificFirst("refs/heads/master", "refs/heads/*");
+    moreSpecificFirst("refs/heads/master", "^refs/heads/.*");
+    moreSpecificFirst("refs/heads/master", "^refs/heads/master.*");
+  }
+
+  /**
+   * Assuming two patterns have the same Levenshtein distance,
+   * the pattern which represents a finite language wins over a pattern
+   * which represents an infinite language.
+   */
+  @Test
+  public void finiteWinsOverInfinite() {
+    cmp = new MostSpecificComparator("refs/heads/master");
+    moreSpecificFirst("^refs/heads/......", "refs/heads/*");
+    moreSpecificFirst("^refs/heads/maste.", "^refs/heads/maste.*");
+  }
+
+  /**
+   * Assuming two patterns have the same Levenshtein distance
+   * and are both either finite or infinite the one with the higher
+   * number of state transitions (in an equivalent automaton) wins
+   */
+  @Test
+  public void higherNumberOfTransitionsWins() {
+    cmp = new MostSpecificComparator("refs/heads/x");
+    moreSpecificFirst("^refs/heads/[a-z].*", "refs/heads/*");
+    // Previously there was a bug where having a '1' in a refname would cause a
+    // glob pattern's Levenshtein distance to decrease by 1.  These two
+    // patterns should be a Levenshtein distance of 12 from the both of the
+    // refnames, where previously the 'branch1' refname would be a distance of
+    // 11 from 'refs/heads/abc/*'
+    cmp = new MostSpecificComparator("refs/heads/abc/spam/branch2");
+    moreSpecificFirst("^refs/heads/.*spam.*", "refs/heads/abc/*");
+    cmp = new MostSpecificComparator("refs/heads/abc/spam/branch1");
+    moreSpecificFirst("^refs/heads/.*spam.*", "refs/heads/abc/*");
+  }
+
+  /**
+   * Assuming the same Levenshtein distance, (in)finity and the number
+   * of transitions, the longer pattern wins
+   */
+  @Test
+  public void longerPatternWins() {
+    cmp = new MostSpecificComparator("refs/heads/x");
+    moreSpecificFirst("^refs/heads/[a-z].*", "^refs/heads/..*");
+  }
+
+  private void moreSpecificFirst(String first, String second) {
+    assertTrue(cmp.compare(first, second) < 0);
+  }
+}
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/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
index 6b2f7c7..0053098 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;
@@ -53,6 +52,8 @@
 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 +80,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 +96,6 @@
     cfg.setBoolean("index", "lucene", "testInmemory", true);
     cfg.setInt("index", "lucene", "testVersion",
         ChangeSchemas.getLatest().getVersion());
-    return cfg;
   }
 
   private final Config cfg;
@@ -149,6 +154,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 +167,6 @@
     install(new DefaultCacheFactory.Module());
     install(new SmtpEmailSender.Module());
     install(new SignedTokenEmailTokenVerifier.Module());
-    install(new MergeabilityChecksExecutorModule());
 
     IndexType indexType = null;
     try {
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..0e9a681 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,30 +17,43 @@
 import static org.easymock.EasyMock.expect;
 
 import com.google.common.collect.Ordering;
+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.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.Project;
 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 java.util.concurrent.atomic.AtomicInteger;
+
 /**
  * Utility functions to create and manipulate Change, ChangeUpdate, and
  * ChangeControl objects for testing.
  */
 public class TestChanges {
+  private static final AtomicInteger nextChangeId = new AtomicInteger(1);
+
   public static Change newChange(Project.NameKey project, IdentifiedUser user) {
-    Change.Id changeId = new Change.Id(1);
+    return newChange(project, user, nextChangeId.getAndIncrement());
+  }
+
+  public static Change newChange(Project.NameKey project, IdentifiedUser user,
+      int id) {
+    Change.Id changeId = new Change.Id(id);
     Change c = new Change(
         new Change.Key("Iabcd1234abcd1234abcd1234abcd1234abcd1234"),
         changeId,
@@ -52,27 +65,31 @@
   }
 
   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 8c8b007..69b8ed9 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
@@ -50,7 +50,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 +107,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));
+        schema, new StandardAnalyzer(CharArraySet.EMPTY_SET));
 
     base = Strings.nullToEmpty(base);
     openIndex = new CloudSolrServer(url);
@@ -147,25 +141,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);
@@ -252,12 +227,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);
@@ -289,7 +264,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());
         }
 
@@ -323,30 +298,25 @@
     }
   }
 
-  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")
@@ -365,7 +335,7 @@
         || 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/CachingPublicKeyAuthenticator.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CachingPublicKeyAuthenticator.java
index f315cff..0471af8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CachingPublicKeyAuthenticator.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CachingPublicKeyAuthenticator.java
@@ -17,56 +17,12 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
-import org.apache.sshd.common.Session;
-import org.apache.sshd.common.SessionListener;
-import org.apache.sshd.server.PublickeyAuthenticator;
-import org.apache.sshd.server.session.ServerSession;
-
-import java.security.PublicKey;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
 @Singleton
-public class CachingPublicKeyAuthenticator implements PublickeyAuthenticator,
-    SessionListener {
-
-  private final PublickeyAuthenticator authenticator;
-  private final Map<ServerSession, Map<PublicKey, Boolean>> sessionCache;
+public class CachingPublicKeyAuthenticator
+    extends org.apache.sshd.server.auth.CachingPublicKeyAuthenticator {
 
   @Inject
   public CachingPublicKeyAuthenticator(DatabasePubKeyAuth authenticator) {
-    this.authenticator = authenticator;
-    this.sessionCache = new ConcurrentHashMap<>();
-  }
-
-  @Override
-  public boolean authenticate(String username, PublicKey key,
-      ServerSession session) {
-    Map<PublicKey, Boolean> m = sessionCache.get(session);
-    if (m == null) {
-      m = new HashMap<>();
-      sessionCache.put(session, m);
-      session.addListener(this);
-    }
-    if (m.containsKey(key)) {
-      return m.get(key);
-    }
-    boolean r = authenticator.authenticate(username, key, session);
-    m.put(key, r);
-    return r;
-  }
-
-  @Override
-  public void sessionCreated(Session session) {
-  }
-
-  @Override
-  public void sessionEvent(Session sesssion, Event event) {
-  }
-
-  @Override
-  public void sessionClosed(Session session) {
-    sessionCache.remove(session);
+    super(authenticator);
   }
 }
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/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 32c13b4..32e45a9e 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
@@ -83,6 +83,7 @@
 import org.apache.sshd.common.session.AbstractSession;
 import org.apache.sshd.common.session.ConnectionService;
 import org.apache.sshd.common.signature.SignatureDSA;
+import org.apache.sshd.common.signature.SignatureECDSA;
 import org.apache.sshd.common.signature.SignatureRSA;
 import org.apache.sshd.common.util.Buffer;
 import org.apache.sshd.common.util.SecurityUtils;
@@ -141,6 +142,7 @@
  */
 @Singleton
 public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
+  @SuppressWarnings("hiding") // Don't use AbstractCloseable's logger.
   private static final Logger log = LoggerFactory.getLogger(SshDaemon.class);
 
   public static enum SshSessionBackend {
@@ -152,7 +154,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
@@ -188,6 +190,15 @@
         IDLE_TIMEOUT,
         String.valueOf(SECONDS.toMillis(idleTimeoutSeconds)));
 
+    long rekeyTimeLimit = ConfigUtil.getTimeUnit(cfg, "sshd", null,
+        "rekeyTimeLimit", 3600, SECONDS);
+    getProperties().put(
+        REKEY_TIME_LIMIT,
+        String.valueOf(SECONDS.toMillis(rekeyTimeLimit)));
+
+    getProperties().put(REKEY_BYTES_LIMIT,
+        String.valueOf(cfg.getLong("sshd", "rekeyBytesLimit", 1024 * 1024 * 1024 /* 1GB */)));
+
     final int maxConnectionsPerUser =
         cfg.getInt("sshd", "maxConnectionsPerUser", 64);
     if (0 < maxConnectionsPerUser) {
@@ -279,26 +290,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) {
@@ -316,14 +327,14 @@
 
   @Override
   public synchronized void stop() {
-    if (acceptor != null) {
+    if (daemonAcceptor != null) {
       try {
-        acceptor.close(true).await();
+        daemonAcceptor.close(true).await();
         log.info("Stopped Gerrit SSHD");
       } catch (InterruptedException e) {
         log.warn("Exception caught while closing", e);
       } finally {
-        acceptor = null;
+        daemonAcceptor = null;
       }
     }
   }
@@ -512,7 +523,11 @@
 
   private void initSignatures() {
     setSignatureFactories(Arrays.<NamedFactory<Signature>> asList(
-        new SignatureDSA.Factory(), new SignatureRSA.Factory()));
+        new SignatureDSA.Factory(),
+        new SignatureRSA.Factory(),
+        new SignatureECDSA.NISTP256Factory(),
+        new SignatureECDSA.NISTP384Factory(),
+        new SignatureECDSA.NISTP521Factory()));
   }
 
   private void initCompression() {
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 7df8db4..39a9fe7 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;
@@ -269,9 +269,8 @@
 
   private String extractWhat(DispatchCommand dcmd) {
     String commandName = dcmd.getCommandName();
-    String[] args = dcmd.getArguments();
-    if (args.length > 1) {
-      return commandName + "." + args[1];
+    for (String arg : dcmd.getArguments()) {
+      commandName = commandName + "." + arg;
     }
     return commandName;
   }
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..db497f4 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,41 @@
 
 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.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountInfo;
 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 +62,12 @@
     @Inject
     protected ListMembersCommandImpl(GroupCache groupCache,
         Factory groupDetailFactory,
-        AccountInfo.Loader.Factory accountLoaderFactory,
-        AccountCache accountCache) {
+        AccountInfo.Loader.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 +77,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(member._id.toString());
+        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/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..2a04402 100644
--- a/gerrit-war/BUCK
+++ b/gerrit-war/BUCK
@@ -9,8 +9,9 @@
     '//gerrit-httpd:httpd',
     '//gerrit-lucene:lucene',
     '//gerrit-openid:openid',
+    '//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..253299d 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;
@@ -278,7 +277,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 +308,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());
@@ -344,6 +342,7 @@
     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 90776eb..1dd9186 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -50,8 +50,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',
 )
 
@@ -115,8 +115,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'],
 )
@@ -170,18 +170,21 @@
 
 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'],
+  deps = ['//lib/hamcrest:hamcrest-core'],
 )
 
 maven_jar(
-  name = 'hamcrest-core',
-  id = 'org.hamcrest:hamcrest-core:1.3',
-  sha1 = '42a25dc3219429f0e5d060061f71acb49bf010a0',
+  name = 'truth',
+  id = 'com.google.truth:truth:0.24',
+  sha1 = '7f70cea641f8812179c60162cb38354e81a459ae',
   license = 'DO_NOT_DISTRIBUTE',
-  visibility = ['//lib:junit'],
+  deps = [
+    ':guava',
+    ':junit',
+  ],
 )
 
 maven_jar(
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..27bf30a 100644
--- a/lib/asciidoctor/java/DocIndexer.java
+++ b/lib/asciidoctor/java/DocIndexer.java
@@ -51,7 +51,7 @@
 import java.util.zip.ZipOutputStream;
 
 public class DocIndexer {
-  private static final Version LUCENE_VERSION = Version.LUCENE_48;
+  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 +99,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/bouncycastle/BUCK b/lib/bouncycastle/BUCK
index 99f960e..038de5d 100644
--- a/lib/bouncycastle/BUCK
+++ b/lib/bouncycastle/BUCK
@@ -1,7 +1,7 @@
 include_defs('//lib/maven.defs')
 
 # This version must match the version that also appears in
-# gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config
+# gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
 VERSION = '1.49'
 
 maven_jar(
diff --git a/lib/codemirror/BUCK b/lib/codemirror/BUCK
index e8539c6..e25b372 100644
--- a/lib/codemirror/BUCK
+++ b/lib/codemirror/BUCK
@@ -2,8 +2,8 @@
 include_defs('//lib/codemirror/cm3.defs')
 include_defs('//lib/codemirror/closure.defs')
 
-VERSION = '28a638a984'
-SHA1 = '68f8f136092a5965778186fb401a33be34cf73ed'
+VERSION = '57e7ed7177'
+SHA1 = 'd78bc5518707960647d0b8b85d9f1ac011b785d5'
 URL = GERRIT + 'net/codemirror/codemirror-%s.zip' % VERSION
 
 ZIP = 'codemirror-%s.zip' % VERSION
@@ -14,6 +14,12 @@
   '--warning_level QUIET'
 ]
 
+# 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'
+
 genrule(
   name = 'css',
   cmd = ';'.join([
@@ -87,3 +93,29 @@
     ' -v ' + SHA1,
   out = ZIP,
 )
+
+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/closure.defs b/lib/codemirror/closure.defs
index e602b9f..08b6897 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,
@@ -22,29 +16,3 @@
     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/cm3.defs b/lib/codemirror/cm3.defs
index 84f09c3..e9eff39 100644
--- a/lib/codemirror/cm3.defs
+++ b/lib/codemirror/cm3.defs
@@ -34,7 +34,6 @@
   'erlang/erlang.js',
   'gas/gas.js',
   'gfm/gfm.js',
-  'go/go.js',
   'groovy/groovy.js',
   'haskell/haskell.js',
   'htmlmixed/htmlmixed.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..3e9908b 100644
--- a/lib/gwt/BUCK
+++ b/lib/gwt/BUCK
@@ -18,12 +18,32 @@
   deps = [
     ':javax-validation',
     ':javax-validation_src',
+    ':json',
   ],
   attach_source = False,
   exclude = ['org/eclipse/jetty/*'],
 )
 
 maven_jar(
+  name = 'codeserver',
+  id = 'com.google.gwt:gwt-codeserver:' + VERSION,
+  sha1 = '940edc715cc31b1957e18f617f75a068f251346a',
+  license = 'Apache2.0',
+  deps = [
+    ':dev',
+  ],
+  attach_source = False,
+)
+
+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 +71,3 @@
   license = 'Apache2.0',
   visibility = [],
 )
-
diff --git a/lib/hamcrest/BUCK b/lib/hamcrest/BUCK
new file mode 100644
index 0000000..38d7baf
--- /dev/null
+++ b/lib/hamcrest/BUCK
@@ -0,0 +1,25 @@
+include_defs('//lib/maven.defs')
+
+VERSION = '1.3'
+
+maven_jar(
+  name = 'hamcrest-core',
+  id = 'org.hamcrest:hamcrest-core:' + VERSION,
+  sha1 = '42a25dc3219429f0e5d060061f71acb49bf010a0',
+  license = 'DO_NOT_DISTRIBUTE',
+  visibility = [
+    '//lib:junit',
+    '//gerrit-acceptance-tests:lib'
+  ],
+)
+
+maven_jar(
+  name = 'hamcrest-library',
+  id = 'org.hamcrest:hamcrest-library:' + VERSION,
+  sha1 = '4785a3c21320980282f9f33d0d1264a69040538f',
+  license = 'DO_NOT_DISTRIBUTE',
+  visibility = [
+    '//lib:junit',
+    '//gerrit-acceptance-tests:lib'
+  ],
+)
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/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..df14528 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.1'
 
 maven_jar(
   name = 'core',
   id = 'org.apache.lucene:lucene-core:' + VERSION,
-  sha1 = 'a549eef6316a2c38d4cda932be809107deeaf8a7',
+  sha1 = '4ff28101d9de465b7f3cf59d7bc2892c1c118b4b',
   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 = '6491c6019c32e7c4f7674f238d5beaa84d3108a6',
   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 = '0174ffd89d5289037ae24759f38111285b98636d',
   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/mina/BUCK b/lib/mina/BUCK
index fac2ba4..2ca734c 100644
--- a/lib/mina/BUCK
+++ b/lib/mina/BUCK
@@ -8,12 +8,12 @@
 
 maven_jar(
   name = 'sshd',
-  id = 'org.apache.sshd:sshd-core:0.11.1-atlassian-1',
-  sha1 = '0de20bfa03ddeedc8eb54ab6e85e90e776ea18f8',
+  id = 'org.apache.sshd:sshd-core:0.12.0-9-g635de65',
+  sha1 = '1ff3c9ebd0acfcd095eef30a0ee0b99846967711',
+  repository = GERRIT,
   license = 'Apache2.0',
   deps = [':core'],
   exclude = EXCLUDE,
-  repository = ATLASSIAN,
 )
 
 maven_jar(
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/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..ca5bd66 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit 6582905669d0ccdd009f839936f8209010ae9d6f
+Subproject commit ca5bd66cee8a026f64d6aec6a334e79f88559ce9
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..5d57d68 100644
--- a/tools/default.defs
+++ b/tools/default.defs
@@ -15,6 +15,8 @@
 # Rule definitions loaded by default into every BUCK file.
 
 include_defs('//tools/gwt-constants.defs')
+include_defs('//tools/java_doc.defs')
+include_defs('//tools/java_sources.defs')
 import copy
 
 def genantlr(
@@ -38,6 +40,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 +80,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 +146,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..1b89701 100644
--- a/tools/eclipse/BUCK
+++ b/tools/eclipse/BUCK
@@ -11,6 +11,7 @@
     '//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',
@@ -18,7 +19,8 @@
     '//lib/bouncycastle:bcprov',
     '//lib/bouncycastle:bcpg',
     '//lib/bouncycastle:bcpkix',
-    '//lib/jetty:webapp',
+    '//lib/gwt:codeserver',
+    '//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..bdb6814 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,14 @@
 <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;"/>
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit/buck-out/gen/lib/gwt/codeserver/gwt-codeserver-2.6.1.jar&quot; path=&quot;3&quot; type=&quot;2&quot;/&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"/>
 </launchConfiguration>
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index 2008316..b206412 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -37,15 +37,17 @@
 
 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 gen_project(name='gerrit', dir=ROOT):
+  p = path.join(dir, '.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,6 +59,23 @@
 </projectDescription>\
 """, file=fd)
 
+def gen_plugin_classpath(dir):
+  p = path.join(dir, '.classpath')
+  with open(p, 'w') as fd:
+    if path.exists(path.join(dir, '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 query_classpath(targets):
     deps = []
@@ -72,7 +91,7 @@
     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 +99,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,6 +108,7 @@
   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):
@@ -119,6 +141,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 +163,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 +185,16 @@
   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)
+
 try:
   if args.src:
     try:
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/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',