Merge branch 'stable-2.10'

* stable-2.10:
  Fix IncludingGroupMembership.containsAnyOf infinite loop
  Fix using HTTP methods other than GET from plugins
  Pass ChangeControl into CodeReviewCommit error instances
  Avoid CodeReviewCommit.error() whenever possible
  Update replication plugin to latest revision

Conflicts:
	gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java

Change-Id: I9c59567bd54a493a5e262e7776df00f1b776b553
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..b23029a 100644
--- a/.buckversion
+++ b/.buckversion
@@ -1 +1 @@
-0fe4569e871fd6588f7cbfb4b1d4a14baa791a9f
+7608e515c2355d2e799f4b404683433fea9cc024
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/.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..31df2c3 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]]
@@ -1208,6 +1220,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..40f2378 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].
+
+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, or have been granted
+link:access-control.html#capability_generateHttpPassword[the 'Generate HTTP Password' global capability]
+in addition to 'Modify Account' global capability.
 
 == 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 c95ff73..0c13b23 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1502,6 +1502,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 +1719,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 +1750,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
@@ -2369,6 +2398,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
 
@@ -2517,6 +2631,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
@@ -2988,6 +3113,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
 
@@ -3008,6 +3151,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
@@ -3015,6 +3168,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..0bf44d0 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
 
@@ -456,6 +467,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
 
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..a14aebc 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -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:
 
@@ -1739,10 +1759,11 @@
 
   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 ;
+    return name;
   }
 
   @Override
@@ -1750,12 +1771,26 @@
     return String.format(placeHolderUrlProjectCommit, project, commit);
   }
 
+  @Override
+  public String getImageUrl() {
+    return imageUrl;
+  }
+
+  @Override
+  public String getTarget() {
+    return "_blank";
+  }
 }
 ----
 
+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 100%
rename from Documentation/doc.css
rename to Documentation/doc.css.in
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/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/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/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 11a482f..b1f6bcc 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..d08e894 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
@@ -3168,6 +3389,12 @@
 |`diff_header`     ||A list of strings representing the patch set diff header.
 |`content`         ||The content differences in the file as a list of
 link:#diff-content[DiffContent] entities.
+|'web_links_a'     |optional|
+Links to the side A file in external sites as a list of
+link:rest-api-changes.html#web-link-info[WebLinkInfo] entries.
+|'web_links_b'     |optional|
+Links to the side B file in external sites as a list of
+link:rest-api-changes.html#web-link-info[WebLinkInfo] entries.
 |==========================
 
 [[diff-intraline-info]]
@@ -3433,6 +3660,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 +3670,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 +3834,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..3a2aebd 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",
@@ -1573,6 +1584,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 +1636,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 +1679,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 +1697,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 +1992,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|
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 b892c4a..9bc10f4 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -155,6 +155,19 @@
   git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental%topic=driver/i42
 ====
 
+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
+====
+
+The `l='label[score]'` option may be specified more than once to
+apply multiple review labels.
+
+The value is optional.  If not specified, it defaults to +1 (if
+the label range allows it).
+
 If you are frequently uploading changes to the same Gerrit server,
 consider adding an SSH host block in `~/.ssh/config` to remember
 your username, hostname and port number.  This permits the use of
@@ -172,8 +185,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
@@ -221,7 +234,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..23f3247
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.11.txt
@@ -0,0 +1,69 @@
+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
+~~~~~
+
+TODO
+
+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..d93b25c 100644
--- a/gerrit-acceptance-tests/BUCK
+++ b/gerrit-acceptance-tests/BUCK
@@ -8,8 +8,9 @@
     '//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:testutil',
@@ -25,8 +26,10 @@
     '//lib:junit',
     '//lib:servlet-api-3_1',
 
-    '//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..fcc8680 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,35 @@
 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.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.Util;
 import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.TempFileUtil;
 import com.google.gson.Gson;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -41,6 +54,8 @@
 import org.apache.http.HttpStatus;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Rule;
 import org.junit.rules.TestRule;
@@ -60,6 +75,9 @@
   public Config baseConfig;
 
   @Inject
+  protected AllProjectsName allProjects;
+
+  @Inject
   protected AccountCreator accounts;
 
   @Inject
@@ -77,6 +95,15 @@
   @Inject
   protected PushOneCommit.Factory pushFactory;
 
+  @Inject
+  protected MetaDataUpdate.Server metaDataUpdateFactory;
+
+  @Inject
+  protected ProjectCache projectCache;
+
+  @Inject
+  protected GroupCache groupCache;
+
   protected Git git;
   protected GerritServer server;
   protected TestAccount admin;
@@ -218,4 +245,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..5f5da47 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;
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..1ca8795 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,6 +22,7 @@
 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;
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..cc75107 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 {
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/RestSession.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
index 45befd0..bf4918c 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) throws IOException {
+    return newRawInput(content.getBytes(StandardCharsets.UTF_8));
+  }
 
+  public static RawInput newRawInput(final byte[] bytes) throws IOException {
+    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/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index c79b198..6a58933 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
@@ -26,7 +26,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;
@@ -72,7 +71,7 @@
       IOException, RestApiException {
     PushOneCommit.Result r = createChange();
     gApi.changes()
-        .id("p~master~" + r.getChangeId())
+        .id(r.getChangeId())
         .abandon();
   }
 
@@ -81,10 +80,10 @@
       IOException, RestApiException {
     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();
   }
 
@@ -93,15 +92,15 @@
       IOException, RestApiException {
     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();
   }
 
@@ -111,12 +110,14 @@
       IOException, RestApiException {
     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 RestApiException {
+    ChangeInfo ci = gApi.changes().id(changeId).get();
     Set<Account.Id> result = Sets.newHashSet();
     for (LabelInfo li : ci.labels.values()) {
       for (ApprovalInfo ai : li.all) {
@@ -132,30 +133,34 @@
     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);
+    assertEquals(ImmutableSet.of(user.id), getReviewers(r.getChangeId()));
   }
 
   @Test
   public void addReviewerToClosedChange() throws GitAPIException,
       IOException, RestApiException {
     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()));
+    assertEquals(ImmutableSet.of(admin.getId()), getReviewers(r.getChangeId()));
 
     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()));
+        getReviewers(r.getChangeId()));
   }
 
   @Test
@@ -278,4 +283,24 @@
     revision(r).review(ReviewInput.recommend());
     assertTrue(get(r.getChangeId()).reviewed);
   }
+
+  @Test
+  public void topic() throws Exception {
+    PushOneCommit.Result r = createChange();
+    assertEquals("", gApi.changes()
+        .id(r.getChangeId())
+        .topic());
+    gApi.changes()
+        .id(r.getChangeId())
+        .topic("mytopic");
+    assertEquals("mytopic", gApi.changes()
+        .id(r.getChangeId())
+        .topic());
+    gApi.changes()
+        .id(r.getChangeId())
+        .topic("");
+    assertEquals("", gApi.changes()
+        .id(r.getChangeId())
+        .topic());
+  }
 }
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..9aa3917 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,22 +14,22 @@
 
 package com.google.gerrit.acceptance.api.project;
 
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.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
@@ -68,8 +68,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")
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..54e8799 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
@@ -17,6 +17,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -26,7 +27,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;
@@ -145,10 +145,67 @@
     assertEquals(2, orig.get().messages.size());
 
     assertTrue(cherry.get().subject.contains(in.message));
-    cherry.current()
-        .review(ReviewInput.approve());
-    cherry.current()
-        .submit();
+    cherry.current().review(ReviewInput.approve());
+    cherry.current().submit();
+  }
+
+  @Test
+  public void cherryPickIdenticalTree() throws GitAPIException,
+      IOException, RestApiException {
+    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());
+    ChangeApi orig = gApi.changes()
+        .id("p~master~" + r.getChangeId());
+
+    assertEquals(1, orig.get().messages.size());
+    ChangeApi cherry = orig.revision(r.getCommit().name())
+        .cherryPick(in);
+    assertEquals(2, orig.get().messages.size());
+
+    assertTrue(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) {
+      assertEquals("Cherry pick failed: identical tree", e.getMessage());
+    }
+  }
+
+  @Test
+  public void cherryPickConflict() throws GitAPIException,
+      IOException, RestApiException {
+    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());
+    assertEquals(1, orig.get().messages.size());
+
+    try {
+      orig.revision(r.getCommit().name()).cherryPick(in);
+      fail("Cherry-pick merge conflict error expected");
+    } catch (RestApiException e) {
+      assertEquals("Cherry pick failed: merge conflict", e.getMessage());
+    }
   }
 
   @Test
@@ -206,12 +263,6 @@
             .isEmpty());
   }
 
-  protected RevisionApi revision(PushOneCommit.Result r) throws Exception {
-    return gApi.changes()
-        .id(r.getChangeId())
-        .current();
-  }
-
   private void merge(PushOneCommit.Result r) throws Exception {
     revision(r).review(ReviewInput.approve());
     revision(r).submit();
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..a24294c
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -0,0 +1,675 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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
+  private PushOneCommit.Factory pushFactory;
+
+  @Inject
+  ChangeEditUtil editUtil;
+
+  @Inject
+  private ChangeEditModifier modifier;
+
+  @Inject
+  private FileContentUtil fileUtil;
+
+  private ReviewDb db;
+  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..5dbf366 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
   }
@@ -192,9 +208,74 @@
     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..6f13c17 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,12 +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;
@@ -37,15 +34,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();
@@ -70,12 +58,6 @@
     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 465befd..71f008a 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
@@ -15,13 +15,18 @@
 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 {
+  public void selectHttpUrl() throws GitAPIException, IOException, URISyntaxException {
+    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/SubmitOnPushIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
index a73169d..cdd3740 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,51 +22,33 @@
 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.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 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;
 
@@ -76,15 +56,6 @@
   private ApprovalsUtil approvalsUtil;
 
   @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
-
-  @Inject
-  private ProjectCache projectCache;
-
-  @Inject
-  private GroupCache groupCache;
-
-  @Inject
   private ChangeNotes.Factory changeNotesFactory;
 
   @Inject
@@ -93,26 +64,6 @@
   @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 {
@@ -129,6 +80,7 @@
       IOException, ConfigInvalidException {
     grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
     grant(Permission.CREATE, project, "refs/tags/*");
+    grant(Permission.PUSH, project, "refs/tags/*");
     final String tag = "v1.0";
     PushOneCommit push = pushFactory.create(db, admin.getIdent());
     push.setTag(tag);
@@ -255,19 +207,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());
@@ -329,12 +268,6 @@
     }
   }
 
-  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..77a7a91
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
@@ -0,0 +1,271 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.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.account.GroupCache;
+import com.google.gerrit.server.edit.ChangeEditModifier;
+import com.google.gerrit.server.git.GitRepositoryManager;
+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 GitRepositoryManager repoManager;
+
+  @Inject
+  private GroupCache groupCache;
+
+  @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..8733611 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;
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..001b85a 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,15 +38,6 @@
 
 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,
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/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index ab8a5fc..1ae21e8 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;
@@ -235,7 +234,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..aa0412c 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
@@ -25,6 +25,7 @@
 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;
@@ -48,10 +49,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
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..02d73ad 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,7 @@
 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 +44,6 @@
 
 public class ChangeOwnerIT extends AbstractDaemonTest {
 
-  @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
-
-  @Inject
-  private ProjectCache projectCache;
-
   private TestAccount user2;
 
   private RestSession sessionOwner;
@@ -63,8 +54,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();
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..a9112cb
--- /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 org.junit.Assert.assertEquals;
+
+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 {
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    List<String> result = toHashtagList(r);
+    assertEquals(expected, result);
+  }
+
+  @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<String>(
+          Lists.newArrayList(Splitter.on(CharMatcher.anyOf(",")).split(toAdd)));
+    }
+    if (toRemove != null) {
+      input.remove = new HashSet<String>(
+          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..eb46b5d 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,8 +19,8 @@
 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.google.gwtorm.server.OrmException;
 
 import com.jcraft.jsch.JSchException;
 
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..1d36319 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,10 +31,8 @@
 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;
 
@@ -50,15 +48,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;
@@ -161,6 +150,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..372723b 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,15 +40,6 @@
 
 public class CacheOperationsIT extends AbstractDaemonTest {
 
-  @Inject
-  private ProjectCache projectCache;
-
-  @Inject
-  private AllProjectsName allProjects;
-
-  @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
-
   @Test
   public void flushAll() throws IOException {
     RestResponse r = adminSession.get("/config/server/caches/project_list");
@@ -140,8 +128,8 @@
     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..729921d 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,15 +36,6 @@
 
 public class FlushCacheIT extends AbstractDaemonTest {
 
-  @Inject
-  private ProjectCache projectCache;
-
-  @Inject
-  private AllProjectsName allProjects;
-
-  @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
-
   @Test
   public void flushCache() throws IOException {
     RestResponse r = adminSession.get("/config/server/caches/groups");
@@ -92,8 +80,8 @@
     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/group/DefaultGroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/DefaultGroupsIT.java
index 0d229ca..f60b5dd 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;
@@ -47,6 +48,7 @@
   public void defaultGroupsCreated_ssh() throws JSchException, IOException {
     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();
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..39f5ef1 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -24,8 +24,6 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.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;
@@ -37,7 +35,6 @@
 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;
@@ -60,22 +57,15 @@
 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 {
     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);
@@ -252,7 +242,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..c912823 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,15 +23,9 @@
 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;
 
@@ -40,15 +33,6 @@
 
 public class DeleteBranchIT extends AbstractDaemonTest {
 
-  @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
-
-  @Inject
-  private ProjectCache projectCache;
-
-  @Inject
-  private AllProjectsName allProjects;
-
   private Branch.NameKey branch;
 
   @Before
@@ -82,8 +66,7 @@
   }
 
   @Test
-  public void deleteBranchByProjectOwner() throws IOException,
-      ConfigInvalidException {
+  public void deleteBranchByProjectOwner() throws Exception {
     grantOwner();
 
     RestResponse r =
@@ -99,8 +82,7 @@
   }
 
   @Test
-  public void deleteBranchByAdminForcePushBlocked() throws IOException,
-      ConfigInvalidException {
+  public void deleteBranchByAdminForcePushBlocked() throws Exception {
     blockForcePush();
     RestResponse r =
         adminSession.delete("/projects/" + project.get()
@@ -116,7 +98,7 @@
 
   @Test
   public void deleteBranchByProjectOwnerForcePushBlocked_Forbidden()
-      throws IOException, ConfigInvalidException {
+      throws Exception {
     grantOwner();
     blockForcePush();
     RestResponse r =
@@ -126,26 +108,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..c9fd73a 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,7 +22,6 @@
 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;
 
@@ -37,19 +36,12 @@
 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());
   }
@@ -72,7 +64,7 @@
   public void testGcOneProject() throws JSchException, IOException {
     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..bd4153b 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,9 +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;
 
@@ -36,12 +33,6 @@
 
 public class GetChildProjectIT extends AbstractDaemonTest {
 
-  @Inject
-  private AllProjectsName allProjects;
-
-  @Inject
-  private ProjectCache projectCache;
-
   @Test
   public void getNonExistingChildProject_NotFound() throws IOException {
     assertEquals(HttpStatus.SC_NOT_FOUND,
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..739810e 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,139 @@
 
 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.GitRepositoryManager;
 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 {
-
   @Inject
-  private ProjectCache projectCache;
+  private GitRepositoryManager repoManager;
 
-  @Inject
-  private AllProjectsName allProjects;
+  private TestRepository<Repository> repo;
 
-  @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
+  @Before
+  public void setUp() throws Exception {
+    repo = new TestRepository<>(repoManager.openRepository(project));
 
-  @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..2e28655 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,19 @@
 
 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,13 +36,6 @@
 import java.util.List;
 
 public class ListBranchesIT extends AbstractDaemonTest {
-
-  @Inject
-  private MetaDataUpdate.Server metaDataUpdateFactory;
-
-  @Inject
-  private ProjectCache projectCache;
-
   @Test
   public void listBranchesOfNonExistingProject_NotFound() throws IOException {
     assertEquals(HttpStatus.SC_NOT_FOUND,
@@ -61,8 +43,7 @@
   }
 
   @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());
@@ -106,8 +87,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 +103,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");
@@ -139,14 +118,6 @@
     return adminSession.get(endpoint);
   }
 
-  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());
-  }
-
   private static List<BranchInfo> toBranchInfoList(RestResponse r)
       throws IOException {
     List<BranchInfo> result =
@@ -154,19 +125,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..495c9da 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,9 +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;
 
@@ -38,9 +36,6 @@
 
 public class ListChildProjectsIT extends AbstractDaemonTest {
 
-  @Inject
-  private AllProjectsName allProjects;
-
   @Test
   public void listChildrenOfNonExistingProject_NotFound() throws IOException {
     assertEquals(HttpStatus.SC_NOT_FOUND,
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..486389d 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
@@ -26,7 +26,6 @@
 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;
@@ -43,9 +42,6 @@
 public class ListProjectsIT extends AbstractDaemonTest {
 
   @Inject
-  private AllProjectsName allProjects;
-
-  @Inject
   private AllUsersName allUsers;
 
   @Test
@@ -121,6 +117,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);
@@ -138,13 +139,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());
@@ -181,6 +191,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);
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..20da55f 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,26 +16,19 @@
 
 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;
 
@@ -44,38 +37,15 @@
 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
@@ -89,13 +59,13 @@
             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());
   }
 
@@ -125,7 +95,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/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..66cd2a0
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -0,0 +1,196 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.extensions.restapi.RestApiException;
+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.api.errors.GitAPIException;
+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 GitAPIException, IOException {
+    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 RestApiException, 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<String, List<ReviewInput.CommentInput>>();
+    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 GitAPIException, IOException {
+    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 GitAPIException, IOException {
+    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 IOException, GitAPIException {
+    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..4a577cc 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,7 +28,10 @@
 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;
@@ -38,6 +41,11 @@
 import java.util.List;
 
 public class GetRelatedIT extends AbstractDaemonTest {
+  @Inject
+  private ChangeEditUtil editUtil;
+
+  @Inject
+  private ChangeEditModifier editModifier;
 
   @Test
   public void getRelatedNoResult() throws GitAPIException,
@@ -139,15 +147,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..7b92f52 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
@@ -23,6 +23,7 @@
 import static org.junit.Assert.assertNull;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.GitUtil.Commit;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
@@ -43,6 +44,7 @@
 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";
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..6ebf783 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,11 +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;
 
@@ -42,24 +41,12 @@
 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..6093f30 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
@@ -21,6 +21,7 @@
 import static org.junit.Assert.assertTrue;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.GitUtil.Commit;
 
 import com.jcraft.jsch.JSchException;
@@ -32,6 +33,7 @@
 import java.io.IOException;
 import java.util.Locale;
 
+@NoHttpd
 public class BanCommitIT extends AbstractDaemonTest {
 
   @Test
@@ -42,7 +44,7 @@
     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..1a46773 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,11 +21,11 @@
 
 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;
@@ -40,12 +40,10 @@
 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 +52,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());
 
@@ -74,11 +68,11 @@
   @UseLocalDisk
   public void testGc() throws JSchException, IOException {
     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);
   }
 
@@ -86,9 +80,9 @@
   @UseLocalDisk
   public void testGcAll() throws JSchException, IOException {
     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
@@ -103,14 +97,14 @@
   @Test
   @UseLocalDisk
   public void testGcAlreadyScheduled() {
-    gcQueue.addAll(Arrays.asList(project1));
+    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..65bb034 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
@@ -161,7 +161,8 @@
       return defaultFactory.build(def);
     }
 
-    SqlStore<K, V> store = newSqlStore(def.name(), def.keyType(), limit);
+    SqlStore<K, V> store = newSqlStore(def.name(), def.keyType(), limit,
+        def.expireAfterWrite(TimeUnit.SECONDS));
     H2CacheImpl<K, V> cache = new H2CacheImpl<K, V>(
         executor, store, def.keyType(),
         (Cache<K, ValueHolder<V>>) defaultFactory.create(def, true).build());
@@ -182,7 +183,8 @@
       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>(
@@ -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/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/ChangeDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
index e067f06..448ce86 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;
@@ -93,6 +94,14 @@
     canCherryPick = a;
   }
 
+  public boolean getCanEditHashtags() {
+    return canEditHashtags;
+  }
+
+  public void setCanEditHashtags(final boolean a) {
+    canEditHashtags = a;
+  }
+
   public boolean canPublish() {
     return canPublish;
   }
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..c6c2d50 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";
 
@@ -109,7 +112,9 @@
     NAMES_ALL.add(CREATE_PROJECT);
     NAMES_ALL.add(EMAIL_REVIEWERS);
     NAMES_ALL.add(FLUSH_CACHES);
+    NAMES_ALL.add(GENERATE_HTTP_PASSWORD);
     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/Permission.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
index 2379b4a..2d9965e 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());
 
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/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/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/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/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/webui/BranchWebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/BranchWebLink.java
new file mode 100644
index 0000000..bc7a1e5
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/BranchWebLink.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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;
+
+@ExtensionPoint
+public interface BranchWebLink extends WebLink {
+
+  /**
+   * URL to branch in external service.
+   *
+   * @param projectName Name of the project
+   * @param branchName Name of the branch
+   * @return url to branch in external service.
+   */
+  String getBranchUrl(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..ee3c62f
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/FileWebLink.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.webui;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+@ExtensionPoint
+public interface FileWebLink extends WebLink {
+
+  /**
+   * URL to file in external service.
+   *
+   * @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 url to project in external service.
+   */
+  String getFileUrl(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/WebLink.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebLink.java
index 19d9ab7..4065c68 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
@@ -15,10 +15,31 @@
 
 public interface WebLink {
 
+  public static class Target {
+    public final static String BLANK = "_blank";
+    public final static String SELF = "_self";
+    public final static String PARENT = "_parent";
+    public final static String TOP = "_top";
+  }
   /**
    * The link-name displayed in UI.
    *
-   * @return name of link.
+   * @return name of link or title of the link if image URL is available.
    */
   String getLinkName();
+
+  /**
+   * URL of image to be displayed
+   *
+   * @return URL to image for link or null for using a text-only link.
+   * Recommended image size is 16x16.
+   */
+  String getImageUrl();
+
+  /**
+   * Target window in which the link should be opened (e.g. "_blank", "_self".).
+   *
+   * @return link target, if null the link is opened in the current window
+   */
+  String getTarget();
 }
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..53db8ca
--- /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<String, String>();
+    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/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/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/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
index 2ad9a5e..cc8e997 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);
     }
@@ -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()) {
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 5b96ba0..bfe3d0b 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 = "";
@@ -434,6 +441,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;
@@ -764,14 +772,16 @@
       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);
           }
         }
       }
@@ -890,6 +900,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, "") {
@@ -914,6 +965,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/WebLinkInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/WebLinkInfo.java
index 64b9cb8..3ed91be 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
@@ -19,7 +19,9 @@
 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() {
   }
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/NewAgreementScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
index 275937e..c6da773 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;
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/UsernameField.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
index bf4d75c..945ae18 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,18 +106,14 @@
       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>() {
           public void onSuccess(final VoidResult result) {
@@ -131,8 +127,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 +136,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/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/GroupListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
index 6579f83..0685cb7 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
@@ -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/ProjectBranchesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
index 577bf1d..9a6dae4 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
@@ -396,6 +395,25 @@
         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())) {
+          Anchor a = new Anchor();
+          a.setHref(weblink.url());
+          if (weblink.target() != null && !weblink.target().isEmpty()) {
+            a.setTarget(weblink.target());
+          }
+          if (weblink.imageUrl() != null && !weblink.imageUrl().isEmpty()) {
+            Image img = new Image();
+            img.setAltText(weblink.name());
+            img.setUrl(weblink.imageUrl());
+            img.setTitle(weblink.name());
+            a.getElement().appendChild(img.getElement());
+          } else {
+            a.setText("(" + weblink.name() + ")");
+          }
+          actionsPanel.add(a);
+        }
+      }
       if (k.actions() != null) {
         k.actions().copyKeysIntoChildren("id");
         for (ActionInfo a : Natives.asList(k.actions().values())) {
@@ -416,9 +434,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..a0b1fc73 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;
@@ -157,6 +158,7 @@
     state.setEnabled(isOwner);
     submitType.setEnabled(isOwner);
     setEnabledForUseContentMerge();
+    newChangeForAllNotInTarget.setEnabled(isOwner);
     descTxt.setEnabled(isOwner);
     contributorAgreements.setEnabled(isOwner);
     signedOffBy.setEnabled(isOwner);
@@ -213,6 +215,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 +344,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 +554,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 +569,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 +600,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..af01ad3 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
@@ -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() {
@@ -239,8 +239,19 @@
 
           for (WebLinkInfo weblink : webLinks) {
             Anchor a = new Anchor();
-            a.setText("(" + weblink.name() + ")");
             a.setHref(weblink.url());
+            if (weblink.target() != null && !weblink.target().isEmpty()) {
+              a.setTarget(weblink.target());
+            }
+            if (weblink.imageUrl() != null && !weblink.imageUrl().isEmpty()) {
+              Image img = new Image();
+              img.setAltText(weblink.name());
+              img.setUrl(weblink.imageUrl());
+              img.setTitle(weblink.name());
+              a.getElement().appendChild(img.getElement());
+            } else {
+              a.setText("(" + weblink.name() + ")");
+            }
             p.add(a);
           }
         }
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/change/Actions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
index 906efca..4ee46e5 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,6 +184,15 @@
     return submit.isVisible() && submit.isEnabled();
   }
 
+  @UiHandler("followUp")
+  void onFollowUp(ClickEvent e) {
+    if (followUpAction == null) {
+      followUpAction = new FollowUpAction(followUp, project,
+          branch, key);
+    }
+    followUpAction.show();
+  }
+
   @UiHandler("abandon")
   void onAbandon(ClickEvent e) {
     if (abandonAction == null) {
@@ -164,6 +206,21 @@
     DraftActions.publish(changeId, revision);
   }
 
+  @UiHandler("deleteEdit")
+  void onDeleteEdit(ClickEvent e) {
+    EditActions.deleteEdit(changeId);
+  }
+
+  @UiHandler("publishEdit")
+  void onPublishEdit(ClickEvent e) {
+    EditActions.publishEdit(changeId);
+  }
+
+  @UiHandler("rebaseEdit")
+  void onRebaseEdit(ClickEvent e) {
+    EditActions.rebaseEdit(changeId);
+  }
+
   @UiHandler("deleteRevision")
   void onDeleteRevision(ClickEvent e) {
     DraftActions.delete(changeId, revision);
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..20eb9b0 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.replyBox(), 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")) {
@@ -520,6 +581,37 @@
     files.openAll();
   }
 
+  @UiHandler("editMode")
+  void onEditMode(ClickEvent e) {
+    fileTableMode = FileTable.Mode.EDIT;
+    refreshFileTable();
+    editMode.setVisible(false);
+    addFile.setVisible(true);
+    reviewMode.setVisible(true);
+  }
+
+  @UiHandler("reviewMode")
+  void onReviewMode(ClickEvent e) {
+    fileTableMode = FileTable.Mode.REVIEW;
+    refreshFileTable();
+    editMode.setVisible(true);
+    addFile.setVisible(false);
+    reviewMode.setVisible(false);
+  }
+
+  @UiHandler("addFile")
+  void onAddFile(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) {
     int n = history.getWidgetCount();
@@ -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()
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..b32a31b 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;
@@ -129,28 +126,43 @@
       RevisionInfo revInfo) {
     GitwebLink gw = Gerrit.getGitwebLink();
     if (gw != null && gw.canLink(revInfo)) {
-      addWebLink(gw.toRevision(change.project(), revision), gw.getLinkName());
+      addWebLink(gw.toRevision(change.project(), revision),
+          gw.getLinkName(), null, null);
     }
 
     JsArray<WebLinkInfo> links = revInfo.web_links();
     if (links != null) {
       for (WebLinkInfo link : Natives.asList(links)) {
-        addWebLink(link.url(), parenthesize(link.name()));
+        addWebLink(link.url(), parenthesize(link.name()), link.imageUrl(),
+            link.target());
       }
     }
   }
 
-  private void addWebLink(String href, String name) {
-    AnchorElement a = DOM.createAnchor().cast();
+  private void addWebLink(String href, String name, String imageUrl,
+      String target) {
+    Anchor a = new Anchor();
     a.setHref(href);
-    a.setInnerText(name);
-    webLinkCell.appendChild(a);
+    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);
+    }
+    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);
 
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/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/EditActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java
new file mode 100644
index 0000000..ec9a375
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.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.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>() {
+      public void onSuccess(JavaScriptObject result) {
+        Gerrit.display(PageLinks.toChange(id));
+      }
+
+      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..8d31478
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditFileAction.java
@@ -0,0 +1,82 @@
+//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;
+
+public class EditFileAction {
+ private final PatchSet.Id id;
+ private final String content;
+ private final String file;
+ private final String style;
+ private final Widget editMessageButton;
+ private final Widget relativeTo;
+
+ private EditFileBox editBox;
+ private PopupPanel popup;
+
+ public EditFileAction(
+     PatchSet.Id id,
+     String content,
+     String file,
+     String 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);
+   if (editMessageButton != null) {
+     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..cd5d5f1
--- /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(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(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..264465d 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,6 +55,7 @@
     this.revision = revision;
     this.originalMessage = msg.trim();
     initWidget(uiBinder.createAndBindUi(this));
+    message.getElement().setAttribute("wrap", "off");
     message.setText("");
     new TextBoxChangeListener(message) {
       public void onTextChanged(String newText) {
@@ -96,7 +97,7 @@
     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..8a61b15 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,42 @@
     String deltaColumn2();
     String inserted();
     String deleted();
+    String editButton();
+    String removeButton();
   }
 
+  public static enum Mode {
+    REVIEW,
+    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 +131,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 +189,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 +200,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 +322,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.replyBox(), 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());
     }
@@ -358,6 +466,7 @@
     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,13 +480,15 @@
         JsArray<FileInfo> list,
         Timestamp myLastReply,
         NativeMap<JsArray<CommentInfo>> comments,
-        NativeMap<JsArray<CommentInfo>> drafts) {
+        NativeMap<JsArray<CommentInfo>> drafts,
+        Mode mode) {
       this.table = new MyTable(map, list);
       this.list = list;
       this.myLastReply = myLastReply;
       this.comments = comments;
       this.drafts = drafts;
       this.hasUser = Gerrit.isSignedIn();
+      this.mode = mode;
       table.addStyleName(R.css().table());
     }
 
@@ -449,7 +560,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 +582,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 +612,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 +815,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..da41985
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FollowUpAction.java
@@ -0,0 +1,46 @@
+// 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;
+  }
+
+  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..bc84984
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
@@ -0,0 +1,272 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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(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(ClickEvent e) {
+    openForm.setVisible(true);
+    UIObject.setVisible(form, false);
+    hashtagTextBox.setFocus(false);
+  }
+
+  @UiHandler("add")
+  void onAdd(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>() {
+          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/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/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/RelatedChangesTab.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
index e1ce99d..4717ad0 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
@@ -311,7 +311,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.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/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/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/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/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/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/PatchTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java
index 425309d..4c46f4c 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)) {
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..917762e 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;
@@ -27,6 +28,8 @@
   public final native FileMeta meta_b() /*-{ return this.meta_b; }-*/;
   public final native JsArrayString diff_header() /*-{ return this.diff_header; }-*/;
   public final native JsArray<Region> content() /*-{ return this.content; }-*/;
+  public final native JsArray<WebLinkInfo> web_links_a() /*-{ return this.web_links_a; }-*/;
+  public final native JsArray<WebLinkInfo> web_links_b() /*-{ return this.web_links_b; }-*/;
 
   public final ChangeType change_type() {
     return ChangeType.valueOf(change_typeRaw());
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..5a1c54f 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
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.client.account.DiffPreferences;
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
@@ -79,6 +80,7 @@
   private SideBySide2 parent;
   private boolean header;
   private boolean headerVisible;
+  private boolean autoHideHeader;
   private boolean visibleA;
   private ChangeType changeType;
 
@@ -133,7 +135,11 @@
   }
 
   void setHeaderVisible(boolean show) {
-    headerVisible = show;
+    headerVisible = !autoHideHeader || show;
+    showHeader(headerVisible);
+  }
+
+  private void showHeader(boolean show) {
     UIObject.setVisible(patchSetNavRow, show);
     UIObject.setVisible(diffHeaderRow, show && header);
     if (show) {
@@ -144,6 +150,13 @@
     parent.resizeCodeMirror();
   }
 
+  void setAutoHideDiffHeader(boolean hide) {
+    autoHideHeader = hide;
+    if (!hide) {
+      showHeader(true);
+    }
+  }
+
   int getHeaderHeight() {
     int h = patchSetSelectBoxA.getOffsetHeight();
     if (header) {
@@ -156,10 +169,14 @@
     return changeType;
   }
 
-  void set(DiffPreferences prefs, JsArray<RevisionInfo> list, DiffInfo info) {
+  void set(DiffPreferences prefs, JsArray<RevisionInfo> list, DiffInfo info,
+      boolean editExists, int currentPatchSet) {
     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(),
+        Natives.asList(info.web_links_a()), editExists, currentPatchSet);
+    patchSetSelectBoxB.setUpPatchSetNav(list, info.meta_b(),
+        Natives.asList(info.web_links_b()), editExists, currentPatchSet);
 
     JsArrayString hdr = info.diff_header();
     if (hdr != null) {
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..212af9a 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()
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..786df47 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,12 @@
 
 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.ui.InlineHyperlink;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
@@ -25,6 +29,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 +39,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> {}
@@ -43,6 +51,7 @@
 
   interface BoxStyle extends CssResource {
     String selected();
+    String replyBox();
   }
 
   @UiField Image icon;
@@ -76,7 +85,8 @@
     this.path = path;
   }
 
-  void setUpPatchSetNav(JsArray<RevisionInfo> list, DiffInfo.FileMeta meta) {
+  void setUpPatchSetNav(JsArray<RevisionInfo> list, DiffInfo.FileMeta meta,
+      List<WebLinkInfo> webLinks, boolean editExists, int currentPatchSet) {
     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;
       }
     }
@@ -100,6 +110,68 @@
     if (meta != null && !Patch.COMMIT_MSG.equals(path)) {
       linkPanel.add(createDownloadLink());
     }
+    if (idActive != null && Gerrit.isSignedIn() && meta != null
+        && !Patch.COMMIT_MSG.equals(path)) {
+      if ((editExists && idActive.get() == 0)
+          || (!editExists && idActive.get() == currentPatchSet)) {
+        linkPanel.add(createEditIcon());
+      }
+    }
+    if (webLinks != null) {
+      for (WebLinkInfo weblink : webLinks) {
+        Anchor a = new Anchor();
+        a.setHref(weblink.url());
+        if (weblink.target() != null && !weblink.target().isEmpty()) {
+          a.setTarget(weblink.target());
+        }
+        if (weblink.imageUrl() != null && !weblink.imageUrl().isEmpty()) {
+          Image img = new Image();
+          img.setAltText(weblink.name());
+          img.setUrl(weblink.imageUrl());
+          img.setTitle(weblink.name());
+          a.getElement().appendChild(img.getElement());
+        } else {
+          a.setText("(" + weblink.name() + ")");
+        }
+        linkPanel.add(a);
+      }
+    }
+  }
+
+  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) {
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..eba362a 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%;
     }
@@ -55,6 +56,10 @@
     .iconCell {
       width: 16px;
     }
+    .replyBox {
+      background-color: trimColor;
+      z-index: 10;
+    }
   </ui:style>
   <g:HTMLPanel>
     <table class='{style.table}'>
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..0978d13 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());
@@ -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());
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/SideBySide2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
index 22df01f..2547a4f 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,15 @@
       @Override
       public void onSuccess(ChangeInfo info) {
         info.revisions().copyKeysIntoChildren("name");
+        if (edit != null) {
+          edit.set_name(edit.commit().commit());
+          info.set_edit(edit);
+          info.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
+        }
+        int currentPatchSet = info.revision(info.current_revision())._number();
         JsArray<RevisionInfo> list = info.revisions().values();
         RevisionInfo.sortRevisionInfoByNumber(list);
-        diffTable.set(prefs, list, diff);
+        diffTable.set(prefs, list, diff, edit != null, currentPatchSet);
         header.setChangeInfo(info);
       }}));
 
@@ -319,7 +340,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))
@@ -384,7 +405,8 @@
           public void run() {
             cm.execCommand("selectAll");
           }
-        }));
+        });
+    cm.addKeyMap(sbsKeyMap);
     if (prefs.renderEntireFile()) {
       cm.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
     }
@@ -396,6 +418,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 +539,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());
@@ -697,6 +746,10 @@
     });
   }
 
+  void setAutoHideDiffHeader(boolean hide) {
+    diffTable.setAutoHideDiffHeader(hide);
+  }
+
   private void render(DiffInfo diff) {
     header.setNoDiff(diff);
     chunkManager.render(diff);
@@ -798,11 +851,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));
           }
         });
       }
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/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/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/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/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/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/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/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 81f6274..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(),
@@ -66,13 +65,11 @@
       Modes.I.properties(),
       Modes.I.python(),
       Modes.I.r(),
-      Modes.I.rst(),
       Modes.I.ruby(),
       Modes.I.scheme(),
       Modes.I.shell(),
       Modes.I.smalltalk(),
       Modes.I.sql(),
-      Modes.I.stex(),
       Modes.I.velocity(),
       Modes.I.verilog(),
       Modes.I.xml(),
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 b794888..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();
@@ -49,13 +48,11 @@
   @Source("properties/properties.js") @DoNotEmbed DataResource properties();
   @Source("python/python.js") @DoNotEmbed DataResource python();
   @Source("r/r.js") @DoNotEmbed DataResource r();
-  @Source("rst/rst.js") @DoNotEmbed DataResource rst();
   @Source("ruby/ruby.js") @DoNotEmbed DataResource ruby();
   @Source("scheme/scheme.js") @DoNotEmbed DataResource scheme();
   @Source("shell/shell.js") @DoNotEmbed DataResource shell();
   @Source("smalltalk/smalltalk.js") @DoNotEmbed DataResource smalltalk();
   @Source("sql/sql.js") @DoNotEmbed DataResource sql();
-  @Source("stex/stex.js") @DoNotEmbed DataResource stex();
   @Source("tcl/tcl.js") @DoNotEmbed DataResource tcl();
   @Source("velocity/velocity.js") @DoNotEmbed DataResource velocity();
   @Source("verilog/verilog.js") @DoNotEmbed DataResource verilog();
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map b/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map
index 2a43b38..2bff364 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map
@@ -94,9 +94,6 @@
 r:
 text/r-src
 
-rst:
-text/x-rst
-
 ruby:
 text/x-ruby
 
@@ -119,9 +116,6 @@
 tcl:
 text/x-tcl
 
-stex:
-text/x-stex
-
 velocity:
 text/velocity
 
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/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 90%
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..4c93d26 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,10 +14,10 @@
 
 package com.google.gerrit.httpd;
 
-public class GerritUiOptions {
+public class GerritOptions {
   private final boolean headless;
 
-  public GerritUiOptions(boolean headless) {
+  public GerritOptions(boolean headless) {
     this.headless = headless;
   }
 
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/HttpLogoutServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
index 7e1aa28..bb9bac4 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,12 @@
 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;
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/RequestUtil.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestUtil.java
new file mode 100644
index 0000000..c47ca30
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestUtil.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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 javax.servlet.http.HttpServletRequest;
+
+/** Utilities for manipulating HTTP request objects. */
+public class RequestUtil {
+  /**
+   * @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-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..d14fa9a 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() {
@@ -110,7 +110,7 @@
         throw new ProvisionException("Unsupported loginType: " + authConfig.getAuthType());
     }
 
-    install(new UrlModule(urlConfig, uiOptions));
+    install(new UrlModule(urlConfig, options));
     install(new UiRpcModule());
     install(new GerritRequestModule());
     install(new GitOverHttpServlet.Module());
@@ -131,6 +131,8 @@
     bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
         HttpRemotePeerProvider.class).in(RequestScoped.class);
 
+    bind(ProxyProperties.class).toProvider(ProxyPropertiesProvider.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 c18a3b7..2a3a76b 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
@@ -16,7 +16,7 @@
 
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.extensions.registration.DynamicItem;
@@ -122,7 +122,7 @@
     if (res != null) {
       webSession.get().login(res, false);
       final StringBuilder rdr = new StringBuilder();
-      rdr.append(Objects.firstNonNull(
+      rdr.append(MoreObjects.firstNonNull(
           Strings.emptyToNull(req.getContextPath()),
           "/"));
       if (IS_DEV && req.getParameter("gwt.codesvr") != null) {
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/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..286f61b 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,6 +26,10 @@
 import com.google.common.collect.Maps;
 import com.google.common.net.HttpHeaders;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.httpd.RequestUtil;
+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;
@@ -205,7 +209,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 +256,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 +368,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 +441,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 +544,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 +564,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 +609,7 @@
     }
   }
 
-  private void sendJsPlugin(Plugin plugin, ResourceKey key,
+  private void sendJsPlugin(Plugin plugin, PluginResourceKey key,
       HttpServletRequest req, HttpServletResponse res) throws IOException {
     File pluginFile = plugin.getSrcFile();
     if (req.getRequestURI().endsWith(getJsPluginPath(plugin))
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceKey.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginResourceKey.java
similarity index 80%
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..4c3c515 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,19 @@
 
 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() {
+  public int weigh() {
     return resource.length() * 2;
   }
 
@@ -36,8 +37,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/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/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index b7528b5..979990a 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;
@@ -71,6 +74,7 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.httpd.RequestUtil;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.AnonymousUser;
@@ -78,7 +82,6 @@
 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.gson.ExclusionStrategy;
 import com.google.gson.FieldAttributes;
 import com.google.gson.FieldNamingPolicy;
@@ -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();
           }
@@ -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;
             }
@@ -340,12 +354,12 @@
       replyError(req, res, status = SC_CONFLICT, e.getMessage(), e.caching());
     } 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());
     } catch (ResourceNotFoundException e) {
       replyError(req, res, status = SC_NOT_FOUND, "Not found", e.caching());
     } catch (UnprocessableEntityException e) {
       replyError(req, res, status = 422,
-          Objects.firstNonNull(e.getMessage(), "Unprocessable Entity"), e.caching());
+          MoreObjects.firstNonNull(e.getMessage(), "Unprocessable Entity"), e.caching());
     } catch (AmbiguousViewException e) {
       replyError(req, res, status = SC_NOT_FOUND, e.getMessage());
     } catch (MalformedJsonException e) {
@@ -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();
     }
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..2457fd2 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;
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..9bcffa8 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,10 +110,13 @@
   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"));
     }
   }
 
@@ -198,9 +205,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());
         }
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/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..4180c70 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;
@@ -46,7 +47,6 @@
 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;
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/RequestUtilTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/RequestUtilTest.java
new file mode 100644
index 0000000..71a43bf
--- /dev/null
+++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/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.httpd;
+
+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-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 64c7ed5..2faece4 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
@@ -58,7 +58,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;
@@ -122,6 +121,8 @@
   private static final String ID_FIELD = ChangeField.LEGACY_ID.getName();
   private static final ImmutableSet<String> FIELDS = ImmutableSet.of(
       ADDED_FIELD, APPROVAL_FIELD, CHANGE_FIELD, DELETED_FIELD, ID_FIELD);
+  private static final Map<String, String> CUSTOM_CHAR_MAPPING = ImmutableMap.of(
+      "_", " ", ".", " ");
 
   private static final Map<Schema<ChangeData>, Version> LUCENE_VERSIONS;
   static {
@@ -135,6 +136,8 @@
     Version lucene46 = Version.LUCENE_46;
     @SuppressWarnings("deprecation")
     Version lucene47 = Version.LUCENE_47;
+    @SuppressWarnings("deprecation")
+    Version lucene48 = Version.LUCENE_48;
     for (Map.Entry<Integer, Schema<ChangeData>> e
         : ChangeSchemas.ALL.entrySet()) {
       if (e.getKey() <= 3) {
@@ -145,8 +148,10 @@
         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 {
-        versions.put(e.getValue(), Version.LUCENE_48);
+        versions.put(e.getValue(), Version.LUCENE_4_10_0);
       }
     }
     LUCENE_VERSIONS = versions.build();
@@ -173,8 +178,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(
@@ -237,9 +244,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);
 
     GerritIndexWriterConfig openConfig =
@@ -281,26 +288,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);
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..688835a 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
@@ -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..dc5e102 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,7 +56,6 @@
 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;
 
@@ -108,29 +108,18 @@
       final Provider<IdentifiedUser> iu,
       CanonicalWebUrl up,
       @GerritServerConfig final Config config, final AuthConfig ac,
-      final AccountManager am) throws ConsumerException, MalformedURLException {
+      final AccountManager am,
+      ProxyProperties proxyProperties)
+          throws ConsumerException, MalformedURLException {
 
-    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/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..3de5a2a 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',
@@ -33,65 +25,80 @@
   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:args4j',
+    '//lib:guava',
+    '//lib:gwtjsonrpc',
+    '//lib:gwtorm',
+    '//lib:h2',
     '//lib/commons:dbcp',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/jgit:jgit',
     '//lib/mina:sshd',
-    '//lib:args4j',
-    '//lib:guava',
-    '//lib:gwtjsonrpc',
-    '//lib:gwtorm',
     '//lib/log:api',
   ],
   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:client',
+    '//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',
@@ -127,6 +134,7 @@
   visibility = [
     '//:',
     '//gerrit-acceptance-tests/...',
+    '//gerrit-gwtdebug:gwtdebug',
     '//tools/eclipse:classpath',
     '//Documentation:licenses.txt',
   ],
@@ -136,8 +144,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..cc00294 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;
@@ -67,6 +67,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;
@@ -284,7 +286,7 @@
       initSshd();
     }
 
-    if (Objects.firstNonNull(httpd, true)) {
+    if (MoreObjects.firstNonNull(httpd, true)) {
       initHttpd();
     }
 
@@ -328,7 +330,7 @@
     modules.add(new PluginRestApiModule());
     modules.add(new RestCacheAdminModule());
     modules.add(createIndexModule());
-    if (Objects.firstNonNull(httpd, true)) {
+    if (MoreObjects.firstNonNull(httpd, true)) {
       modules.add(new CanonicalWebUrlModule() {
         @Override
         protected Class<? extends Provider<String>> provider() {
@@ -354,7 +356,10 @@
     modules.add(new AbstractModule() {
       @Override
       protected void configure() {
-        bind(GerritUiOptions.class).toInstance(new GerritUiOptions(headless));
+        bind(GerritOptions.class).toInstance(new GerritOptions(headless));
+        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..cf4fdbf 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,50 +15,27 @@
 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.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.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;
@@ -67,28 +44,13 @@
 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.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;
@@ -96,17 +58,12 @@
 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();
 
@@ -127,7 +84,6 @@
   private boolean dryRun;
 
   private Injector dbInjector;
-  private Config cfg;
   private Injector sysInjector;
   private ChangeIndex index;
 
@@ -135,10 +91,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 +122,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,39 +143,10 @@
         throw new IllegalStateException("unsupported index.type");
     }
     modules.add(changeIndexModule);
-    modules.add(new ReviewDbModule());
-    modules.add(new FactoryModule() {
-      @SuppressWarnings("rawtypes")
+    modules.add(dbInjector.getInstance(BatchProgramModule.class));
+    modules.add(new AbstractModule() {
       @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 {
@@ -254,61 +168,16 @@
     }
   }
 
-  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());
+      install(new BatchGitModule());
     }
 
     @Provides
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..de0b75a 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,7 +27,7 @@
   @Inject
   AllUsersNameOnInitProvider(Section.Factory sections) {
     String n = sections.get("gerrit", null).get("allUsers");
-    name = Objects.firstNonNull(
+    name = MoreObjects.firstNonNull(
         Strings.emptyToNull(n), AllUsersNameProvider.DEFAULT);
   }
 
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 93%
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..dc881c6 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;
@@ -166,8 +163,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 +191,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 +226,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..83c4510 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;
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..4c026ac 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;
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..0fda842 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;
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..668b307 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;
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..264031d 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;
 
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..9370a14 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;
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..b522d07 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;
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..5f9aade 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;
 
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..e9cc1ed 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;
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..4dc05c5 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;
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..7b64308 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;
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 97%
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..dfb2f38 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,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.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.SitePaths;
@@ -93,7 +93,7 @@
     throw new UnsupportedOperationException();
   }
 
-  void save(String message) throws IOException {
+  public void save(String message) throws IOException {
     save(new PersonIdent("Gerrit Initialization", "init@gerrit"), message);
   }
 
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 90%
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..956f459 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,7 +26,7 @@
   @Inject
   AllProjectsNameOnInitProvider(Section.Factory sections) {
     String n = sections.get("gerrit", null).get("allProjects");
-    name = Objects.firstNonNull(
+    name = MoreObjects.firstNonNull(
         Strings.emptyToNull(n), AllProjectsNameProvider.DEFAULT);
   }
 
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..8f9f1f4
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.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.TagCache;
+import com.google.gerrit.server.group.GroupModule;
+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(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());
+    install(ChangeCache.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/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..8569112 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;
 
@@ -47,34 +45,9 @@
 
   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..7147b1c 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',
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-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/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-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..6bdd4b0 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
@@ -92,6 +92,7 @@
     p.setContext(DEFAULT_CONTEXT);
     p.setManualReview(false);
     p.setHideEmptyPane(false);
+    p.setAutoHideDiffTableHeader(true);
     return p;
   }
 
@@ -156,6 +157,9 @@
   @Column(id = 20)
   protected boolean hideEmptyPane;
 
+  @Column(id = 21)
+  protected boolean autoHideDiffTableHeader;
+
   protected AccountDiffPreference() {
   }
 
@@ -183,6 +187,7 @@
     this.hideLineNumbers = p.hideLineNumbers;
     this.renderEntireFile = p.renderEntireFile;
     this.hideEmptyPane = p.hideEmptyPane;
+    this.autoHideDiffTableHeader = p.autoHideDiffTableHeader;
   }
 
   public Account.Id getAccountId() {
@@ -343,4 +348,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/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index dbce048..94d2f64 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;
     }
   }
 
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/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/PatchSetAncestorAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
index ac2c849..47d8971 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;
@@ -33,7 +33,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/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 139d6cf..3668dbd 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..973149c 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;
@@ -197,6 +200,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. */
@@ -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()
@@ -610,6 +617,52 @@
       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;
+    }
+
+    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);
+    }
+
     public void doClaSignupHook(Account account, ContributorAgreement cla) {
       if (account != null) {
         final List<String> args = new ArrayList<>();
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..65c1c20 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;
 
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..b4166ca 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();
@@ -269,7 +269,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 +283,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 +292,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..a5ed1d8 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.collect.ImmutableList;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.errors.EmailException;
+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;
@@ -46,7 +50,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;
@@ -508,19 +511,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/PatchLineCommentsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
index 4918546..e8420b9 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<PatchLineComment>();
+    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/WebLinks.java b/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
index fe07100..d2e3e49 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
@@ -17,27 +17,53 @@
 import com.google.common.collect.Lists;
 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.inject.Inject;
+import com.google.inject.Singleton;
 
 import java.util.List;
 
+@Singleton
 public class WebLinks {
 
   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)));
+          webLink.getImageUrl(),
+          webLink.getPatchSetUrl(project, commit),
+          webLink.getTarget()));
+    }
+    return links;
+  }
+
+  public Iterable<WebLinkInfo> getFileLinks(String project, String revision,
+      String file) {
+    List<WebLinkInfo> links = Lists.newArrayList();
+    for (FileWebLink webLink : fileLinks) {
+      links.add(new WebLinkInfo(webLink.getLinkName(),
+          webLink.getImageUrl(),
+          webLink.getFileUrl(project, revision, file),
+          webLink.getTarget()));
     }
     return links;
   }
@@ -46,7 +72,20 @@
     List<WebLinkInfo> links = Lists.newArrayList();
     for (ProjectWebLink webLink : projectLinks) {
       links.add(new WebLinkInfo(webLink.getLinkName(),
-          webLink.getProjectUrl(project)));
+          webLink.getImageUrl(),
+          webLink.getProjectUrl(project),
+          webLink.getTarget()));
+    }
+    return links;
+  }
+
+  public Iterable<WebLinkInfo> getBranchLinks(String project, String branch) {
+    List<WebLinkInfo> links = Lists.newArrayList();
+    for (BranchWebLink webLink : branchLinks) {
+      links.add(new WebLinkInfo(webLink.getLinkName(),
+          webLink.getImageUrl(),
+          webLink.getBranchUrl(project, branch),
+          webLink.getTarget()));
     }
     return links;
   }
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/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
index a521840..c64eba7 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;
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..be85a26 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) throws OrmException {
     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/AddSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
index 3c21d17..58c674c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
@@ -63,7 +63,7 @@
   public Response<SshKeyInfo> apply(AccountResource rsrc, Input input)
       throws AuthException, BadRequestException, OrmException, IOException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
+        && !self.get().getCapabilities().canModifyAccount()) {
       throw new AuthException("not allowed to add SSH keys");
     }
     return apply(rsrc.getUser(), input);
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..631256a 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)
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/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/GetCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
index 47047ed..4ac65ec 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
@@ -20,7 +20,9 @@
 import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
 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.GENERATE_HTTP_PASSWORD;
 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;
@@ -113,7 +115,9 @@
     have.put(CREATE_PROJECT, cc.canCreateProject());
     have.put(EMAIL_REVIEWERS, cc.canEmailReviewers());
     have.put(FLUSH_CACHES, cc.canFlushCaches());
+    have.put(GENERATE_HTTP_PASSWORD, cc.canGenerateHttpPassword());
     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/GetHttpPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java
index c49ab98..e080015 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java
@@ -36,7 +36,7 @@
   public String apply(AccountResource rsrc) throws AuthException,
       ResourceNotFoundException {
     if (self.get() != rsrc.getUser()
-        && !self.get().getCapabilities().canAdministrateServer()) {
+        && !self.get().getCapabilities().canGenerateHttpPassword()) {
       throw new AuthException("not allowed to get http password");
     }
     AccountState s = rsrc.getUser().state();
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/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..93b35c6 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
@@ -86,14 +86,14 @@
 
     } else if (input.httpPassword == null) {
       if (self.get() != rsrc.getUser()
-          && !self.get().getCapabilities().canAdministrateServer()) {
+          && !self.get().getCapabilities().canGenerateHttpPassword()) {
         throw new AuthException("not allowed to clear HTTP password");
       }
       newPassword = null;
     } else {
-      if (!self.get().getCapabilities().canAdministrateServer()) {
+      if (!self.get().getCapabilities().canGenerateHttpPassword()) {
         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..cc61695 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) {
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/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..84d0e40
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
@@ -0,0 +1,437 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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,
+        InvalidChangeOperationException {
+      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, InvalidChangeOperationException {
+      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..184f1a8 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,15 @@
 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.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;
@@ -66,6 +70,8 @@
   private final ChangeMessagesUtil cmUtil;
   private final MergeabilityChecker mergeabilityChecker;
   private final CreateChangeSender.Factory createChangeSenderFactory;
+  private final HashtagsUtil hashtagsUtil;
+  private final AccountCache accountCache;
 
   private final RefControl refControl;
   private final Change change;
@@ -77,6 +83,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;
 
@@ -90,6 +97,8 @@
       ChangeMessagesUtil cmUtil,
       MergeabilityChecker mergeabilityChecker,
       CreateChangeSender.Factory createChangeSenderFactory,
+      HashtagsUtil hashtagsUtil,
+      AccountCache accountCache,
       @Assisted RefControl refControl,
       @Assisted Change change,
       @Assisted RevCommit commit) {
@@ -101,12 +110,15 @@
     this.cmUtil = cmUtil;
     this.mergeabilityChecker = mergeabilityChecker;
     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 +157,11 @@
     return this;
   }
 
+  public ChangeInserter setHashtags(Set<String> hashtags) {
+    this.hashtags = hashtags;
+    return this;
+  }
+
   public ChangeInserter setRunHooks(boolean runHooks) {
     this.runHooks = runHooks;
     return this;
@@ -191,7 +208,19 @@
     } finally {
       db.rollback();
     }
+
     update.commit();
+
+    if (hashtags != null && hashtags.size() > 0) {
+      try {
+        HashtagsInput input = new HashtagsInput();
+        input.add = hashtags;
+        hashtagsUtil.setHashtags(ctl, input, false, false);
+      } catch (ValidationException | AuthException e) {
+        log.error("Cannot add hashtags to change " + change.getId(), e);
+      }
+    }
+
     CheckedFuture<?, IOException> f = mergeabilityChecker.newCheck()
         .addChange(change)
         .reindex()
@@ -221,6 +250,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..e6b36e5 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,7 +31,7 @@
 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.HashBasedTable;
 import com.google.common.collect.HashMultimap;
@@ -62,6 +62,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 +78,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;
@@ -124,9 +126,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;
 
@@ -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);
   }
 
@@ -266,6 +271,7 @@
     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);
     ChangedLines changedLines = cd.changedLines();
@@ -326,6 +332,14 @@
           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;
   }
@@ -679,7 +693,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);
@@ -821,16 +836,15 @@
         && userProvider.get().isIdentifiedUser()) {
       IdentifiedUser user = (IdentifiedUser)userProvider.get();
       out.hasDraftComments =
-          db.get().patchComments()
-              .draftByPatchSetAuthor(in.getId(), user.getAccountId())
-              .iterator().hasNext()
+          plcUtil.draftByPatchSetAuthor(db.get(), in.getId(),
+              user.getAccountId(), ctl.getNotes()).iterator().hasNext()
           ? true
           : null;
     }
 
     if (has(WEB_LINKS)) {
       out.webLinks = Lists.newArrayList();
-      for (WebLinkInfo link : webLinks.get().getPatchSetLinks(
+      for (WebLinkInfo link : webLinks.getPatchSetLinks(
           project, in.getRevision().get())) {
         out.webLinks.add(link);
       }
@@ -880,21 +894,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 +937,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/ChangeKindCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index 8e2b6ac..82e0c91 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -218,7 +218,7 @@
         // having the same tree as would exist when the prior commit is
         // cherry-picked onto the next commit's new first parent.
         ThreeWayMerger merger = MergeUtil.newThreeWayMerger(
-            key.repo, MergeUtil.createDryRunInserter(), key.strategyName);
+            key.repo, MergeUtil.createDryRunInserter(key.repo), key.strategyName);
         merger.setBase(prior.getParent(0));
         if (merger.merge(next.getParent(0), prior)
             && merger.getResultTreeId().equals(next.getTree())) {
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..7cc3027 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);
     }
@@ -101,39 +99,6 @@
     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..bd1d1bd 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,6 +14,7 @@
 
 package com.google.gerrit.server.change;
 
+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;
@@ -27,7 +28,9 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeConflictException;
 import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.MergeIdenticalTreeException;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidators;
@@ -37,7 +40,6 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.ssh.NoSshInfo;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -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()) {
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/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..de7669f 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;
@@ -30,7 +31,6 @@
 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;
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..9efe8dc 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.base.Strings;
 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,14 +37,16 @@
 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;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.diff.ReplaceEdit;
 import org.kohsuke.args4j.CmdLineException;
@@ -53,6 +58,8 @@
 import org.kohsuke.args4j.spi.Parameters;
 import org.kohsuke.args4j.spi.Setter;
 
+import java.io.IOException;
+import java.util.ArrayList;
 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.webLinksA =
+            getFileWebLinks(state.getProject(), rev, result.metaA.name);
       }
 
       if (ps.getDisplayMethodB() != DisplayMethod.NONE) {
@@ -149,6 +169,9 @@
         result.metaB.name = ps.getNewName();
         setContentType(result.metaB, state, ps.getFileModeB(), ps.getMimeTypeB());
         result.metaB.lines = ps.getB().size();
+        result.webLinksB = getFileWebLinks(state.getProject(),
+            resource.getRevision().getPatchSet().getRefName(),
+            result.metaB.name);
       }
 
       if (intraline) {
@@ -178,6 +201,18 @@
     }
   }
 
+  private List<WebLinkInfo> getFileWebLinks(Project project, String rev,
+      String file) {
+    List<WebLinkInfo> fileWebLinks = new ArrayList<>();
+    for (WebLinkInfo link : webLinks.getFileLinks(project.getName(),
+        rev, file)) {
+      if (!Strings.isNullOrEmpty(link.name) && !Strings.isNullOrEmpty(link.url)) {
+        fileWebLinks.add(link);
+      }
+    }
+    return fileWebLinks;
+  }
+
   static class Result {
     FileMeta metaA;
     FileMeta metaB;
@@ -185,6 +220,8 @@
     ChangeType changeType;
     List<String> diffHeader;
     List<ContentEntry> content;
+    List<WebLinkInfo> webLinksA;
+    List<WebLinkInfo> webLinksB;
   }
 
   static class FileMeta {
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..077ec6f1 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..f7d7125
--- /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<String>(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..146ded8 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
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/MergeabilityCheckQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCheckQueue.java
index 6598238..ae44ea2 100644
--- 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
@@ -16,12 +16,11 @@
 
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.inject.Singleton;
 
 import java.util.Collection;
 import java.util.Set;
 
-import javax.inject.Singleton;
-
 @Singleton
 class MergeabilityCheckQueue {
   private final Set<Change.Id> pending = Sets.newHashSet();
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..d5d1f26 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,6 +34,7 @@
 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.mail.ReplacePatchSetSender;
@@ -44,7 +46,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 +54,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;
@@ -356,7 +358,7 @@
     }
   }
 
-  private void validate() throws InvalidChangeOperationException {
+  private void validate() throws InvalidChangeOperationException, IOException {
     CommitValidators cv =
         commitValidatorsFactory.create(ctl.getRefControl(), sshInfo, git);
 
@@ -372,7 +374,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..9714174 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) {
@@ -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/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/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/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/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..08e60ee 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,15 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.edit.ChangeEdit;
+import com.google.gerrit.server.edit.ChangeEditUtil;
+import com.google.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;
 import java.util.Collections;
 import java.util.List;
 
@@ -36,12 +45,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 +77,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 +102,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 +128,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 | InvalidChangeOperationException 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..6094bf7 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,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;
@@ -139,10 +139,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));
   }
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..72b3de5 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
@@ -18,7 +18,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.base.Throwables;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
@@ -86,7 +86,7 @@
     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(),
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..67b1b76 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
@@ -16,7 +16,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
 import com.google.gerrit.extensions.common.SubmitType;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -66,7 +66,7 @@
     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(),
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..1e9f94a 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;
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..3805a0e 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
@@ -29,7 +29,9 @@
   public String createProject;
   public String emailReviewers;
   public String flushCaches;
+  public String generateHttpPassword;
   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/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..8e06229 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;
@@ -89,6 +89,8 @@
 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;
@@ -126,6 +128,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;
@@ -232,7 +235,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);
@@ -267,9 +271,11 @@
         .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 +284,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 +294,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/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/contact/EncryptedContactStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java
index 8c1fdb6..aca91ab 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;
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..276eba6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditJson.java
@@ -0,0 +1,141 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.edit;
+
+import com.google.common.collect.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.io.IOException;
+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)
+      throws IOException {
+    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) throws IOException {
+    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..91bf65d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -0,0 +1,416 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.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<ReviewDb> dbProvider,
+      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, repo, 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, Repository repo, RevWalk rw,
+      ObjectInserter ins, RevCommit prevEdit, ObjectReader reader,
+      String fileName, byte[] content, RevCommit base)
+      throws IOException, InvalidChangeOperationException {
+    DirCache newTree = createTree(reader, prevEdit);
+    editTree(
+        op,
+        repo,
+        rw,
+        base,
+        newTree.editor(),
+        ins,
+        fileName,
+        content);
+    return newTree.writeTree(ins);
+  }
+
+  private static void editTree(TreeOperation op, Repository repo, 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, repo, rw, base, path, ins, content));
+        break;
+    }
+    dce.finish();
+  }
+
+  private static PathEdit getPathEdit(TreeOperation op, Repository repo, 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(repo, 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(Repository repo,
+      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..5abb2e2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -0,0 +1,295 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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, InvalidChangeOperationException {
+    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(repo, 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, InvalidChangeOperationException {
+    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(Repository repo, 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/HashtagsChangedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/HashtagsChangedEvent.java
new file mode 100644
index 0000000..4b4bbba
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/HashtagsChangedEvent.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.events;
+
+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;
+}
\ No newline at end of file
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..d26632b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefOperationReceivedEvent.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.server.events;
+
+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;
+}
\ No newline at end of file
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..b97ddb5 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,8 +84,8 @@
       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,
@@ -99,30 +94,33 @@
       MergeException, 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());
             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..c1afb6b 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);
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..d134427 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);
   }
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/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..1844292 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,30 @@
     return basePath;
   }
 
-  private File gitDirOf(Project.NameKey name) {
-    return new File(getBasePath(), name.get());
+  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 +187,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 +202,22 @@
     }
   }
 
-  public Repository createRepository(final Project.NameKey name)
+  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 +232,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 +255,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 {
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..a01ca3f 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;
@@ -64,7 +65,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 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 +73,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;
@@ -389,12 +388,7 @@
       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");
@@ -663,6 +657,14 @@
     return account;
   }
 
+  private String getByAccountName(CodeReviewCommit codeReviewCommit) {
+    Account account = getAccount(codeReviewCommit);
+    if (account != null && account.getFullName() != null) {
+      return " by " + account.getFullName();
+    }
+    return "";
+  }
+
   private void updateChangeStatus(final List<Change> submitted) throws NoSuchChangeException {
     for (final Change c : submitted) {
       final CodeReviewCommit commit = commits.get(c.getId());
@@ -679,12 +681,13 @@
       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:
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 b4a1f0c..5824c6f 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");
     }
   }
 
@@ -393,7 +401,7 @@
       return false;
     }
 
-    final ThreeWayMerger m = newThreeWayMerger(repo, createDryRunInserter());
+    ThreeWayMerger m = newThreeWayMerger(repo, createDryRunInserter(repo));
     try {
       return m.merge(new AnyObjectId[] {mergeTip, toMerge});
     } catch (LargeObjectException e) {
@@ -442,8 +450,7 @@
       // that on the current merge tip.
       //
       try {
-        final ThreeWayMerger m =
-            newThreeWayMerger(repo, createDryRunInserter());
+        ThreeWayMerger m = newThreeWayMerger(repo, createDryRunInserter(repo));
         m.setBase(toMerge.getParent(0));
         return m.merge(mergeTip, toMerge);
       } catch (IOException e) {
@@ -470,12 +477,12 @@
     }
   }
 
-  public static ObjectInserter createDryRunInserter() {
-    return new ObjectInserter() {
+  public static ObjectInserter createDryRunInserter(Repository db) {
+    final ObjectInserter delegate = db.newObjectInserter();
+    return new ObjectInserter.Filter() {
       @Override
-      public ObjectId insert(int objectType, long length, InputStream in)
-          throws IOException {
-        return idFor(objectType, length, in);
+      protected ObjectInserter delegate() {
+        return delegate;
       }
 
       @Override
@@ -487,11 +494,6 @@
       public void flush() throws IOException {
         // Do nothing.
       }
-
-      @Override
-      public void release() {
-        // Do nothing.
-      }
     };
   }
 
@@ -611,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(
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..5e4ffab 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,43 @@
 
     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)
+        throws RepositoryNotFoundException, IOException {
+      MetaDataUpdate md = factory.create(name, repository, batch);
       md.getCommitBuilder().setAuthor(createPersonIdent(user));
       md.getCommitBuilder().setCommitter(serverIdent);
       return md;
@@ -86,7 +125,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 +140,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 +181,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..8b902bd 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;
@@ -115,6 +115,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";
@@ -220,6 +222,10 @@
     this.projectName = projectName;
   }
 
+  public Project.NameKey getName() {
+    return projectName;
+  }
+
   public Project getProject() {
     return project;
   }
@@ -415,6 +421,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));
@@ -673,7 +680,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);
@@ -814,6 +821,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);
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..de0ab65 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;
@@ -25,6 +26,7 @@
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_MISSING_OBJECT;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE;
 
 import com.google.common.base.Function;
 import com.google.common.base.Joiner;
@@ -33,6 +35,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.BiMap;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.HashBiMap;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Iterables;
@@ -40,6 +43,7 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Ordering;
 import com.google.common.collect.SetMultimap;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.CheckedFuture;
@@ -49,6 +53,7 @@
 import com.google.gerrit.common.ChangeHookRunner.HookResult;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
@@ -97,7 +102,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 +117,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;
@@ -309,6 +315,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 +332,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();
@@ -375,7 +383,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;
@@ -415,17 +424,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);
@@ -588,31 +599,24 @@
     for (final ReceiveCommand c : commands) {
         if (c.getResult() == OK) {
           try {
-            switch (c.getType()) {
-              case CREATE:
-                if (isHead(c) || isConfig(c)) {
-                  autoCloseChanges(c);
-                }
-                break;
-
-              case UPDATE: // otherwise known as a fast-forward
+            if (c.getType() == 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_NONFASTFORWARD:
-                if (isHead(c) || isConfig(c)) {
+            if (isHead(c) || isConfig(c)) {
+              switch (c.getType()) {
+                case CREATE:
+                case UPDATE:
+                case UPDATE_NONFASTFORWARD:
                   autoCloseChanges(c);
-                }
-                break;
+                  break;
 
-              case DELETE:
-                break;
+                case DELETE:
+                  break;
+              }
             }
 
             if (isConfig(c)) {
@@ -643,7 +647,10 @@
     closeProgress.end();
     commandProgress.end();
     progress.end();
+    reportMessages();
+  }
 
+  private void reportMessages() {
     Iterable<CreateRequest> created =
         Iterables.filter(newChanges, new Predicate<CreateRequest>() {
           @Override
@@ -660,15 +667,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 +1005,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 +1110,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 +1125,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 +1141,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 +1154,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 +1188,10 @@
       return new MailRecipients(reviewer, cc);
     }
 
+    Set<String> getHashtags() {
+      return hashtags;
+    }
+
     Map<String, Short> getLabels() {
       return labels;
     }
@@ -1213,7 +1251,7 @@
       return;
     }
 
-    magicBranch = new MagicBranchInput(cmd, labelTypes);
+    magicBranch = new MagicBranchInput(cmd, labelTypes, notesMigration);
     magicBranch.reviewer.addAll(reviewersFromCommandLine);
     magicBranch.cc.addAll(ccFromCommandLine);
 
@@ -1280,6 +1318,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 +1355,18 @@
           return;
         }
       }
+    } else if (newChangeForAllNotInTarget) {
+      String destBranch = magicBranch.dest.get();
+      try {
+        ObjectId baseHead = repo.getRef(destBranch).getObjectId();
+        magicBranch.baseCommit =
+            Collections.singletonList(walk.parseCommit(baseHead));
+      } catch (IOException ex) {
+        log.warn(String.format("Project %s cannot read %s", project.getName(),
+            destBranch), ex);
+        reject(cmd, "internal server error");
+        return;
+      }
     }
 
     // Validate that the new commits are connected with the target
@@ -1308,7 +1375,6 @@
     // commits and the target branch head.
     //
     try {
-      final RevCommit tip = walk.parseCommit(magicBranch.cmd.getNewId());
       Ref targetRef = rp.getAdvertisedRefs().get(magicBranch.ctl.getRefName());
       if (targetRef == null || targetRef.getObjectId() == null) {
         // The destination branch does not yet exist. Assume the
@@ -1431,6 +1497,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 +1515,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 +1538,12 @@
 
         changeKey = new Change.Key(idStr);
         pending.add(new ChangeLookup(c, changeKey));
+        if (maxBatchChanges != 0 && pending.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) {
@@ -1622,6 +1702,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 +2061,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());
@@ -2125,18 +2215,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) {
@@ -2268,7 +2365,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());
@@ -2281,33 +2379,36 @@
   private void autoCloseChanges(final ReceiveCommand cmd) throws NoSuchChangeException {
     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.Id> 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) {
+            byKey.remove(closedChange);
           }
         }
 
-        rw.parseBody(c);
         for (final String changeId : c.getFooterLines(CHANGE_ID)) {
+          if (byKey == null) {
+            byKey = openChangesByKey(branch);
+          }
+
           final Change.Id onto = byKey.get(new Change.Key(changeId.trim()));
           if (onto != null) {
             final ReplaceRequest req = new ReplaceRequest(onto, c, cmd, false);
@@ -2328,18 +2429,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) {
@@ -2388,10 +2483,8 @@
   private SetMultimap<ObjectId, Ref> changeRefsById() throws IOException {
     if (refsById == null) {
       refsById =  HashMultimap.create();
-      for (Ref r : repo.getRefDatabase().getRefs(REFS_CHANGES).values()) {
-        if (PatchSet.isRef(r.getName())) {
-          refsById.put(r.getObjectId(), r);
-        }
+      for (Ref r : refsByChange().values()) {
+        refsById.put(r.getObjectId(), r);
       }
     }
     return refsById;
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/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/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/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..dcc579b 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;
@@ -397,7 +397,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/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..cad5174 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<>();
 
@@ -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<>();
@@ -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..361a773 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,7 +99,6 @@
 
     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();
 
@@ -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..aaeffd9 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);
@@ -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..74d5946 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
@@ -109,27 +109,10 @@
     return groups;
   }
 
-  private void writeAudits(final List<AccountGroupById> toBeRemoved)
+  private void writeAudits(final List<AccountGroupById> toRemoved)
       throws OrmException {
     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..605933b 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
@@ -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/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/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/ChangeBatchIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java
index 5ee240b..9a992cf 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/ChangeBatchIndexer.java
@@ -32,7 +32,9 @@
 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;
@@ -112,6 +115,7 @@
   private final ListeningExecutorService executor;
   private final ChangeIndexer.Factory indexerFactory;
   private final MergeabilityChecker mergeabilityChecker;
+  private final ThreeWayMergeStrategy mergeStrategy;
 
   @Inject
   ChangeBatchIndexer(SchemaFactory<ReviewDb> schemaFactory,
@@ -119,6 +123,7 @@
       GitRepositoryManager repoManager,
       @IndexExecutor ListeningExecutorService executor,
       ChangeIndexer.Factory indexerFactory,
+      @GerritServerConfig Config config,
       @Nullable MergeabilityChecker mergeabilityChecker) {
     this.schemaFactory = schemaFactory;
     this.changeDataFactory = changeDataFactory;
@@ -126,6 +131,7 @@
     this.executor = executor;
     this.indexerFactory = indexerFactory;
     this.mergeabilityChecker = mergeabilityChecker;
+    this.mergeStrategy = MergeUtil.getMergeStrategy(config);
   }
 
   public Result indexAll(ChangeIndex index, Iterable<Project.NameKey> projects,
@@ -162,13 +168,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 +195,7 @@
           fail(project, e);
           throw e;
         }
-      }, MoreExecutors.sameThreadExecutor());
+      }, MoreExecutors.directExecutor());
     }
 
     try {
@@ -239,8 +245,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 +270,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 +280,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 +387,7 @@
           walk.parseBody(a);
           return walk.parseTree(a.getTree());
         case 2:
-          return PatchListLoader.automerge(repo, walk, b);
+          return PatchListLoader.automerge(repo, walk, b, mergeStrategy);
         default:
           return null;
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
index 41dfba5..01db36f 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()) {
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/ChangeSchemas.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
index 8bb8f0b..031e741 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
@@ -243,7 +243,35 @@
         ChangeField.DELETED,
         ChangeField.DELTA);
 
+  // For upgrade to Lucene 4.10.0 index format only.
+  static final Schema<ChangeData> V12 = release(V11.getFields().values());
 
+  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.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/IndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
index 67d0fef..3aeeef2 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
@@ -106,7 +106,7 @@
         threads = config.getInt("index", null, "threads", 0);
       }
       if (threads <= 0) {
-        return MoreExecutors.sameThreadExecutor();
+        return MoreExecutors.newDirectExecutorService();
       }
       return MoreExecutors.listeningDecorator(
           workQueue.createQueue(threads, "index"));
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/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/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index 2beb49f..5f2ffcb 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)
@@ -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/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/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..eb7f6c4
--- /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, OrmException {
+    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..d89651b 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,32 @@
 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
@@ -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,6 +298,7 @@
         reviewers.put(e.getValue(), e.getKey());
       }
       this.reviewers = reviewers.build();
+      this.allPastReviewers = ImmutableList.copyOf(parser.allPastReviewers);
 
       submitRecords = ImmutableList.copyOf(parser.submitRecords);
     } catch (ParseException e1) {
@@ -579,13 +309,15 @@
     }
   }
 
-  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 +326,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..aa16562
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -0,0 +1,437 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.text.ParseException;
+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, ParseException {
+    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, IOException {
+    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, ParseException {
+    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..3eea41d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
@@ -0,0 +1,337 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.PatchLineCommentsUtil;
+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,
+      PatchLineCommentsUtil plcUtil,
+      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..5647639 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,17 +33,20 @@
 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.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.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
@@ -58,14 +63,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 +95,49 @@
   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,
+      DraftCommentNotes.Factory draftNotesFactory,
+      Provider<AllUsersName> allUsers,
+      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, draftNotesFactory, allUsers, draftUpdateFactory,
         projectCache, ctl, serverIdent.getWhen(), commentsUtil);
   }
 
   @AssistedInject
   private ChangeUpdate(
       @GerritPersonIdent PersonIdent serverIdent,
+      @AnonymousCowardName String anonymousCowardName,
       GitRepositoryManager repoManager,
       NotesMigration migration,
       AccountCache accountCache,
       MetaDataUpdate.User updateFactory,
+      DraftCommentNotes.Factory draftNotesFactory,
+      Provider<AllUsersName> allUsers,
+      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, draftNotesFactory, allUsers, draftUpdateFactory, ctl,
         when,
         projectCache.get(getProjectName(ctl)).getLabelTypes().nameComparator(),
         commentsUtil);
@@ -128,15 +150,21 @@
   @AssistedInject
   private ChangeUpdate(
       @GerritPersonIdent PersonIdent serverIdent,
+      @AnonymousCowardName String anonymousCowardName,
       GitRepositoryManager repoManager,
       NotesMigration migration,
       AccountCache accountCache,
       MetaDataUpdate.User updateFactory,
+      DraftCommentNotes.Factory draftNotesFactory,
+      Provider<AllUsersName> allUsers,
+      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 +202,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) throws OrmException {
+    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) throws OrmException {
+    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 +395,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 +409,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 +454,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 +511,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..c2e5dfc 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,15 @@
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.config.AnonymousCowardName;
 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.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 +51,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 +72,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 +83,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 +95,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();
@@ -232,6 +239,7 @@
 
     if (note[ptr.value] == '\n') {
       range.setEndLine(startLine);
+      ptr.value += 1;
       return range;
     } else if (note[ptr.value] == ':') {
       range.setStartLine(startLine);
@@ -368,7 +376,7 @@
 
   private PersonIdent newIdent(Account author, Date when) {
     return new PersonIdent(
-        author.getFullName(),
+        new AccountInfo(author).getName(anonymousCowardName),
         author.getId().get() + "@" + GERRIT_PLACEHOLDER_HOST,
         when, serverIdent.getTimeZone());
   }
@@ -410,13 +418,15 @@
 
   private final AccountCache accountCache;
   private final PersonIdent serverIdent;
+  private final String anonymousCowardName;
 
-  @VisibleForTesting
   @Inject
   public CommentsInNotesUtil(AccountCache accountCache,
-      @GerritPersonIdent PersonIdent serverIdent) {
+      @GerritPersonIdent PersonIdent serverIdent,
+      @AnonymousCowardName String anonymousCowardName) {
     this.accountCache = accountCache;
     this.serverIdent = serverIdent;
+    this.anonymousCowardName = anonymousCowardName;
   }
 
   /**
@@ -454,11 +464,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());
 
@@ -520,12 +530,10 @@
         throws OrmException, 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/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..de36020 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
@@ -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..f3137fe 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;
 
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..919b41a 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,
+      InvalidChangeOperationException, 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, InvalidChangeOperationException {
+    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/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..1ef1d82 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
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..b1cba2b 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;
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..20633d1 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;
@@ -72,7 +72,8 @@
   static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
 
   public String getPluginName(File srcFile) throws IOException {
-    return Objects.firstNonNull(getGerritPluginName(srcFile), nameOf(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>,"
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..930360a 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
@@ -51,8 +51,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)
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..e93f69c 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
@@ -413,6 +413,16 @@
     }
   }
 
+  /** 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 List<SubmitRecord> getSubmitRecords(ReviewDb db, PatchSet patchSet) {
     return canSubmit(db, patchSet, null, false, true, false);
   }
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..38bbba4 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;
@@ -173,7 +173,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 +194,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)) {
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/ListBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
index 7c8b29f..01e705a 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,15 +14,18 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.base.Strings;
 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.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;
@@ -47,12 +50,15 @@
 public class ListBranches implements RestReadView<ProjectResource> {
   private final GitRepositoryManager repoManager;
   private final DynamicMap<RestView<BranchResource>> branchViews;
+  private final WebLinks webLinks;
 
   @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
@@ -161,6 +167,13 @@
       }
       info.actions.put(d.getId(), new ActionInfo(d));
     }
+    info.webLinks = Lists.newArrayList();
+    for (WebLinkInfo link : webLinks.getBranchLinks(
+        refControl.getProjectControl().getProject().getName(), ref.getName())) {
+      if (!Strings.isNullOrEmpty(link.name) && !Strings.isNullOrEmpty(link.url)) {
+        info.webLinks.add(link);
+      }
+    }
     return info;
   }
 
@@ -169,6 +182,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..6fd7d16 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,7 @@
 
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
+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,14 +39,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;
 import org.eclipse.jgit.lib.Ref;
@@ -113,7 +110,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 +191,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;
@@ -386,7 +383,7 @@
           }
 
           info.webLinks = Lists.newArrayList();
-          for (WebLinkInfo link : webLinks.get().getProjectLinks(projectName.get())) {
+          for (WebLinkInfo link : webLinks.getProjectLinks(projectName.get())) {
             if (!Strings.isNullOrEmpty(link.name) && !Strings.isNullOrEmpty(link.url)) {
               info.webLinks.add(link);
             }
@@ -449,8 +446,10 @@
 
   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>() {
             public boolean apply(Project.NameKey in) {
@@ -459,32 +458,31 @@
             }
           });
     } 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/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/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index 53b7368..2ae09f2 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,6 +31,7 @@
 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;
@@ -37,15 +39,16 @@
 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 +151,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 +167,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 +272,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()} */
@@ -426,7 +435,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 +446,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 +522,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..5ff0448 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
@@ -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;
   }
@@ -54,7 +53,7 @@
     info.id = Url.encode(info.name);
 
     info.webLinks = Lists.newArrayList();
-    for (WebLinkInfo link : webLinks.get().getProjectLinks(p.getName())) {
+    for (WebLinkInfo link : webLinks.getProjectLinks(p.getName())) {
       if (!Strings.isNullOrEmpty(link.name) && !Strings.isNullOrEmpty(link.url)) {
         info.webLinks.add(link);
       }
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..df48993 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
@@ -404,6 +404,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..7c7b966 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);
       }
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/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..e853656 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
@@ -30,46 +30,44 @@
           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, 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;
-
   @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..52ca127 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,7 +16,7 @@
 
 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;
@@ -35,6 +35,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.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NotesMigration;
@@ -78,7 +79,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 +120,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()));
@@ -154,7 +155,7 @@
    */
   static ChangeData createForTest(Change.Id id, int currentPatchSetId) {
     ChangeData cd = new ChangeData(null, null, null, null, null,
-        null, null, null, null, id);
+        null, null, null, null, null, id);
     cd.currentPatchSet = new PatchSet(new PatchSet.Id(id, currentPatchSetId));
     return cd;
   }
@@ -166,6 +167,7 @@
   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 Change.Id legacyId;
@@ -179,7 +181,7 @@
   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;
@@ -194,6 +196,7 @@
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
+      PatchLineCommentsUtil plcUtil,
       PatchListCache patchListCache,
       NotesMigration notesMigration,
       @Assisted ReviewDb db,
@@ -205,6 +208,7 @@
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
+    this.plcUtil = plcUtil;
     this.patchListCache = patchListCache;
     this.notesMigration = notesMigration;
     legacyId = id;
@@ -218,6 +222,7 @@
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
+      PatchLineCommentsUtil plcUtil,
       PatchListCache patchListCache,
       NotesMigration notesMigration,
       @Assisted ReviewDb db,
@@ -229,6 +234,7 @@
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
+    this.plcUtil = plcUtil;
     this.patchListCache = patchListCache;
     this.notesMigration = notesMigration;
     legacyId = c.getId();
@@ -243,6 +249,7 @@
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       ChangeMessagesUtil cmUtil,
+      PatchLineCommentsUtil plcUtil,
       PatchListCache patchListCache,
       NotesMigration notesMigration,
       @Assisted ReviewDb db,
@@ -254,6 +261,7 @@
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
     this.cmUtil = cmUtil;
+    this.plcUtil = plcUtil;
     this.patchListCache = patchListCache;
     this.notesMigration = notesMigration;
     legacyId = c.getChange().getId();
@@ -519,12 +527,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()
@@ -545,7 +553,7 @@
 
   @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..30862b6 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;
@@ -90,6 +91,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";
@@ -146,6 +148,7 @@
     final CapabilityControl.Factory capabilityControlFactory;
     final ChangeControl.GenericFactory changeControlGenericFactory;
     final ChangeData.Factory changeDataFactory;
+    final PatchLineCommentsUtil plcUtil;
     final AccountResolver accountResolver;
     final GroupBackend groupBackend;
     final AllProjectsName allProjectsName;
@@ -168,6 +171,7 @@
         CapabilityControl.Factory capabilityControlFactory,
         ChangeControl.GenericFactory changeControlGenericFactory,
         ChangeData.Factory changeDataFactory,
+        PatchLineCommentsUtil plcUtil,
         AccountResolver accountResolver,
         GroupBackend groupBackend,
         AllProjectsName allProjectsName,
@@ -187,6 +191,7 @@
       this.capabilityControlFactory = capabilityControlFactory;
       this.changeControlGenericFactory = changeControlGenericFactory;
       this.changeDataFactory = changeDataFactory;
+      this.plcUtil = plcUtil;
       this.accountResolver = accountResolver;
       this.groupBackend = groupBackend;
       this.allProjectsName = allProjectsName;
@@ -272,22 +277,15 @@
 
   @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
@@ -393,6 +391,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);
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..8c22aca 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
@@ -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;
@@ -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 =
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/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
index 7927cbb..b2560de 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,9 +14,10 @@
 
 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;
@@ -32,7 +33,6 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gson.Gson;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
@@ -366,7 +366,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 +416,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..3d6f8b4 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) {
     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/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index 11479cc..bd1f8e8 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_99> C = Schema_99.class;
 
   public static class Module extends AbstractModule {
     @Override
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_65.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java
index 636e0c6..f3f8bfe 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;
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-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/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/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/resources/com/google/gerrit/server/config/CapabilityConstants.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties
index 9eb7d9b..056da87 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
@@ -5,7 +5,9 @@
 createProject = Create Project
 emailReviewers = Email Reviewers
 flushCaches = Flush Caches
+generateHttpPassword = Generate HTTP Password
 killTask = Kill Task
+modifyAccount = Modify Account
 priority = Priority
 queryLimit = Query Limit
 runAs = Run As
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mime-types.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/mime-types.properties
index 8960ff9..817790f 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mime-types.properties
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mime-types.properties
@@ -30,11 +30,9 @@
 properties = text/x-ini
 py = text/x-python
 r = text/r-src
-rst = text/x-rst
 rb = text/x-ruby
 scala = text/x-scala
 st = text/x-stsrc
-stex = text/x-stex
 v = text/x-verilog
 vh = text/x-verilog
 vm = text/velocity
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..1bc78f8 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 {
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/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/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..add4c96 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,12 @@
 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 {
@@ -60,42 +58,32 @@
 
   @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"));
+    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();
+    return new ScheduleConfig(
+        config(startTime, interval),
+        "section", "subsection", NOW).getInitialDelay();
   }
 
-  private static ScheduleConfig config(String startTime, String interval)
+  private static Config 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;
+    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/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..5e10e66 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;
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..7090f0d 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, 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..c2a46ce 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<String>();
+    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<String>();
+    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..c3fbccd
--- /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<InMemoryRepository>(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..a44666b 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;
@@ -46,6 +45,7 @@
 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;
@@ -61,6 +61,7 @@
 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 +179,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,12 +242,13 @@
       }
 
       @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) {
       }
     };
 
@@ -266,6 +270,8 @@
         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);
@@ -281,25 +287,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 +319,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..4d87471 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,6 +1047,15 @@
     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);
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..d1e21df 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,42 @@
 
 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.lib.Config;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
 public class LuceneQueryChangesTest extends AbstractQueryChangesTest {
   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/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/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
index 6b2f7c7..5e9858c 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
@@ -53,6 +53,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 +81,11 @@
 public class InMemoryModule extends FactoryModule {
   public static Config newDefaultConfig() {
     Config cfg = new Config();
+    setDefaults(cfg);
+    return cfg;
+  }
+
+  public static void setDefaults(Config cfg) {
     cfg.setEnum("auth", null, "type", AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT);
     cfg.setString("gerrit", null, "basePath", "git");
     cfg.setString("gerrit", null, "allProjects", "Test-Projects");
@@ -90,7 +97,6 @@
     cfg.setBoolean("index", "lucene", "testInmemory", true);
     cfg.setInt("index", "lucene", "testVersion",
         ChangeSchemas.getLatest().getVersion());
-    return cfg;
   }
 
   private final Config cfg;
@@ -149,6 +155,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() {
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..9e9f103 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);
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..bb431f4 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;
@@ -475,7 +474,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/PluginCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
index 334f155..63d3fed 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;
 
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/SshDaemon.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
index 32c13b4..2f4b293 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;
@@ -188,6 +189,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) {
@@ -512,7 +522,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/SshLog.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
index 7df8db4..61f5fa40 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.Multimap;
 import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.audit.SshAuditEvent;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -25,7 +26,6 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.gerrit.server.util.SystemLog;
-import com.google.gerrit.server.util.TimeUtil;
 import com.google.gerrit.sshd.SshScope.Context;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
index 4f9fe33..f134d48 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.server.plugins.Plugin;
 import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.plugins.StartPluginListener;
+import com.google.inject.Inject;
 import com.google.inject.Key;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -25,8 +26,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import javax.inject.Inject;
-
 @Singleton
 class SshPluginStarterCallback
     implements StartPluginListener, ReloadPluginListener {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
index f8b5ddb..8e763df 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;
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/CreateProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index fb39dee5..624d4e5 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
@@ -87,6 +87,9 @@
   @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) {
     contributorAgreements = InheritableBoolean.TRUE;
@@ -107,6 +110,11 @@
     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(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 +174,7 @@
         input.useSignedOffBy = signedOffBy;
         input.useContentMerge = contentMerge;
         input.requireChangeId = requireChangeID;
+        input.createNewChangeForAllNotInTarget = createNewChangeForAllNotInTarget;
         input.branches = branch;
         input.createEmptyCommit = createEmptyCommit;
         input.maxObjectSizeLimit = maxObjectSizeLimit;
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..a312d0d 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,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.MethodNotAllowedException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -26,41 +26,33 @@
 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 {
@@ -102,10 +94,11 @@
           }
 
           formatter.addColumn(member._id.toString());
-          formatter.addColumn(Objects.firstNonNull(member.username, "n/a"));
-          formatter.addColumn(Objects.firstNonNull(
+          formatter.addColumn(MoreObjects.firstNonNull(
+              member.username, "n/a"));
+          formatter.addColumn(MoreObjects.firstNonNull(
               Strings.emptyToNull(member.name), "n/a"));
-          formatter.addColumn(Objects.firstNonNull(member.email, "n/a"));
+          formatter.addColumn(MoreObjects.firstNonNull(member.email, "n/a"));
           formatter.nextLine();
         }
 
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/PluginLsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
index 9f6bb50..dc8abf2 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,23 @@
 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);
+  }
+
+  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/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index 5088424..76a5f24 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,10 @@
 
 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.RawInput;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -34,13 +37,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 +58,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 +79,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 +91,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 +101,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 +144,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 +158,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 +177,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
         }
@@ -227,13 +237,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);
@@ -253,7 +263,7 @@
   private void deleteSshKey(SshKeyInfo i) throws 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);
   }
 
@@ -272,18 +282,28 @@
   private void deleteEmail(String email) throws UnloggedFailure,
       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/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..7771d7f 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;
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..fbe2743 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
@@ -310,7 +310,7 @@
     modules.add(new AbstractModule() {
       @Override
       protected void configure() {
-        bind(GerritUiOptions.class).toInstance(new GerritUiOptions(false));
+        bind(GerritOptions.class).toInstance(new GerritOptions(false));
       }
     });
     modules.add(GarbageCollectionRunner.module());
@@ -344,6 +344,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 51c4481..a7d34de 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -51,8 +51,8 @@
 
 maven_jar(
   name = 'guava',
-  id = 'com.google.guava:guava:17.0',
-  sha1 = '9c6ef172e8de35fd8d4d8783e4821e57cdef7445',
+  id = 'com.google.guava:guava:18.0',
+  sha1 = 'cce0823396aa693798f8882e64213b1772032b09',
   license = 'Apache2.0',
 )
 
@@ -116,8 +116,8 @@
 
 maven_jar(
   name = 'pegdown',
-  id = 'org.pegdown:pegdown:1.2.1',
-  sha1 = '47689e060d90f90431b5ab2df911452b93930d8c',
+  id = 'org.pegdown:pegdown:1.4.2',
+  sha1 = 'd96db502ed832df867ff5d918f05b51ba3879ea7',
   license = 'Apache2.0',
   deps = [':parboiled-java'],
 )
@@ -171,18 +171,10 @@
 
 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'],
-)
-
-maven_jar(
-  name = 'hamcrest-core',
-  id = 'org.hamcrest:hamcrest-core:1.3',
-  sha1 = '42a25dc3219429f0e5d060061f71acb49bf010a0',
-  license = 'DO_NOT_DISTRIBUTE',
-  visibility = ['//lib:junit'],
+  deps = ['//lib/hamcrest:hamcrest-core'],
 )
 
 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..4736b0b 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_0;
   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/cm3.defs b/lib/codemirror/cm3.defs
index 93cf81e..e9eff39 100644
--- a/lib/codemirror/cm3.defs
+++ b/lib/codemirror/cm3.defs
@@ -20,7 +20,6 @@
   'addon/search/search.js',
   'addon/selection/mark-selection.js',
   'addon/edit/trailingspace.js',
-  'addon/mode/overlay.js',
 ]
 
 CM3_MODES = [
@@ -35,7 +34,6 @@
   'erlang/erlang.js',
   'gas/gas.js',
   'gfm/gfm.js',
-  'go/go.js',
   'groovy/groovy.js',
   'haskell/haskell.js',
   'htmlmixed/htmlmixed.js',
@@ -48,13 +46,11 @@
   'properties/properties.js',
   'python/python.js',
   'r/r.js',
-  'rst/rst.js',
   'ruby/ruby.js',
   'scheme/scheme.js',
   'shell/shell.js',
   'smalltalk/smalltalk.js',
   'sql/sql.js',
-  'stex/stex.js',
   'tcl/tcl.js',
   'velocity/velocity.js',
   'verilog/verilog.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/guice/BUCK b/lib/guice/BUCK
index 162ad07..703573e 100644
--- a/lib/guice/BUCK
+++ b/lib/guice/BUCK
@@ -1,7 +1,6 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '4.0-beta'
-COOKIE_PATCH = '4.0-beta-98-g8d88344'
+VERSION = '4.0-beta5'
 EXCLUDE = [
   'META-INF/DEPENDENCIES',
   'META-INF/LICENSE',
@@ -20,7 +19,7 @@
 maven_jar(
   name = 'guice_library',
   id = 'com.google.inject:guice:' + VERSION,
-  sha1 = 'a82be989679df08b66d48b42659a3ca2daaf1d5b',
+  sha1 = 'fdf5df843620978a6f2929fd56f719a20d713c2b',
   license = 'Apache2.0',
   deps = [':aopalliance'],
   exclude_java_sources = True,
@@ -34,7 +33,7 @@
 maven_jar(
   name = 'guice-assistedinject',
   id = 'com.google.inject.extensions:guice-assistedinject:' + VERSION,
-  sha1 = 'abd6511011a9e4b64e2ebb60caac2e1cd6cd19a1',
+  sha1 = '820f10e0650cd9ed2591f398937df50f330b147d',
   license = 'Apache2.0',
   deps = [':guice'],
   exclude = EXCLUDE,
@@ -42,9 +41,8 @@
 
 maven_jar(
   name = 'guice-servlet',
-  id = 'com.google.inject.extensions:guice-servlet:' + COOKIE_PATCH,
-  repository = GERRIT,
-  sha1 = 'fa17d57a083fe9fc86b93f2dc37069573a2e65cd',
+  id = 'com.google.inject.extensions:guice-servlet:' + VERSION,
+  sha1 = '852af296c8a06aac968d17491fd8c1eab1ec8b10',
   license = 'Apache2.0',
   deps = [':guice'],
   exclude = EXCLUDE,
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..6191f64 100644
--- a/lib/jetty/BUCK
+++ b/lib/jetty/BUCK
@@ -1,12 +1,12 @@
 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',
@@ -18,7 +18,7 @@
 maven_jar(
   name = 'security',
   id = 'org.eclipse.jetty:jetty-security:' + VERSION,
-  sha1 = '8ac8cc9e5c66eb6022cbe80f4e22d4e42dc5e643',
+  sha1 = 'b66f8f4b9afd82af24b9f7ffcd5312eb628ee0c9',
   license = 'Apache2.0',
   deps = [':server'],
   exclude = EXCLUDE,
@@ -26,11 +26,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 +38,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 +52,7 @@
 maven_jar(
   name = 'jmx',
   id = 'org.eclipse.jetty:jetty-jmx:' + VERSION,
-  sha1 = '1258d5ac618b120026da8a82283e6cb8ff4638a6',
+  sha1 = '64b3ff4d0cee0acee363a1c98193332e8f845a6e',
   license = 'Apache2.0',
   exported_deps = [
     ':continuation',
@@ -74,7 +64,7 @@
 maven_jar(
   name = 'continuation',
   id = 'org.eclipse.jetty:jetty-continuation:' + VERSION,
-  sha1 = 'e5bf20cdcd9c2878677f3c0f43baea2725f8c59e',
+  sha1 = 'a39e8cee9c36d159c6ab3283eb59f8bc2fd16c43',
   license = 'Apache2.0',
   exclude = EXCLUDE,
 )
@@ -82,7 +72,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 +81,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 +91,7 @@
 maven_jar(
   name = 'util',
   id = 'org.eclipse.jetty:jetty-util:' + VERSION,
-  sha1 = '4ae7ac5d3cfcb21bc288dd3f4ec3ba2823442f0d',
+  sha1 = '2e199b14a57b0336b4f2c9fdf8add525d6112833',
   license = 'Apache2.0',
   exclude = EXCLUDE,
   visibility = [],
diff --git a/lib/jgit/BUCK b/lib/jgit/BUCK
index 9161a7b..ba390ab 100644
--- a/lib/jgit/BUCK
+++ b/lib/jgit/BUCK
@@ -1,12 +1,12 @@
 include_defs('//lib/maven.defs')
 
-VERS = '3.4.0.201406110918-r'
+VERS = '3.5.0.201409071800-rc1'
 
 maven_jar(
   name = 'jgit',
   id = 'org.eclipse.jgit:org.eclipse.jgit:' + VERS,
-  bin_sha1 = '60e74a29895be82ec7bd1fbb6304975e92b955a5',
-  src_sha1 = '69adaa263e2b5c21a84a25105c0c93761bdd8a80',
+  bin_sha1 = 'e520f25877d6ec940ac98faa5ffa97a75d83b653',
+  src_sha1 = '2c9eda84801364e9b07a458114a5a5c04bb8c99b',
   license = 'jgit',
   unsign = True,
   deps = [':ewah'],
@@ -20,7 +20,7 @@
 maven_jar(
   name = 'jgit-servlet',
   id = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + VERS,
-  sha1 = '5c778052a90520a970041f2f51e89cac9cf2cb3f',
+  sha1 = '089e041d5065a7aa30366c3f1610868bac50bdba',
   license = 'jgit',
   deps = [':jgit'],
   unsign = True,
@@ -33,7 +33,7 @@
 maven_jar(
   name = 'jgit-archive',
   id = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + VERS,
-  sha1 = '8644e0dde6127c2d1b5a4fa902bf9b534764e969',
+  sha1 = 'b335e60660653a6a421e76af5604d84ca526aa6a',
   license = 'jgit',
   deps = [':jgit',
     '//lib/commons:compress',
@@ -49,7 +49,7 @@
 maven_jar(
   name = 'junit',
   id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + VERS,
-  sha1 = '4d1232e8b2412521d707d741ca14db9fc6806db2',
+  sha1 = '9253ab01d92ccd657290b5996defa64177e738d0',
   license = 'DO_NOT_DISTRIBUTE',
   unsign = True,
   deps = [':jgit'],
diff --git a/lib/local.defs b/lib/local.defs
new file mode 100644
index 0000000..6eec581
--- /dev/null
+++ b/lib/local.defs
@@ -0,0 +1,33 @@
+def local_jar(
+    name,
+    jar,
+    src = None,
+    deps = [],
+    visibility = ['PUBLIC']):
+  binjar = name + '.jar'
+  srcjar = name + '-src.jar'
+  genrule(
+    name = '%s__local_bin' % name,
+    cmd = 'ln -s %s $OUT' % jar,
+    out = binjar)
+  if src:
+    genrule(
+      name = '%s__local_src' % name,
+      cmd = 'ln -s %s $OUT' % src,
+      out = srcjar)
+    prebuilt_jar(
+      name = '%s_src' % name,
+      binary_jar = ':%s__local_src' % name,
+      visibility = visibility,
+    )
+  else:
+    srcjar = None
+
+  prebuilt_jar(
+    name = name,
+    deps = deps,
+    binary_jar = ':%s__local_bin' % name,
+    source_jar = ':%s__local_src' % name if srcjar else None,
+    visibility = visibility,
+ )
+
diff --git a/lib/lucene/BUCK b/lib/lucene/BUCK
index 9ccc5aa..dd148b1 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.0'
 
 maven_jar(
   name = 'core',
   id = 'org.apache.lucene:lucene-core:' + VERSION,
-  sha1 = 'a549eef6316a2c38d4cda932be809107deeaf8a7',
+  sha1 = 'a4ceea9a80e81fe84e81fe4fccce9e9930dc703a',
   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 = '912962d436d9851dc90091e48251c802d3b65941',
   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 = '7a00eb4b97a6cb7a5a29957b62c7f002dd71b7ff',
   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..c882e58 160000
--- a/plugins/commit-message-length-validator
+++ b/plugins/commit-message-length-validator
@@ -1 +1 @@
-Subproject commit a93b85656a68b6a71fe7cf0bb0cc4ed8143657bf
+Subproject commit c882e583226d712053ad02fdee4afcfd1e4df915
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
index 6582905..a2e56f0 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit 6582905669d0ccdd009f839936f8209010ae9d6f
+Subproject commit a2e56f0f76dac45a5084c28a27e24ba039b57e09
diff --git a/plugins/download-commands b/plugins/download-commands
index 6287d6a..4e978f9 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 6287d6a8941f68ba8a3a8c27f2a979c02ede489a
+Subproject commit 4e978f916d429ab22b0ea28d25ac87755513cc56
diff --git a/plugins/replication b/plugins/replication
index 054122e..211e928 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 054122e7824ed64f3e3b697d97afa187ec91417e
+Subproject commit 211e9289cc5771293855845d31bc0ce756545851
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 6170241..b2e2b70 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 61702414c046dd6b811c9137b765f9db422f83db
+Subproject commit b2e2b7046f4830ff41fcfe2e115fa349e3635135
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..2239026 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(
@@ -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..d117bda
--- /dev/null
+++ b/tools/java_doc.defs
@@ -0,0 +1,37 @@
+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/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',