Merge "Add an exponentially rolling garbage collection script"
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 5172e03..78acbd2 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -23,7 +23,7 @@
 
 Users in the 'Administrators' group can perform any action under
 the Admin menu, to any group or project, without further validation
-of any other access controls.  In most installations only those
+or any other access controls.  In most installations only those
 users who have direct filesystem and database access would be
 placed into this group.
 
@@ -444,6 +444,14 @@
 A restart is required after making database changes.
 See <<restart_changes,below>>.
 
+[[category_abandon]]
+Abandon
+~~~~
+
+This category controls whether users are allowed to abandon changes
+to projects in Gerrit. It can give permission to abandon a specific
+change to a given ref.
+
 [[category_create]]
 Create reference
 ~~~~~~~~~~~~~~~~
@@ -451,7 +459,7 @@
 The create reference category controls whether it is possible to
 create new references, branches or tags.  This implies that the
 reference must not already exist, it's not a destructive permission
-in that you can't overwrite or remove any previosuly existing
+in that you can't overwrite or remove any previously existing
 references (and also discard any commits in the process).
 
 It's probably most common to either permit the creation of a single
@@ -462,7 +470,7 @@
 branch permissions, allowing the holder of both to create new branches
 as well as bypass review for new commits on that branch.
 
-To push lightweight (non annotated) tags, grant
+To push lightweight (non-annotated) tags, grant
 `Create Reference` for reference name `refs/tags/*`, as lightweight
 tags are implemented just like branches in Git.
 
@@ -815,7 +823,7 @@
 
 Below follows a set of typical roles on a server and which access
 rights these roles typically should be granted. You may see them as
-general guide lines for a typical way to set up your project on a
+general guidelines for a typical way to set up your project on a
 brand new Gerrit instance.
 
 [[examples_contributor]]
@@ -898,8 +906,8 @@
 project and how much the CI system can be trusted for accurate results, a
 blocking label might not be feasible.  A recommended alternative is to set the
 label `Code-review` to -1 instead, as it isn't a blocking label but still
-shows a red label in the Gerrit UI.  Optionally; to enable the possibility to
-deliver different results (build error vs unstable for instance) it's also
+shows a red label in the Gerrit UI.  Optionally, to enable the possibility to
+deliver different results (build error vs unstable for instance), it's also
 possible to set `Code-review` +1 as well.
 
 If pushing new changes is granted, it's possible to automate cherry-pick of
@@ -1142,7 +1150,8 @@
 Start Replication
 ~~~~~~~~~~~~~~~~~
 
-Allow access to execute link:cmd-replicate.html[the `gerrit replicate` command].
+Allow access to execute `replication start` command, if the
+replication plugin is installed on the server.
 
 
 [[capability_viewCaches]]
diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt
index f22141c..02aa078 100644
--- a/Documentation/cmd-create-project.txt
+++ b/Documentation/cmd-create-project.txt
@@ -19,7 +19,7 @@
   [--use-signed-off-by | --so]
   [--use-content-merge]
   [--require-change-id | --id]
-  [--branch <REF> | -b <REF>]
+  [[--branch <REF> | -b <REF>] ...]
   [--empty-commit]
   { <NAME> | --name <NAME> }
 
@@ -59,8 +59,11 @@
 
 --branch::
 -b::
-	Name of the initial branch in the newly created project.
-	Defaults to 'master'.
+	Name of the initial branch(es) in the newly created project.
+	Several branches can be specified on the command line.
+	If several branches are specified then the first one becomes HEAD
+	of the project. If none branches are specified then default value
+	('master') is used.
 
 --owner::
 -o::
@@ -163,7 +166,8 @@
 
 REPLICATION
 -----------
-The remote repository creation is performed by a Bourne shell script:
+If the replication plugin is installed, the plugin will attempt to
+perform remote repository creation by a Bourne shell script:
 
 ====
   mkdir -p '/base/project.git' && cd '/base/project.git' && git init --bare && git update-ref HEAD refs/heads/master
@@ -174,10 +178,13 @@
 environment variable.  Administrators could also run this command line
 by hand to establish a new empty repository.
 
+A custom extension or plugin may also be developed to implement the
+NewProjectCreatedListener extension point and handle custom logic
+for remote repository creation.
+
 SEE ALSO
 --------
 
-* link:config-replication.html[Git Replication/Mirroring]
 * link:project-setup.html[Project Setup]
 
 GERRIT
diff --git a/Documentation/cmd-hook-commit-msg.txt b/Documentation/cmd-hook-commit-msg.txt
index 6318bba..39cb3a5 100644
--- a/Documentation/cmd-hook-commit-msg.txt
+++ b/Documentation/cmd-hook-commit-msg.txt
@@ -54,7 +54,7 @@
 OBTAINING
 ---------
 To obtain the 'commit-msg' script use scp, wget or curl to download it
-to your local system from your gerrit server.
+to your local system from your Gerrit server.
 
 You can use either of the below commands:
 
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index 7970084..936729e 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -111,9 +111,6 @@
 link:cmd-gsql.html[gerrit gsql]::
 	Administrative interface to active database.
 
-link:cmd-replicate.html[gerrit replicate]::
-	Manually trigger replication, to recover a node.
-
 link:cmd-set-project-parent.html[gerrit set-project-parent]::
 	Change the project permissions are inherited from.
 
diff --git a/Documentation/cmd-ls-projects.txt b/Documentation/cmd-ls-projects.txt
index c1b37fa..25cd9a9 100644
--- a/Documentation/cmd-ls-projects.txt
+++ b/Documentation/cmd-ls-projects.txt
@@ -46,8 +46,11 @@
 	Allows listing of projects together with their respective
 	description.
 +
-Line-feeds are escaped to allow ls-project to keep the
-"one project per line"-style.
+For text format output, all non-printable characters (ASCII value 31 or
+less) are escaped according to the conventions used in languages like C,
+Python, and Perl, employing standard sequences like `\n` and `\t`, and
+`\xNN` for all others. In shell scripts, the `printf` command can be
+used to unescape the output.
 
 --tree::
 -t::
diff --git a/Documentation/cmd-replicate.txt b/Documentation/cmd-replicate.txt
deleted file mode 100644
index 7722027..0000000
--- a/Documentation/cmd-replicate.txt
+++ /dev/null
@@ -1,103 +0,0 @@
-gerrit replicate
-================
-
-NAME
-----
-gerrit replicate - Manually trigger replication, to recover a node
-
-SYNOPSIS
---------
-[verse]
-'ssh' -p <port> <host> 'gerrit replicate'
-  [--url <PATTERN>]
-  {--all | <PROJECT> ...}
-
-DESCRIPTION
------------
-Schedules replication of the specified projects to all configured
-replication destinations, or only those whose URLs match the pattern
-given on the command line.
-
-Normally Gerrit automatically schedules replication whenever it
-makes a change to a managed Git repository.  However, there are
-other reasons why an administrator may wish to trigger replication:
-
-* Destination disappears, then later comes back online.
-+
-If a destination went offline for a period of time, when it comes
-back, it may be missing commits that it should have.  Triggering a
-replication run for all projects against that URL will update it.
-
-* After repacking locally, and using `rsync` to distribute the new
-  pack files to the destinations.
-+
-If the local server is repacked, and then the resulting pack files
-are sent to remote peers using `rsync -a --delete-after`, there
-is a chance that the rsync missed a change that was added during
-the rsync data transfer, and the rsync will remove that changes's
-data from the remote, even though the automatic replication pushed
-it there in parallel to the rsync.
-+
-Its a good idea to run replicate with `--all` to ensure all
-projects are consistent after the rsync is complete.
-
-* After deleting a ref by hand.
-+
-If a ref must be removed (e.g. to purge a change or patch set
-that shouldn't have been created, and that must be eradicated)
-that delete must be done by direct git access on the local,
-managed repository.  Gerrit won't know about the delete, and is
-unable to replicate it automatically.  Triggering replication on
-just the affected project can update the mirrors.
-
-ACCESS
-------
-Caller must be a member of the privileged 'Administrators' group,
-or have been granted
-link:access-control.html#capability_startReplication[the 'Start Replication' global capability].
-
-SCRIPTING
----------
-This command is intended to be used in scripts.
-
-OPTIONS
--------
---all::
-	Schedule replicating for all projects.
-
---url <PATTERN>::
-	Replicate only to replication destinations whose URL
-	contains the substring <PATTERN>.  This can be useful to
-	replicate only to a previously down node, which has been
-	brought back online.
-
-EXAMPLES
---------
-Replicate every project, to every configured remote:
-
-====
-  $ ssh -p 29418 review.example.com gerrit replicate --all
-====
-
-Replicate only to `srv2` now that it is back online:
-
-====
-  $ ssh -p 29418 review.example.com gerrit replicate --url srv2 --all
-====
-
-Replicate only the `tools/gerrit` project, after deleting a ref
-locally by hand:
-
-====
-  $ git --git-dir=/home/git/tools/gerrit.git update-ref -d refs/changes/00/100/1
-  $ ssh -p 29418 review.example.com gerrit replicate tools/gerrit
-====
-
-SEE ALSO
---------
-
-* link:config-replication.html[Git Replication/Mirroring]
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index bf78051..b3c6037 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -41,9 +41,10 @@
 SCHEMA
 ------
 The JSON messages consist of nested objects referencing the *change*,
-*patchset*, *account* involved, and other attributes as appropriate.
+*patchSet*, *account* involved, and other attributes as appropriate.
 The currently supported message types are *patchset-created*,
-*comment-added*, *change-merged*, and *change-abandoned*.
+*change-abandoned*, *change-restored*, *change-merged*,
+*comment-added* and *ref-updated*.
 
 Note that any field may be missing in the JSON messages, so consumers of
 this JSON stream should deal with that appropriately.
@@ -56,7 +57,7 @@
 
 change:: link:json.html#change[change attribute]
 
-patchset:: link:json.html#patchset[patchset attribute]
+patchSet:: link:json.html#patchSet[patchSet attribute]
 
 uploader:: link:json.html#account[account attribute]
 
@@ -66,27 +67,31 @@
 
 change:: link:json.html#change[change attribute]
 
-patchset:: link:json.html#patchset[patchset attribute]
+patchSet:: link:json.html#patchSet[patchSet attribute]
 
 abandoner:: link:json.html#account[account attribute]
 
+reason:: Reason for abandoning the change.
+
 Change Restored
-^^^^^^^^^^^^^^^^
+^^^^^^^^^^^^^^^
 type:: "change-restored"
 
 change:: link:json.html#change[change attribute]
 
-patchset:: link:json.html#patchset[patchset attribute]
+patchSet:: link:json.html#patchSet[patchSet attribute]
 
 restorer:: link:json.html#account[account attribute]
 
+reason:: Reason for restoring the change.
+
 Change Merged
 ^^^^^^^^^^^^^
 type:: "change-merged"
 
 change:: link:json.html#change[change attribute]
 
-patchset:: link:json.html#patchset[patchset attribute]
+patchSet:: link:json.html#patchSet[patchSet attribute]
 
 submitter:: link:json.html#account[account attribute]
 
@@ -96,10 +101,12 @@
 
 change:: link:json.html#change[change attribute]
 
-patchset:: link:json.html#patchset[patchset attribute]
+patchSet:: link:json.html#patchSet[patchSet attribute]
 
 author:: link:json.html#account[account attribute]
 
+approvals:: All link:json.html#approval[approval attributes] granted.
+
 comment:: Comment text author had written
 
 Ref Updated
@@ -108,7 +115,7 @@
 
 submitter:: link:json.html#account[account attribute]
 
-refUpdate:: link:json.html#refupdate[refupdate attribute]
+refUpdate:: link:json.html#refUpdate[refUpdate attribute]
 
 
 SEE ALSO
diff --git a/Documentation/config-contact.txt b/Documentation/config-contact.txt
index 5c633cf..4d8851f 100644
--- a/Documentation/config-contact.txt
+++ b/Documentation/config-contact.txt
@@ -48,7 +48,7 @@
 The actual values chosen don't matter later, and are only to help
 document the purpose of the key.
 
-Chose a fairly long expiration period, such as 20 years.  For most
+Choose a fairly long expiration period, such as 20 years.  For most
 Gerrit instances, contact data will be written once, and rarely,
 if ever, read back.
 
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index ddbddbe..0435f4e 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -107,7 +107,7 @@
 * `CLIENT_SSL_CERT_LDAP`
 +
 This authentication type is actually kind of SSO. Gerrit will configure
-Jetty's SSL channel to request client's SSL certificate. For this
+Jetty's SSL channel to request the client's SSL certificate. For this
 authentication to work a Gerrit administrator has to import the root
 certificate of the trust chain used to issue the client's certificate
 into the <review-site>/etc/keystore.
@@ -161,7 +161,7 @@
 +
 List of permitted OpenID providers.  A user may only authenticate
 with an OpenID that matches this list.  Only used if `auth.type`
-was set to OpenID (the default).
+is set to OpenID (the default).
 +
 Patterns may be either a
 link:http://download.oracle.com/javase/6/docs/api/java/util/regex/Pattern.html[standard
@@ -173,7 +173,7 @@
 
 [[auth.trustedOpenID]]auth.trustedOpenID::
 +
-List of trusted OpenID providers.  Only used if `auth.type` was
+List of trusted OpenID providers.  Only used if `auth.type` is
 set to OpenID (the default).
 +
 In order for a user to take advantage of permissions beyond those
@@ -232,7 +232,7 @@
 [[auth.httpHeader]]auth.httpHeader::
 +
 HTTP header to trust the username from, or unset to select HTTP basic
-or digest authentication.  Only used if `auth.type` was set to HTTP.
+or digest authentication.  Only used if `auth.type` is set to HTTP.
 
 [[auth.logoutUrl]]auth.logoutUrl::
 +
@@ -354,8 +354,8 @@
 
 [[cache.name.maxAge]]cache.<name>.maxAge::
 +
-Maximum age to keep an entry in the cache.  If an entry has not
-been accessed in this period of time, it is removed from the cache.
+Maximum age to keep an entry in the cache. Entries are removed from
+the cache and refreshed from source data every maxAge interval.
 Values should use common unit suffixes to express their setting:
 +
 * s, sec, second, seconds
@@ -371,7 +371,7 @@
 supplied, the maximum age is infinite and items are never purged
 except when the cache is full.
 +
-Default is `90 days` for most caches, except:
+Default is `0`, meaning store forever with no expire, except:
 +
 * `"adv_bases"`: default is `10 minutes`
 * `"ldap_groups"`: default is `1 hour`
@@ -379,33 +379,42 @@
 
 [[cache.name.memoryLimit]]cache.<name>.memoryLimit::
 +
-Maximum number of cache items to retain in memory.  Keep in mind
-this is total number of items, not bytes of heap used.
+The total cost of entries to retain in memory. The cost computation
+varies by the cache. For most caches where the in-memory size of each
+entry is relatively the same, memoryLimit is currently defined to be
+the number of entries held by the cache (each entry costs 1).
++
+For caches where the size of an entry can vary significantly between
+individual entries (notably `"diff"`, `"diff_intraline"`), memoryLimit
+is an approximation of the total number of bytes stored by the cache.
+Larger entries that represent bigger patch sets or longer source files
+will consume a bigger portion of the memoryLimit. For these caches the
+memoryLimit should be set to roughly the amount of RAM (in bytes) the
+administrator can dedicate to the cache.
 +
 Default is 1024 for most caches, except:
 +
 * `"adv_bases"`: default is `4096`
-* `"diff"`: default is `128`
-* `"diff_intraline"`: default is `128`
+* `"diff"`: default is `10m` (10 MiB of memory)
+* `"diff_intraline"`: default is `10m` (10 MiB of memory)
+* `"plugin_resources"`: default is 2m (2 MiB of memory)
+
++
+If set to 0 the cache is disabled. Entries are removed immediately
+after being stored by the cache. This is primarily useful for testing.
 
 [[cache.name.diskLimit]]cache.<name>.diskLimit::
 +
-Maximum number of cache items to retain on disk, if this cache
-supports storing its items to disk.  Like memoryLimit, this is
-total number of items, not bytes of disk used.  If 0, disk storage
-for this cache is disabled.
+Total size in bytes of the keys and values stored on disk. Caches that
+have grown bigger than this size are scanned daily at 1 AM local
+server time to trim the cache. Entries are removed in least recently
+accessed order until the cache fits within this limit.  Caches may
+grow larger than this during the day, as the size check is only
+performed once every 24 hours.
 +
-Default is 16384.
-
-[[cache.name.diskBuffer]]cache.<name>.diskBuffer::
+Default is 128 MiB per cache.
 +
-Number of bytes to buffer in memory before writing less frequently
-accessed cache items to disk, if this cache supports storing its
-items to disk.
-+
-Default is 5 MiB.
-+
-Common unit suffixes of 'k', 'm', or 'g' are supported.
+If 0, disk storage for the cache is disabled.
 
 [[cache_names]]Standard Caches
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -447,14 +456,10 @@
 directory and file levels.  Gerrit uses this cache to accelerate
 the display of affected file names, as well as file contents.
 +
-Entries in this cache are relatively large, so the memory limit
-should not be set incredibly high.  Administrators should try to
-target cache.diff.memoryLimit to be roughly the number of changes
-which their users will process in a 1 or 2 day span.
-+
-Keeping entries for 90 days gives sufficient time for most changes
-to be submitted or abandoned before their relevant difference items
-expire out.
+Entries in this cache are relatively large, so memoryLimit is an
+estimate in bytes of memory used. Administrators should try to target
+cache.diff.memoryLimit to fit all changes users will view in a 1 or 2
+day span.
 
 cache `"diff_intraline"`::
 +
@@ -462,14 +467,10 @@
 between two commits. Gerrit uses this cache to accelerate display of
 intraline differences when viewing a file.
 +
-Entries in this cache are relatively large, so the memory limit
-should not be set incredibly high.  Administrators should try to
-target cache.diff.memoryLimit to be roughly the number of changes
-which their users will process in a 1 or 2 day span.
-+
-Keeping entries for 90 days gives sufficient time for most changes
-to be submitted or abandoned before their relevant difference items
-expire out.
+Entries in this cache are relatively large, so memoryLimit is an
+estimate in bytes of memory used. Administrators should try to target
+cache.diff.memoryLimit to fit all files users will view in a 1 or 2
+day span.
 
 cache `"git_tags"`::
 +
@@ -512,11 +513,17 @@
 
 cache `"permission_sort"`::
 +
-Caches the order access control sections must be applied to a
+Caches the order in which access control sections must be applied to a
 reference.  Sorting the sections can be expensive when regular
 expressions are used, so this cache remembers the ordering for
 each branch.
 
+cache `"plugin_resources"`::
++
+Caches formatted plugin resources, such as plugin documentation that
+has been converted from Markdown to HTML. The memoryLimit refers to
+the bytes of memory dedicated to storing the documentation.
+
 cache `"projects"`::
 +
 Caches the project description records, from the `projects` table
@@ -550,8 +557,8 @@
 unable to persist the session information.  Enabling a disk cache
 is strongly recommended.
 +
-Session storage is relatively inexpensive, the average entry in
-this cache is approximately 248 bytes, depending on the JVM.
+Session storage is relatively inexpensive. The average entry in
+this cache is approximately 346 bytes.
 
 See also link:cmd-flush-caches.html[gerrit flush-caches].
 
@@ -563,7 +570,7 @@
 Number of idle worker threads to maintain for the intraline difference
 computations.  There is no upper bound on how many concurrent requests
 can occur at once, if additional threads are started to handle a peak
-load, only this many will remaining idle afterwards.
+load, only this many will remain idle afterwards.
 +
 Default is 1.5x number of available CPUs.
 
@@ -639,7 +646,7 @@
 to changes which reference it.  The second configuration 'bugzilla'
 will hyperlink terms such as 'bug 42' to an external bug tracker,
 supplying the argument record number '42' for display.  The third
-configuration 'tracker' uses raw HTML to more preciously control
+configuration 'tracker' uses raw HTML to more precisely control
 how the replacement is displayed to the user.
 
 ----
@@ -1069,11 +1076,6 @@
 by the system administrator, and might not even be running on the
 same host as Gerrit.
 
-[[gerrit.replicateOnStartup]]gerrit.replicateOnStartup::
-+
-If true, replicates to all remotes on startup to ensure they are
-in-sync with this server.  By default, true.
-
 [[gitweb]]Section gitweb
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -1193,6 +1195,21 @@
 Optional filename for the change abandoned hook, if not specified then
 `change-abandoned` will be used.
 
+[[hooks.changeRestoredHook]]hooks.changeRestoredHook::
++
+Optional filename for the change restored hook, if not specified then
+`change-restored` will be used.
+
+[[hooks.refUpdatedHook]]hooks.refUpdatedHook::
++
+Optional filename for the ref updated hook, if not specified then
+`ref-updated` will be used.
+
+[[hooks.claSignedHook]]hooks.claSignedHook::
++
+Optional filename for the CLA signed hook, if not specified then
+`cla-signed` will be used.
+
 [[http]]Section http
 ~~~~~~~~~~~~~~~~~~~~
 
@@ -1316,7 +1333,7 @@
 [[httpd.sslKeyPassword]]httpd.sslKeyPassword::
 +
 Password used to decrypt the private portion of the sslKeyStore.
-Java key stores require a password, even if the administrator
+Java keystores require a password, even if the administrator
 doesn't want to enable one.
 +
 If set to the empty string the embedded server will prompt for the
@@ -1336,7 +1353,7 @@
 [[httpd.acceptorThreads]]httpd.acceptorThreads::
 +
 Number of worker threads dedicated to accepting new incoming TCP
-connections and allocate them connection-specific resources.
+connections and allocating them connection-specific resources.
 +
 By default, 2, which should be suitable for most high-traffic sites.
 
@@ -1364,7 +1381,7 @@
 
 [[httpd.maxWait]]httpd.maxWait::
 +
-Maximum amount of time a client will wait to for an available
+Maximum amount of time a client will wait for an available
 thread to handle a project clone, fetch or push request over the
 smart HTTP transport.
 +
@@ -1389,7 +1406,7 @@
 [[ldap]]Section ldap
 ~~~~~~~~~~~~~~~~~~~~
 
-LDAP integration is only enabled if `auth.type` was set to
+LDAP integration is only enabled if `auth.type` is set to
 `HTTP_LDAP`, `LDAP` or `CLIENT_SSL_CERT_LDAP`.  See above for a
 detailed description of the auth.type settings and their
 implications.
@@ -1457,7 +1474,7 @@
 _(Optional)_ The read timeout for an LDAP operation. The value is
 in the usual time-unit format like "1 s", "100 ms", etc...
 A timeout can be used to avoid blocking all of the SSH command start
-threads in case when the LDAP server becomes slow.
+threads in case the LDAP server becomes slow.
 +
 By default there is no timeout and Gerrit will wait for the LDAP
 server to respond until the TCP connection times out.
@@ -1504,8 +1521,8 @@
 Typically this is the `displayName` property in LDAP, but could
 also be `legalName` or `cn`.
 +
-Attribute values may be concatenated with literal strings, for
-example to join given name and surname together use the pattern
+Attribute values may be concatenated with literal strings.  For
+example to join given name and surname together, use the pattern
 `${givenName} ${SN}`.
 +
 If set, users will be unable to modify their full name field, as
@@ -1526,7 +1543,7 @@
 `${sAMAccountName.toLowerCase}@example.com`.
 +
 If set, the preferred email address will be prefilled from LDAP,
-but users may still be able to register additional email address,
+but users may still be able to register additional email addresses,
 and select a different preferred email address.
 +
 Default is `mail`.
@@ -1616,7 +1633,7 @@
 +
 If set, it must be ensured that the local usernames for all existing
 accounts are converted to lower case, otherwise a user that has a
-local username that contains upper case characters cannot login
+local username that contains upper case characters will not be able to login
 anymore. The local usernames for the existing accounts can be
 converted to lower case by running the server program
 link:pgm-LocalUsernamesToLowerCase.html[LocalUsernamesToLowerCase].
@@ -1726,7 +1743,7 @@
 and the push operation will fail. If set to zero then there is no
 limit.
 +
-Gerrit administrator can use this setting to prevent developers
+Gerrit administrators can use this setting to prevent developers
 from pushing objects which are too large to Gerrit.
 +
 Default is zero.
@@ -1877,6 +1894,22 @@
 +
 By default, unset, permitting delivery to any email address.
 
+[[sendemail.includeDiff]]sendemail.includeDiff::
++
+If true, new change emails from Gerrit will include the complete
+unified diff of the change. Variable maxmimumDiffSize places an upper
+limit on how large the email can get when this option is enabled.
++
+By default, false.
+
+[[sendemail.maximumDiffSize]]sendemail.maximumDiffSize::
++
+Largest size of unified diff output to include in an email. When
+the diff exceeds this size the file paths will be listed instead.
+Standard byte unit suffixes are supported.
++
+By default, 256 KiB.
+
 [[sendemail.importance]]sendemail.importance::
 +
 If present, emails sent from Gerrit will have the given level
@@ -1920,6 +1953,39 @@
 and text results for changes. If false, the URL is disabled and
 returns 404 to clients. Default is true, enabling `/query`.
 
+[[site.upgradeSchemaOnStartup]]site.upgradeSchemaOnStartup::
++
+Control whether schema upgrade should be done on Gerrit startup. The following
+values are supported:
++
+* `OFF`
++
+No automatic schema upgrade on startup.
++
+* `AUTO`
++
+Perform schema migration on startup, if necessary.  If, as a result of
+schema migration, there would be any unused database objects they will
+be dropped automatically.
++
+* `AUTO_NO_PRUNE`
++
+Like `AUTO` but unused database objects will not be pruned.
+
++
+The default is `OFF`.
+
+[[ssh-alias]] Section ssh-alias
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Variables in section ssh-alias permit the site administrator to alias
+another command from Gerrit or a plugin into the `gerrit` command
+namespace. To alias `replication start` to `gerrit replicate`:
+
+----
+[ssh-alias]
+  replicate = replication start
+----
 
 [[sshd]] Section sshd
 ~~~~~~~~~~~~~~~~~~~~~
@@ -1982,7 +2048,7 @@
 +
 Number of threads to use when executing SSH command requests.
 If additional requests are received while all threads are busy they
-are queued and serviced in a first-come-first-serve order.
+are queued and serviced in a first-come-first-served order.
 +
 By default, 1.5x the number of CPUs available to the JVM.
 
@@ -2049,7 +2115,7 @@
 +
 Maximum number of concurrent SSH sessions that a user account
 may open at one time.  This is the number of distinct SSH logins
-the each user may have active at one time, and is not related to
+that each user may have active at one time, and is not related to
 the number of commands a user may issue over a single connection.
 If set to 0, there is no limit.
 +
@@ -2197,7 +2263,7 @@
 external tracking id part of the footer line. The match can
 result in several entries in the DB.  If grouping is used in the
 regex the first group will be interpreted as the tracking id.
-Tracking ids > 20 char will be ignored.
+Tracking ids longer than 20 characters will be ignored.
 +
 The configuration file parser eats one level of backslashes, so the
 character class `\s` requires `\\s` in the configuration file.  The
@@ -2206,7 +2272,7 @@
 
 [[trackingid.name.system]]trackingid.<name>.system::
 +
-The name of the external tracking system(max 10 char).
+The name of the external tracking system (maximum 10 characters).
 It is possible to have several trackingid entries for the same
 tracking system.
 
@@ -2307,15 +2373,6 @@
   password = s3kr3t
 ----
 
-File `etc/replication.config`
------------------------------
-
-The optional file `'$site_path'/etc/replication.config` controls how
-Gerrit automatically replicates changes it makes to any of the Git
-repositories under its control.
-
-* link:config-replication.html[Git Replication/Mirroring]
-
 File `etc/peer_keys`
 --------------------
 
@@ -2351,7 +2408,6 @@
 Other files support site customization.
 +
 * link:config-headerfooter.html[Site Header/Footer]
-* link:config-replication.html[Git Replication/Mirroring]
 
 GERRIT
 ------
diff --git a/Documentation/config-gitweb.txt b/Documentation/config-gitweb.txt
index a08eb87..35d5c0d 100644
--- a/Documentation/config-gitweb.txt
+++ b/Documentation/config-gitweb.txt
@@ -27,7 +27,7 @@
 Alternatively, if Gerrit is served behind reverse proxy, it can
 generate different URLs for gitweb's links (they need to be
 rewritten to `<gerrit>/gitweb?args` on the web server). This allows
-for serving gitweb under different URL than the Gerrit instance.
+for serving gitweb under a different URL than the Gerrit instance.
 To enable this feature, set both: `gitweb.cgi` and `gitweb.url`.
 
 ====
diff --git a/Documentation/config-headerfooter.txt b/Documentation/config-headerfooter.txt
index c06080b..ae5d8f7 100644
--- a/Documentation/config-headerfooter.txt
+++ b/Documentation/config-headerfooter.txt
@@ -42,7 +42,7 @@
 or `GerritSite.css` by the relative URL `static/$name`
 (e.g. `static/logo.png`).
 
-To simplify security management, only files are served from
+To simplify security management, files are only served from
 `'$site_path'/static`.  Subdirectories are explicitly forbidden from
 being served from this location by enforcing the rule that file names
 cannot contain `/` or `\`.  (Client requests for `static/foo/bar`
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index ceb7c78..ec45837 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -24,7 +24,7 @@
 ~~~~~~~~~~~~~~~~
 
 This is called whenever a patchset is created (this includes new
-changes)
+changes).
 
 ====
   patchset-created --change <change id> --change-url <change url> --project <project name> --branch <branch> --uploader <uploader> --commit <sha1> --patchset <patchset id>
@@ -58,7 +58,7 @@
 ====
 
 change-restored
-~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~
 
 Called whenever a change has been restored.
 
@@ -76,9 +76,9 @@
 ====
 
 cla-signed
-~~~~~~~~~~~
+~~~~~~~~~~
 
-Called whenever a user signs a contributor license agreement
+Called whenever a user signs a contributor license agreement.
 
 ====
   cla-signed --submitter <submitter> --user-id <user_id> --cla-id <cla_id>
@@ -88,13 +88,14 @@
 Configuration Settings
 ----------------------
 
-It is possible to change where gerrit looks for hooks, and what
-filenames it looks for by adding a [hooks] section to gerrit.config.
+It is possible to change where Gerrit looks for hooks, and what
+filenames it looks for, by adding a [hooks] section in gerrit.config.
 
-Gerrit will use the value of hooks.path for the hooks directory, and
-the values of hooks.patchsetCreatedHook, hooks.commentAddedHook,
-hooks.changeMergedHook and hooks.changeAbandonedHook for the
-filenames for the hooks.
+Gerrit will use the value of hooks.path for the hooks directory.
+
+For the hook filenames, Gerrit will use the values of hooks.patchsetCreatedHook,
+hooks.commentAddedHook, hooks.changeMergedHook, hooks.changeAbandonedHook,
+hooks.changeRestoredHook, hooks.refUpdatedHook and hooks.claSignedHook.
 
 Missing Change URLs
 -------------------
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index 8aa7d08..bdf0c3a 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -20,7 +20,7 @@
 Supported Mail Templates:
 -------------------------
 
-Each mail that Gerrit sends out is controlled by at least one template, these
+Each mail that Gerrit sends out is controlled by at least one template.  These
 are listed below.  Change emails are influenced by two additional templates,
 one to set the subject line, and one to set the footer which gets appended to
 all the change emails (see `ChangeSubject.vm` and `ChangeFooter.vm` below.)
@@ -36,7 +36,7 @@
 ~~~~~~~~~~~~~~~
 
 The `ChangeFooter.vm` template will determine the contents of the footer
-text that will be appended to emails related to changes (all `ChangeEmails)`.
+text that will be appended to emails related to changes (all `ChangeEmail`s).
 
 ChangeSubject.vm
 ~~~~~~~~~~~~~~~~
@@ -49,6 +49,7 @@
 
 The `Comment.vm` template will determine the contents of the email related to
 a user submitting comments on changes.  It is a `ChangeEmail`: see
+`ChangeSubject.vm` and `ChangeFooter.vm`.
 
 Merged.vm
 ~~~~~~~~~
@@ -62,6 +63,7 @@
 
 The `MergeFail.vm` template will determine the contents of the email related
 to a failure upon attempting to merge a change to the head.  It is a
+`ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
 
 NewChange.vm
 ~~~~~~~~~~~~
@@ -107,7 +109,7 @@
 Warning
 ~~~~~~~
 
-Be aware that modifying templates can cause them to fail to parse and therefor
+Be aware that modifying templates can cause them to fail to parse and therefore
 not send out the actual email, or worse, calling methods on the available
 objects could have internal side effects which would adversely affect the
 health of your Gerrit server and/or data.
@@ -125,7 +127,7 @@
 
 $messageClass::
 +
-A String containing the messageClass
+A String containing the messageClass.
 
 $StringUtils::
 +
@@ -139,35 +141,35 @@
 
 $change::
 +
-A reference to the current `Change` object
+A reference to the current `Change` object.
 
 $changeId::
 +
-Id of the current change (a `Change.Key`)
+Id of the current change (a `Change.Key`).
 
 $coverLetter::
 +
-The text of the `ChangeMessage`
+The text of the `ChangeMessage`.
 
 $branch::
 +
-A reference to the branch of this change (a `Branch.NameKey`)
+A reference to the branch of this change (a `Branch.NameKey`).
 
 $fromName::
 +
-The name of the from user
+The name of the from user.
 
 $projectName::
 +
-The name of this change's project
+The name of this change's project.
 
 $patchSet::
 +
-A reference to the current `PatchSet`
+A reference to the current `PatchSet`.
 
 $patchSetInfo::
 +
-A reference to the current `PatchSetInfo`
+A reference to the current `PatchSetInfo`.
 
 
 See Also
diff --git a/Documentation/config-replication.txt b/Documentation/config-replication.txt
deleted file mode 100644
index 772282e..0000000
--- a/Documentation/config-replication.txt
+++ /dev/null
@@ -1,273 +0,0 @@
-Gerrit Code Review - Git Replication
-====================================
-
-Gerrit can automatically push any changes it makes to its managed Git
-repositories to another system.  Usually this would be configured to
-provide mirroring of changes, for warm-standby backups, or a
-load-balanced public mirror farm.
-
-The replication runs on a short delay.  This gives Gerrit a small
-time window to batch updates going to the same project, such as
-when a user uploads multiple changes at once.
-
-Typically replication should be done over SSH, with a passwordless
-public/private key pair.  On a trusted network it is also possible to
-use replication over the insecure (but much faster) git:// protocol,
-by enabling the `receive-pack` service on the receiving system, but
-this configuration is not recommended.  It is also possible to
-specify a local path as replication target. This makes e.g. sense if
-a network share is mounted to which the repositories should be
-replicated.
-
-Enabling Replication
---------------------
-
-If replicating over SSH (recommended), ensure the host key of the
-remote system(s) is already in the Gerrit user's `~/.ssh/known_hosts`
-file.  The easiest way to add the host key is to connect once by hand
-with the command line:
-
-====
-  sudo su -c 'ssh mirror1.us.some.org echo' gerrit2
-====
-
-Next, create `'$site_path'/etc/replication.config` as a Git-style
-config file, and restart Gerrit.
-
-Example `replication.config` to replicate in parallel to four
-different hosts:
-
-====
-  [remote "host-one"]
-    url = gerrit2@host-one.example.com:/some/path/${name}.git
-
-  [remote "pubmirror"]
-    url = mirror1.us.some.org:/pub/git/${name}.git
-    url = mirror2.us.some.org:/pub/git/${name}.git
-    url = mirror3.us.some.org:/pub/git/${name}.git
-    push = +refs/heads/*:refs/heads/*
-    push = +refs/tags/*:refs/tags/*
-    threads = 3
-    authGroup = Public Mirror Group
-    authGroup = Second Public Mirror Group
-====
-
-To manually trigger replication at runtime, see
-link:cmd-replicate.html[gerrit replicate].
-
-[[replication_config]]File `replication.config`
------------------------------------------------
-
-The optional file `'$site_path'/etc/replication.config` is a
-Git-style config file that controls the replication settings for
-Gerrit.
-
-The file is composed of one or more `remote` sections, each remote
-section provides common configuration settings for one or more
-destination URLs.
-
-Each remote section uses its own thread pool.  If pushing to
-multiple remotes, over differing types of network connections
-(e.g. LAN and also public Internet), its a good idea to put them
-into different remote sections, so that replication to the slower
-connection does not starve out the faster local one.  The example
-file above does this.
-
-[[remote]]Section remote
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-In the keys below, the <name> portion is unused by Gerrit, but must be
-unique to distinguish the different sections if more than one remote
-section appears in the file.
-
-[[remote.name.url]]remote.<name>.url::
-+
-Address of the remote server to push to.  Multiple URLs may
-be specified within a single remote block, listing different
-destinations which share the same settings.  Assuming sufficient
-threads in the thread pool, Gerrit pushes to all URLs in parallel,
-using one thread per URL.
-+
-Within each URL value the magic placeholder `${name}` is replaced
-with the Gerrit project name.  This is a Gerrit specific extension
-to the otherwise standard Git URL syntax and it must be included
-in each URL so that Gerrit can figure out where each project needs
-to be replicated.
-+
-See link:http://www.kernel.org/pub/software/scm/git/docs/git-push.html#URLS[GIT URLS]
-for details on Git URL syntax.
-
-[[remote.name.url]]remote.<name>.adminUrl::
-+
-Address of the alternative remote server only for repository creation.  Multiple URLs may
-be specified within a single remote block, listing different
-destinations which share the same settings.
-+
-The adminUrl can be used as a ssh alternative to the url option, but only related to repository creation.
-If not specified, the repository creation tries to follow the default way through the url value specified.
-+
-It is useful when remote.<name>.url protocols does not allow repository creation
-although their usage are mandatory in the local environment.
-In that case, an alternative ssh url could be specified to repository creation.
-
-[[remote.name.receivepack]]remote.<name>.receivepack::
-+
-Path of the `git-receive-pack` executable on the remote system, if
-using the SSH transport.
-+
-Defaults to `git-receive-pack`.
-
-[[remote.name.uploadpack]]remote.<name>.uploadpack::
-+
-Path of the `git-upload-pack` executable on the remote system, if
-using the SSH transport.
-+
-Defaults to `git-upload-pack`.
-
-[[remote.name.push]]remote.<name>.push::
-+
-Standard Git refspec denoting what should be replicated.  Setting this
-to `+refs/heads/*:refs/heads/*` would mirror only the active
-branches, but not the change refs under `refs/changes/`, or the tags
-under `refs/tags/`.
-+
-Multiple push keys can be supplied, to specify multiple patterns
-to match against.  In the example file above, remote "pubmirror"
-uses two push keys to match both `refs/heads/*` and `refs/tags/*`,
-but excludes all others, including `refs/changes/*`.
-+
-Defaults to `+refs/*:refs/*` (all refs) if not specified.
-
-[[remote.name.timeout]]remote.<name>.timeout::
-+
-Number of seconds to wait for a network read or write to complete
-before giving up and declaring the remote side is not responding.
-If 0, there is no timeout, and the push client waits indefinitely.
-+
-A timeout should be large enough to mostly transfer the objects to
-the other side.  1 second may be too small for larger projects,
-especially over a WAN link, while 10-30 seconds is a much more
-reasonable timeout value.
-+
-Defaults to 0 seconds, wait indefinitely.
-
-[[remote.name.replicationDelay]]remote.<name>.replicationDelay::
-+
-Number of seconds to wait before scheduling a remote push operation.
-Setting the delay to 0 effectively disables the delay, causing the
-push to start as soon as possible.
-+
-This is a Gerrit specific extension to the Git remote block.
-+
-By default, 15 seconds.
-
-[[remote.name.replicationRetry]]remote.<name>.replicationRetry::
-+
-Number of minutes to wait before scheduling a remote push operation
-previously failed due to an offline remote server.
-+
-If a remote push operation fails because a remote server was
-offline, all push operations to the same destination URL are
-blocked, and the remote push is continuously retried.
-+
-This is a Gerrit specific extension to the Git remote block.
-+
-By default, 1 minute.
-
-[[remote.name.threads]]remote.<name>.threads::
-+
-Number of worker threads to dedicate to pushing to the repositories
-described by this remote.  Each thread can push one project at a
-time, to one destination URL.  Scheduling within the thread pool
-is done on a per-project basis.  If a remote block describes 4
-URLs, allocating 4 threads in the pool will permit some level of
-parallel pushing.
-+
-By default, 1 thread.
-
-[[remote.name.authGroup]]remote.<name>.authGroup::
-+
-Specifies the name of a group that the remote should use to access
-the repositories. Multiple authGroups may be specified within a
-single remote block to signify a wider access right. In the project
-administration web interface the read access can be specified for
-this group to control if a project should be replicated or not to the
-remote.
-+
-By default, replicates without group control, i.e replicates
-everything to all remotes.
-
-[[remote.name.replicatePermissions]]remote.<name>.replicatePermissions::
-+
-If true, permissions-only projects and the refs/meta/config branch
-will also be replicated to the remote site.  These projects and
-branches may be needed to keep a backup or slave server current.
-+
-By default, true, replicating everything.
-
-[[remote.name.mirror]]remote.<name>.mirror::
-+
-If true, replication will remove remote branches that absent locally
-or invisible to the replication (i.e. read access denied via 'authGroup'
-option).
-+
-By default, false, do not remove remote branches.
-
-
-[[secure_config]]File `secure.config`
------------------------------------------------
-
-The optional file `'$site_path'/secure.config` is a Git-style config
-file that provides secure values that should not be world-readable,
-such as passwords. Passwords for HTTP remotes can be obtained from
-this file.
-
-[[remote.name.username]]remote.<name>.username::
-+
-Username to use for HTTP authentication on this remote, if not given
-in the URL.
-
-[[remote.name.password]]remote.<name>.password::
-+
-Password to use for HTTP authentication on this remote.
-
-
-[[ssh_config]]File `~/.ssh/config`
-----------------------------------
-
-If present, Gerrit reads and caches `~/.ssh/config` at startup, and
-supports most SSH configuration options.  For example:
-
-====
-  Host host-one.example.com:
-    IdentityFile ~/.ssh/id_hostone
-    PreferredAuthentications publickey
-
-  Host mirror*.us.some.org:
-    User mirror-updater
-    IdentityFile ~/.ssh/id_pubmirror
-    PreferredAuthentications publickey
-====
-
-Supported options:
-
- * Host
- * Hostname
- * User
- * Port
- * IdentityFile
- * PreferredAuthentications
- * StrictHostKeyChecking
-
-SSH authentication must be by passwordless public key, as there is
-no facility to read passphases on startup or passwords during the
-SSH connection setup, and SSH agents are not supported from Java.
-
-Host keys for any destination SSH servers must appear in the user's
-`~/.ssh/known_hosts` file, and must be added in advance, before
-Gerrit starts.  If a host key is not listed, Gerrit will be unable to
-connect to that destination, and replication to that URL will fail.
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-reverseproxy.txt b/Documentation/config-reverseproxy.txt
index 4eecf67..7161c4a 100644
--- a/Documentation/config-reverseproxy.txt
+++ b/Documentation/config-reverseproxy.txt
@@ -5,7 +5,7 @@
 -----------
 
 Gerrit can be configured to run behind a third-party web server.
-This allows the other web server to bind to the privileged ports 80
+This allows the other web server to bind to the privileged port 80
 (or 443 for SSL), as well as offloads the SSL processing overhead
 from Java to optimized native C code.
 
diff --git a/Documentation/config-sso.txt b/Documentation/config-sso.txt
index 9aa06be..e915ffb 100644
--- a/Documentation/config-sso.txt
+++ b/Documentation/config-sso.txt
@@ -146,7 +146,7 @@
 The auth.type must always be HTTP, indicating the user identity
 will be obtained from the HTTP authorization data.
 
-The auth.httpHeader indicates which HTTP header field the Siteminder
+The auth.httpHeader indicates in which HTTP header field the Siteminder
 product has stored the username.  Usually this is "SM_USER", but
 may differ in your environment.  Please refer to your organization's
 single sign-on or security group to ensure the setting is correct.
diff --git a/Documentation/dev-design.txt b/Documentation/dev-design.txt
index 5cd62e3..ce2868c 100644
--- a/Documentation/dev-design.txt
+++ b/Documentation/dev-design.txt
@@ -37,7 +37,7 @@
 
 Git is a distributed version control system, wherein each repository
 is assumed to be owned/maintained by a single user.  There are no
-inherit security controls built into Git, so the ability to read
+inherent security controls built into Git, so the ability to read
 from or write to a repository is controlled entirely by the host's
 filesystem access controls.  When multiple maintainers collaborate
 on a single shared repository a high degree of trust is required,
@@ -675,11 +675,12 @@
 this feature of PostgreSQL or MySQL to ensure the warm-standby is
 reasonably current should the master go offline.
 
-Gerrit can be configured to replicate changes made to the local
-Git repositories over any standard Git transports.  This can be
-configured in `'$site_path'/etc/replication.conf` to send copies
-of all changes over SSH to other servers, or to the Amazon S3 blob
-storage service.
+Using the standard replication plugin, Gerrit can be configured
+to replicate changes made to the local Git repositories over any
+standard Git transports. After the plugin is installed, remote
+destinations can be configured in `'$site_path'/etc/replication.conf`
+to send copies of all changes over SSH to other servers, or to the
+Amazon S3 blob storage service.
 
 
 Logging Plan
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index ca56da3..a19d85e 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -73,9 +73,15 @@
 Running Hosted Mode
 ~~~~~~~~~~~~~~~~~~~
 
-Import the gerrit-gwtdebug project:
+To debug the GWT code executing in the web browser, three additional Git
+repositories need to be cloned.
 
-* Import gerrit-gwtdebug/pom.xml using General -> Maven Projects
+* https://gerrit.googlesource.com/gwtexpui
+* https://gerrit.googlesource.com/gwtjsonrpc
+* https://gerrit.googlesource.com/gwtorm
+
+In Eclipse, import the pom.xml file in the root directory of each of
+these cloned gits via General -> Maven Projects.
 
 Duplicate the existing `gwtui_dbg` launch configuration:
 
@@ -97,12 +103,17 @@
 Known problems
 --------------
 
-When running Gerrit under the Eclipse debugger, code that attempts
+* When running Gerrit under the Eclipse debugger, code that attempts
 to load Prolog code may erroneously raise ClassNotFoundException,
 claiming that classes in the `Gerrit` package can't be found. The
 error can often be resolved by rebuilding Gerrit with `mvn package`
 and restarting the debug session.
 
+* OpenID authentication won't work in hosted mode, so you need to change
+the link:config-gerrit.html#auth.type[auth.type] configuration parameter
+to `DEVELOPMENT_BECOME_ANY_ACCOUNT` to disable OpenID and allow you to
+impersonate whatever account you otherwise would've used.
+
 
 GERRIT
 ------
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index f79b8c0..586ae07 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -1,39 +1,341 @@
 Gerrit Code Review - Plugin Development
 =======================================
 
-A plugin in gerrit is tightly coupled code that runs in the same
-JVM as gerrit. It has full access to all gerrit internals. Plugins
-are coupled to a specific major.minor gerrit version.
+A plugin in Gerrit is tightly coupled code that runs in the same
+JVM as Gerrit. It has full access to all server internals. Plugins
+are tightly coupled to a specific major.minor server version and
+may require source code changes to compile against a different
+server version.
 
-REQUIREMENTS
+An extension in Gerrit runs inside of the same JVM as Gerrit
+in the same way as a plugin, but has limited visibility to the
+server's internals. The limited visiblity reduces the extension's
+dependencies, enabling it to be compatiable across a wider range
+of server versions.
+
+Most of this documentation refers to either type as a plugin.
+
+Requirements
 ------------
 
-To start development, you may download the sample maven project, which downloads
-the following dependencies;
+To start development, clone the sample maven project:
 
-* gerrit-sdk.jar file that matches the war file you are developing against
+----
+$ git clone https://gerrit.googlesource.com/plugins/helloworld
+----
 
+This project includes the dependencies file that matches the war file to
+develop against. Dependencies are offered in two different formats:
+
+gerrit-extension-api.jar::
+  A stable but thin interface. Suitable for extensions that need
+  to be notified of events, but do not require tight coupling to
+  the internals of Gerrit. Extensions built against this API can
+  expect to be binary compatible across a wide range of server
+  versions.
+
+gerrit-plugin-api.jar::
+  The complete internals of the Gerrit server, permitting a
+  plugin to tightly couple itself and provide additional
+  functionality that is not possible as an extension. Plugins
+  built against this API are expected to break at the source
+  code level between every major.minor Gerrit release. A plugin
+  that compiles against 2.5 will probably need source code level
+  changes to work with 2.6, 2.7, and so on.
 
 Manifest
 --------
 
-Plugins need to include the following data in the jar manifest file;
-Gerrit-Plugin = plugin_name
-Gerrit-Module = pkg.class
+Plugins may provide optional description information with standard
+manifest fields:
+
+====
+  Implementation-Title: Example plugin showing examples
+  Implementation-Version: 1.0
+  Implementation-Vendor: Example, Inc.
+  Implementation-URL: http://example.com/opensource/plugin-foo/
+====
+
+ApiType
+~~~~~~~
+
+Plugins using the tightly coupled `gerrit-plugin-api.jar` must
+declare this API dependency in the manifest to gain access to server
+internals. If no Gerrit-ApiType is specified the stable `extension`
+API will be assumed. This may cause ClassNotFoundExceptions when
+loading a plugin that needs the plugin API.
+
+====
+  Gerrit-ApiType: plugin
+====
+
+Explicit Registration
+~~~~~~~~~~~~~~~~~~~~~
+
+Plugins that use explicit Guice registration must name the Guice
+modules in the manifest. Up to three modules can be named in the
+manifest. Gerrit-Module supplies bindings to the core server;
+Gerrit-SshModule supplies SSH commands to the SSH server (if
+enabled); Gerrit-HttpModule supplies servlets and filters to the HTTP
+server (if enabled). If no modules are named automatic registration
+will be performed by scanning all classes in the plugin JAR for
+`@Listen` and `@Export("")` annotations.
+
+====
+  Gerrit-Module:     tld.example.project.CoreModuleClassName
+  Gerrit-SshModule:  tld.example.project.SshModuleClassName
+  Gerrit-HttpModule: tld.example.project.HttpModuleClassName
+====
+
+Reload Method
+~~~~~~~~~~~~~
+
+If a plugin holds an exclusive resource that must be released before
+loading the plugin again (for example listening on a network port or
+acquiring a file lock) the manifest must declare Gerrit-ReloadMode
+to be `restart`. Otherwise the preferred method of `reload` will
+be used, as it enables the server to hot-patch an updated plugin
+with no down time.
+
+====
+  Gerrit-ReloadMode: restart
+====
+
+In either mode ('restart' or 'reload') any plugin or extension can
+be updated without restarting the Gerrit server. The difference is
+how Gerrit handles the upgrade:
+
+restart::
+  The old plugin is completely stopped. All registrations of SSH
+  commands and HTTP servlets are removed. All registrations of any
+  extension points are removed. All registered LifecycleListeners
+  have their `stop()` method invoked in reverse order. The new
+  plugin is started, and registrations are made from the new
+  plugin. There is a brief window where neither the old nor the
+  new plugin is connected to the server. This means SSH commands
+  and HTTP servlets will return not found errors, and the plugin
+  will not be notified of events that occurred during the restart.
+
+reload::
+  The new plugin is started. Its LifecycleListeners are permitted
+  to perform their `start()` methods. All SSH and HTTP registrations
+  are atomically swapped out from the old plugin to the new plugin,
+  ensuring the server never returns a not found error. All extension
+  point listeners are atomically swapped out from the old plugin to
+  the new plugin, ensuring no events are missed (however some events
+  may still route to the old plugin if the swap wasn't complete yet).
+  The old plugin is stopped.
+
+Classpath
+---------
+
+Each plugin is loaded into its own ClassLoader, isolating plugins
+from each other. A plugin or extension inherits the Java runtime
+and the Gerrit API chosen by `Gerrit-ApiType` (extension or plugin)
+from the hosting server.
+
+Plugins are loaded from a single JAR file. If a plugin needs
+additional libraries, it must include those dependencies within
+its own JAR. Plugins built using Maven may be able to use the
+link:http://maven.apache.org/plugins/maven-shade-plugin/[shade plugin]
+to package additional dependencies. Relocating (or renaming) classes
+should not be necessary due to the ClassLoader isolation.
 
 SSH Commands
 ------------
 
-You may develop plugins which provide commands that can be accessed through the SSH interface.
-These commands register themselves as a part of SSH Commands (link).
+Plugins may provide commands that can be accessed through the SSH
+interface (extensions do not have this option).
 
-Each of your plugins commands needs to extend BaseCommand.
+Command implementations must extend the base class SshCommand:
 
-Any plugin which implements at least one ssh command needs to also provide a class which extends
-the PluginCommandModule in order to register the ssh command(s) in its configure method which you
-must override.
+====
+  import com.google.gerrit.sshd.SshCommand;
 
-Registering is done by calling the command(String commandName).to(ClassName<? extends BaseCommand> klass)
+  class PrintHello extends SshCommand {
+    protected abstract void run() {
+      stdout.print("Hello\n");
+    }
+  }
+====
+
+If no Guice modules are declared in the manifest, SSH commands may
+use auto-registration by providing an @Export annotatation:
+
+====
+  import com.google.gerrit.extensions.annotations.Export;
+  import com.google.gerrit.sshd.SshCommand;
+
+  @Export("print")
+  class PrintHello extends SshCommand {
+    protected abstract void run() {
+      stdout.print("Hello\n");
+    }
+  }
+====
+
+If explicit registration is being used, a Guice module must be
+supplied to register the SSH command and declared in the manifest
+with the `Gerrit-SshModule` attribute:
+
+====
+  import com.google.gerrit.sshd.PluginCommandModule;
+
+  class MyCommands extends PluginCommandModule {
+    protected void configureCommands() {
+      command("print").to(PrintHello.class);
+    }
+  }
+====
+
+For a plugin installed as name `helloworld`, the command implemented
+by PrintHello class will be available to users as:
+
+----
+$ ssh -P 29418 review.example.com helloworld print
+----
+
+HTTP Servlets
+-------------
+
+Plugins or extensions may register additional HTTP servlets, and
+wrap them with HTTP filters.
+
+Servlets may use auto-registration to declare the URL they handle:
+
+====
+  import com.google.gerrit.extensions.annotations.Export;
+  import com.google.inject.Singleton;
+  import javax.servlet.http.HttpServlet;
+  import javax.servlet.http.HttpServletRequest;
+  import javax.servlet.http.HttpServletResponse;
+
+  @Export("/print")
+  @Singleton
+  class HelloServlet extends HttpServlet {
+    protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
+      res.setContentType("text/plain");
+      res.setCharacterEncoding("UTF-8");
+      res.getWriter().write("Hello");
+    }
+  }
+====
+
+If explicit registration is being used, a Guice ServletModule must
+be supplied to register the HTTP servlets, and the module must be
+declared in the manifest with the `Gerrit-HttpModule` attribute:
+
+====
+  import com.google.inject.servlet.ServletModule;
+
+  class MyWebUrls extends ServletModule {
+    protected void configureServlets() {
+      serve("/print").with(HelloServlet.class);
+    }
+  }
+====
+
+For a plugin installed as name `helloworld`, the servlet implemented
+by HelloServlet class will be available to users as:
+
+----
+$ curl http://review.example.com/plugins/helloworld/print
+----
+
+Documentation
+-------------
+
+If a plugin does not register a filter or servlet to handle URLs
+`/Documentation/*` or `/static/*`, the core Gerrit server will
+automatically export these resources over HTTP from the plugin JAR.
+
+Static resources under `static/` directory in the JAR will be
+available as `/plugins/helloworld/static/resource`.
+
+Documentation files under `Documentation/` directory in the JAR
+will be available as `/plugins/helloworld/Documentation/resource`.
+
+Documentation may be written in
+link:http://daringfireball.net/projects/markdown/[Markdown] style
+if the file name ends with `.md`. Gerrit will automatically convert
+Markdown to HTML if accessed with extension `.html`.
+
+Automatic Index
+~~~~~~~~~~~~~~~
+
+If a plugin does not handle its `/` URL itself, Gerrit will
+redirect clients to the plugin's `/Documentation/index.html`.
+Requests for `/Documentation/` (bare directory) will also redirect
+to `/Documentation/index.html`.
+
+If neither resource `Documentation/index.html` or
+`Documentation/index.md` exists in the plugin JAR, Gerrit will
+automatically generate an index page for the plugin's documentation
+tree by scanning every `*.md` and `*.html` file in the Documentation/
+directory.
+
+For any discovered Markdown (`*.md`) file, Gerrit will parse the
+header of the file and extract the first level one title. This
+title text will be used as display text for a link to the HTML
+version of the page.
+
+For any discovered HTML (`*.html`) file, Gerrit will use the name
+of the file, minus the `*.html` extension, as the link text. Any
+hyphens in the file name will be replaced with spaces.
+
+If a discovered file name beings with `cmd-` it will be clustered
+into a 'Commands' section of the generated index page. All other
+files are clustered under a 'Documentation' section.
+
+Some optional information from the manifest is extracted and
+displayed as part of the index page, if present in the manifest:
+
+[width="40%",options="header"]
+|===================================================
+|Field       | Source Attribute
+|Name        | Implementation-Title
+|Vendor      | Implementation-Vendor
+|Version     | Implementation-Version
+|URL         | Implementation-URL
+|API Version | Gerrit-ApiVersion
+|===================================================
+
+Deployment
+----------
+
+Compiled plugins and extensions can be deployed to a
+running Gerrit server using the SSH interface by any user with
+link:access-control.html#capability_administrateServer[Administrate Server]
+capability. Binaries can be specified in three different formats:
+
+* Absolute file path on the server's host. The server will copy
+  the plugin from this location to its own site path.
++
+----
+$ ssh -P 29418 localhost gerrit plugin install -n name $(pwd)/my-plugin.jar
+----
+
+* Valid URL, including any HTTP or FTP site reachable by the
+  server. The server will download the plugin and save a copy in
+  its own site path.
++
+----
+$ ssh -P 29418 localhost gerrit plugin install -n name http://build-server/output/our-plugin.jar
+----
+
+* As piped input to the plugin install command. The server will
+  copy input until EOF, and save a copy under its own site path.
++
+----
+$ ssh -P 29418 localhost gerrit plugin install -n name - <target/name-0.1.jar
+----
+
+Plugins can also be copied directly into the server's
+directory at `$site_path/plugins/$name.jar`.  The name of
+the JAR file, minus the `.jar` extension, will be used as the
+plugin name. Unless disabled, servers periodically scan this
+directory for updated plugins. The time can be adjusted by
+link:config-gerrit.html#plugins.checkFrequency[plugins.checkFrequency].
 
 GERRIT
 ------
diff --git a/Documentation/dev-release-subproject.txt b/Documentation/dev-release-subproject.txt
index a9d0553..b0607fd 100644
--- a/Documentation/dev-release-subproject.txt
+++ b/Documentation/dev-release-subproject.txt
@@ -4,43 +4,38 @@
 Preparing a New Gerrit Subproject Snapshot for Publishing
 ---------------------------------------------------------
 
-* You will need to have the following in the pom.xml to make it deployable to:
-gerrit-maven-repository.googlecode.com
+* You will need to have the following in the pom.xml to make it
+  deployable to the gerrit-maven storage bucket:
+
 ----
   <distributionManagement>
-    <snapshotRepository>
-      <id>gerrit-snapshot-repository</id>
-      <name>gerrit Snapshot Repository</name>
-      <url>dav:https://gerrit-maven-repository.googlecode.com/svn/</url>
-      <uniqueVersion>true</uniqueVersion>
-    </snapshotRepository>
-
     <repository>
-      <id>gerrit-maven-repository</id>
+      <id>gerrit-maven</id>
       <name>gerrit Maven Repository</name>
-      <url>dav:https://gerrit-maven-repository.googlecode.com/svn/</url>
+      <url>s3://gerrit-maven@commondatastorage.googleapis.com</url>
       <uniqueVersion>true</uniqueVersion>
     </repository>
   </distributionManagement>
 ----
 
 
-* Since ubuntu maven is incomplete, also add this to the pom.xml:
+* Add this to the pom.xml to enable the wagon provider:
 
 ----
   <build>
-   <extensions>
-        <extension>
-            <groupId>org.apache.maven.wagon</groupId>
-            <artifactId>wagon-webdav-jackrabbit</artifactId>
-            <version>1.0-beta-6</version>
-        </extension>
+    <extensions>
+      <extension>
+        <groupId>net.anzix.aws</groupId>
+        <artifactId>s3-maven-wagon</artifactId>
+        <version>3.2</version>
+      </extension>
     </extensions>
   </build>
 ----
 
 
-* Add your username and password to your ~/.m2/settings.xml file:
+* Add your username and password to your ~/.m2/settings.xml file.
+  These need to come from the link:https://code.google.com/apis/console/[API Console].
 
 ----
   <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
@@ -48,15 +43,9 @@
             xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
     <servers>
       <server>
-        <id>gerrit-maven-repository</id>
-          <username>JohnDoe@example.com</username>
-          <password>OpenSessame</password>
-      </server>
-
-      <server>
-        <id>gerrit-snapshot-repository</id>
-          <username>JohnDoe@example.com</username>
-          <password>OpenSessame</password>
+        <id>gerrit-maven</id>
+        <username>GOOG..EXAMPLE.....EXAMPLE</username>
+        <password>EXAMPLE..EXAMPLE..EXAMPLE</password>
       </server>
     </servers>
   </settings>
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index 83c47f3..21dc1c7 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -64,6 +64,9 @@
 Create the Actual Release
 ---------------------------
 
+In the example commands below we assume that the last release was '2.4' and that
+we are preparing '2.5' release.
+
 Prepare the Subprojects
 ~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -75,20 +78,21 @@
 Prepare Gerrit
 ~~~~~~~~~~~~~~
 
-* Update the top level pom in Gerrit to ensure that none of the Subprojects
-  point to snapshot releases
+* In the 'stable-2.5' branch: Update the top level pom in Gerrit to ensure that
+none of the Subprojects point to snapshot releases
 
-* Update the poms for the Gerrit version, push for review, get merged
+* In the 'master' branch: Update the poms for the Gerrit version, push for
+review, get merged
 
 ====
- tools/version.sh --snapshot=2.3
+ tools/version.sh --snapshot=2.5
 ====
 
 * Tag
 
 ====
- git tag -a -m "gerrit 2.2.2-rc0" v2.2.2-rc0
- git tag -a -m "gerrit 2.2.2.1" v2.2.2.1
+ git tag -a -m "gerrit 2.5-rc0" v2.5-rc0
+ git tag -a -m "gerrit 2.5" v2.5
 ====
 
 * Build
@@ -118,7 +122,7 @@
 ~~~~~~~~~~~~~~~~~~~
 
 * Push JAR to commondatastorage.googleapis.com
-** Run tools/deploy_plugin_api.sh
+** Run tools/deploy_api.sh
 
 Tag
 ~~~
@@ -126,7 +130,8 @@
 * Push the New Tag
 
 ====
- git push google refs/tags/v2.2.2.1:refs/tags/v2.2.2.1
+ git push google refs/tags/v2.5-rc0:refs/tags/v2.5-rc0
+ git push google refs/tags/v2.5:refs/tags/v2.5
 ====
 
 
@@ -134,7 +139,7 @@
 ~~~~
 
 ====
- make -C Documentation PRIOR=2.2.2 update
+ make -C Documentation PRIOR=2.4 update
  make -C ReleaseNotes update
 ====
 
@@ -142,7 +147,9 @@
 
 * Update Google Code project links
 ** Go to http://code.google.com/p/gerrit/admin
-** Point the main page to the new docs
+** Point the main page to the new docs. The link to the documentation has to be
+updated at two places: in the project description and also in the Links
+section.
 ** Point the main page to the new release notes
 
 [NOTE]
@@ -154,7 +161,11 @@
 
     https://code.google.com/hosting/settings
 
-(requires overriding svn username on command line)
+If subversion assumes a different username than your google one and asks for a
+password right away simply hit enter. Subversion will fail and then ask for
+another username and password. This time enter the username and password from
+the page linked above. After that subversion should save the username/password
+somewhere under `~/.subversion/auth` folder.
 ========================================================================
 
 
@@ -181,7 +192,7 @@
 Mailing List
 ~~~~~~~~~~~~
 
-* Send an email to the mailing list to annouce the release
+* Send an email to the mailing list to announce the release
 * Consider including some or all of the following in the email:
 ** A link to the release and the release notes (if a final release)
 ** A link to the docs
diff --git a/Documentation/error-branch-not-found.txt b/Documentation/error-branch-not-found.txt
index e2dcff1..2aad0e1 100644
--- a/Documentation/error-branch-not-found.txt
+++ b/Documentation/error-branch-not-found.txt
@@ -7,8 +7,8 @@
 To push a change for code review the commit has to be pushed to the
 project's magical `refs/for/'branch'` ref (for details have a look at
 link:user-upload.html#push_create[Create Changes]).
-If you specify a non existing branch in the `refs/for/'branch'` ref
-the push is failing with the error message 'branch ... not found'.
+If you specify a non-existing branch in the `refs/for/'branch'` ref
+the push fails with the error message 'branch ... not found'.
 
 To fix this problem verify
 
diff --git a/Documentation/error-change-closed.txt b/Documentation/error-change-closed.txt
index 7170a65..74e7c48 100644
--- a/Documentation/error-change-closed.txt
+++ b/Documentation/error-change-closed.txt
@@ -14,7 +14,7 @@
 new change. To do this you have to remove the Change-Id from the
 commit message as explained link:error-push-fails-due-to-commit-message.html[here] and ideally generate a new Change-Id
 using the link:cmd-hook-commit-msg.html[commit hook] or EGit. Before pushing again it is also
-recommendable to do a link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[git rebase] to base your commit on the submitted
+recommended to do a link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[git rebase] to base your commit on the submitted
 change. Pushing again should now create a new change in Gerrit.
 
 If the change for which you wanted to upload a new patch set was
diff --git a/Documentation/error-change-does-not-belong-to-project.txt b/Documentation/error-change-does-not-belong-to-project.txt
index 29957e1..e747881 100644
--- a/Documentation/error-change-does-not-belong-to-project.txt
+++ b/Documentation/error-change-does-not-belong-to-project.txt
@@ -7,7 +7,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-ID's 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 c9ac0d8..b6df13b 100644
--- a/Documentation/error-change-not-found.txt
+++ b/Documentation/error-change-not-found.txt
@@ -7,7 +7,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-ID's 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-no-changes-made.txt b/Documentation/error-no-changes-made.txt
index 7ef7082..d0e1d4f 100644
--- a/Documentation/error-no-changes-made.txt
+++ b/Documentation/error-no-changes-made.txt
@@ -2,10 +2,10 @@
 ===============
 
 With this error message Gerrit rejects to push a commit as a new
-patch set for a change, if the pushed commit is identical with the
+patch set for a change, if the pushed commit is identical to the
 current patch set of this change.
 
-A pushed commit is considered to be identical with the current patch
+A pushed commit is considered to be identical to the current patch
 set if
 
 - the files in the commit,
diff --git a/Documentation/error-no-new-changes.txt b/Documentation/error-no-new-changes.txt
index 347c080..8e409ef 100644
--- a/Documentation/error-no-new-changes.txt
+++ b/Documentation/error-no-new-changes.txt
@@ -3,7 +3,7 @@
 
 With this error message Gerrit rejects to push a commit if the pushed
 commit was already successfully pushed to Gerrit. In this case there
-is no new change and consequently there is nothing to do for Gerrit.
+is no new change and consequently there is nothing for Gerrit to do.
 
 If your push is failing with this error message, you normally
 don't have to do anything since the commit was already successfully
diff --git a/Documentation/error-non-fast-forward.txt b/Documentation/error-non-fast-forward.txt
index 7dba51b..6604e10 100644
--- a/Documentation/error-non-fast-forward.txt
+++ b/Documentation/error-non-fast-forward.txt
@@ -1,15 +1,15 @@
 non-fast forward
 ================
 
-With this error message Git rejects a push if the remote branch can't
+With this error message Gerrit rejects a push if the remote branch can't
 be fast forwarded onto the pushed commit. This is the case if the
 pushed commit is not based on the current tip of the remote branch.
 
 If a non-fast forward update would be done, all commits from the
 remote branch that succeed the base commit of the pushed commit would
 be removed. This would be especially confusing for other users that
-have based their work on such a commit. Because of this Git is by
-default not allowing non-fast forward updates.
+have based their work on such a commit. Because of this Git by
+default does not allow non-fast forward updates.
 
 When working with Gerrit, this error can only occur if
 link:user-upload.html#bypass_review[code review is bypassed].
@@ -46,7 +46,7 @@
 the commit to the correct project.
 
 
-Although it is considered as bad practice, it is possible to allow
+Although it is considered bad practice, it is possible to allow
 non-fast forward updates with Git. For this the remote Git repository
 has to be configured to not deny non-fast forward updates (set the
 link:http://www.kernel.org/pub/software/scm/git/docs/git-config.html[Git configuration] parameter 'receive.denyNonFastForwards' to
diff --git a/Documentation/error-not-a-gerrit-administrator.txt b/Documentation/error-not-a-gerrit-administrator.txt
index 0468d83..b771af6 100644
--- a/Documentation/error-not-a-gerrit-administrator.txt
+++ b/Documentation/error-not-a-gerrit-administrator.txt
@@ -1,7 +1,7 @@
 Not a Gerrit administrator
 ==========================
 
-With this error message Gerrit rejects to execute a SSH command that
+With this error message Gerrit rejects to execute an SSH command that
 requires administrator privileges if the user is not a Gerrit
 administrator.
 
diff --git a/Documentation/error-not-a-gerrit-project.txt b/Documentation/error-not-a-gerrit-project.txt
index 368a102..dac98ae 100644
--- a/Documentation/error-not-a-gerrit-project.txt
+++ b/Documentation/error-not-a-gerrit-project.txt
@@ -18,7 +18,7 @@
   project is listed. If the project is not listed the project either
   does not exist or you don't have
   link:access-control.html#category_read['Read'] access for it. This
-  means if you certain that the project name is right you should
+  means if you are certain that the project name is right you should
   contact the Gerrit Administrator or project owner to request access
   to the project.
 
diff --git a/Documentation/error-not-allowed-to-upload-merges.txt b/Documentation/error-not-allowed-to-upload-merges.txt
index 981ba91c..515eef5 100644
--- a/Documentation/error-not-allowed-to-upload-merges.txt
+++ b/Documentation/error-not-allowed-to-upload-merges.txt
@@ -2,11 +2,11 @@
 ====================================
 
 With this error message Gerrit rejects to push a merge commit if the
-pushing user has no permissions to upload merge commits for the
+pushing user has no permission to upload merge commits for the
 project to which the push is done.
 
 If you need to upload merge commits, you can contact one of the
-project owners and request permissions to upload merge commits
+project owners and request permission to upload merge commits
 (access right link:access-control.html#category_push_merge['Push Merge Commit'])
 for this project.
 
diff --git a/Documentation/error-permission-denied.txt b/Documentation/error-permission-denied.txt
index 1cb5708..2ec0a3f 100644
--- a/Documentation/error-permission-denied.txt
+++ b/Documentation/error-permission-denied.txt
@@ -1,7 +1,7 @@
 Permission denied (publickey)
 =============================
 
-With this error message a SSH command to Gerrit is rejected if the
+With this error message an SSH command to Gerrit is rejected if the
 SSH authentication is not successful.
 
 The link:http://en.wikipedia.org/wiki/Secure_Shell[SSH] protocol uses link:http://en.wikipedia.org/wiki/Public-key_cryptography[Public-key Cryptography] for authentication.
diff --git a/Documentation/error-prohibited-by-gerrit.txt b/Documentation/error-prohibited-by-gerrit.txt
index 69f80c1..df46566 100644
--- a/Documentation/error-prohibited-by-gerrit.txt
+++ b/Documentation/error-prohibited-by-gerrit.txt
@@ -20,7 +20,7 @@
 4. if you push a lightweight tag without the access right link:access-control.html#category_create['Create
    Reference'] for the reference name 'refs/tags/*'
 
-For new users it happens often that they accidentally try to bypass
+For new users it often happens that they accidentally try to bypass
 code review. The push then fails with the error message 'prohibited
 by Gerrit' because the project didn't allow to bypass code review.
 Bypassing the code review is done by pushing directly to refs/heads/*
diff --git a/Documentation/error-push-fails-due-to-commit-message.txt b/Documentation/error-push-fails-due-to-commit-message.txt
index 01e0a8e..172d64f 100644
--- a/Documentation/error-push-fails-due-to-commit-message.txt
+++ b/Documentation/error-push-fails-due-to-commit-message.txt
@@ -3,7 +3,7 @@
 
 If Gerrit rejects pushing a commit it is often the case that there is
 an issue with the commit message of the pushed commit. In this case
-often the problem can be resolved by fixing the commit message.
+the problem can often be resolved by fixing the commit message.
 
 If the commit message of the last commit needs to be fixed you can
 simply amend the last commit (please find a detailed description in
diff --git a/Documentation/error-squash-commits-first.txt b/Documentation/error-squash-commits-first.txt
index 138ad98..2181c52 100644
--- a/Documentation/error-squash-commits-first.txt
+++ b/Documentation/error-squash-commits-first.txt
@@ -9,7 +9,7 @@
 prevents such dependencies between patch sets within the same change
 to keep the review process simple. Otherwise reviewers would not only
 have to review the latest patch set but also all the patch sets the
-latest one is depending on.
+latest one depends on.
 
 This error is quite common, it appears when a user tries to address
 review comments and creates a new commit instead of amending the
@@ -93,8 +93,8 @@
 ----
 
 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
+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
 removed (not recommended since then amending this commit to create
diff --git a/Documentation/error-you-are-not-author.txt b/Documentation/error-you-are-not-author.txt
index a245252..13de5d8 100644
--- a/Documentation/error-you-are-not-author.txt
+++ b/Documentation/error-you-are-not-author.txt
@@ -1,7 +1,7 @@
 you are not author ...
 ======================
 
-Gerrit verifies for every pushed commit that the e-mail address of
+For every pushed commit Gerrit verifies that the e-mail address of
 the author matches one of the registered e-mail addresses of the
 pushing user. If this is not the case pushing the commit fails with
 the error message "you are not author ...". This policy can be
@@ -27,7 +27,7 @@
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Check in Gerrit under 'Settings -> Identities' which e-mail addresses
-you've configured for your Gerrit account, if no e-mail address is
+you've configured for your Gerrit account.  If no e-mail address is
 registered go to 'Settings -> Contact Information' and register a new
 e-mail address there. Make sure you confirm your e-mail address by
 clicking on the link in the e-mail verification mail sent by Gerrit.
@@ -92,7 +92,7 @@
 git rebase for the affected commits. While doing the interactive
 rebase you have to choose 'edit' for those commits for which the
 author should be rewritten. When the rebase stops at such a commit
-you have to amend the commit with explicitly setting the author
+you have to amend the commit, explicitly setting the author
 before continuing the rebase.
 
 Here is an example that shows how the interactive rebase is used to
diff --git a/Documentation/error-you-are-not-committer.txt b/Documentation/error-you-are-not-committer.txt
index b5b8c44..57229ba 100644
--- a/Documentation/error-you-are-not-committer.txt
+++ b/Documentation/error-you-are-not-committer.txt
@@ -1,7 +1,7 @@
 you are not committer ...
 =========================
 
-Gerrit verifies for every pushed commit that the e-mail address of
+For every pushed commit Gerrit verifies that the e-mail address of
 the committer matches one of the registered e-mail addresses of the
 pushing user. If this is not the case pushing the commit fails with
 the error message "you are not committer ...". This policy can be
@@ -29,7 +29,7 @@
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Check in Gerrit under 'Settings -> Identities' which e-mail addresses
-you've configured for your Gerrit account, if no e-mail address is
+you've configured for your Gerrit account.  If no e-mail address is
 registered go to 'Settings -> Contact Information' and register a new
 e-mail address there. Make sure you confirm your e-mail address by
 clicking on the link in the e-mail verification mail sent by Gerrit.
diff --git a/Documentation/i18n-readme.txt b/Documentation/i18n-readme.txt
index 080ecb6..a84c3dc 100644
--- a/Documentation/i18n-readme.txt
+++ b/Documentation/i18n-readme.txt
@@ -1,7 +1,7 @@
 Gerrit Code Review - i18n
 =========================
 
-Aside from actually writing translations, there's some issues with
+Aside from actually writing translations, there are some issues with
 the way the code produces output.  Most of the UI should support
 right-to-left (RTL) languages.
 
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 7647aaf..2b53772 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -19,6 +19,7 @@
 * link:access-control.html[Access Controls]
 * link:error-messages.html[Error Messages]
 * link:rest-api.html[REST API]
+* link:user-notify.html[Subscribing to Email Notifications]
 * link:user-submodules.html[Subscribing to Git Submodules]
 
 Installation
@@ -34,7 +35,6 @@
 
 * link:config-gerrit.html[System Settings]
 * link:config-contact.html[User Contact Information]
-* link:config-replication.html[Git Replication/Mirroring]
 * link:config-gitweb.html[Gitweb Integration]
 * link:config-headerfooter.html[Site Header/Footer]
 * link:config-sso.html[Single Sign-On Systems]
diff --git a/Documentation/install-j2ee.txt b/Documentation/install-j2ee.txt
index 507d6c5..96814a0 100644
--- a/Documentation/install-j2ee.txt
+++ b/Documentation/install-j2ee.txt
@@ -44,7 +44,7 @@
 +
 If you enabled Bouncy Castle Crypto during 'init', copy the JAR
 from `'$site_path'/lib` into your servlet container's extensions
-directory so its available to Gerrit Code Review.
+directory so it's available to Gerrit Code Review.
 
 
 Jetty 7.x
diff --git a/Documentation/install-quick.txt b/Documentation/install-quick.txt
index 6bea7f8..ce35484 100644
--- a/Documentation/install-quick.txt
+++ b/Documentation/install-quick.txt
@@ -12,7 +12,7 @@
 flavors or BSD.
 
 It's also presumed that you have access to an OpenID enabled email address.
-Examples of OpenID enable email providers are gmail, yahoo and hotmail.
+Examples of OpenID enable email providers are Gmail, Yahoo! Mail and Hotmail.
 It's also possible to register a custom email address with OpenID, but that is
 outside the scope of this quick installation guide. For testing purposes one of
 the above providers should be fine. Please note that network access to the
@@ -42,7 +42,7 @@
 Create a user to host the Gerrit service
 ----------------------------------------
 
-We will run the service as a non privileged user on your system.
+We will run the service as a non-privileged user on your system.
 First create the user and then become the user:
 
 ----
@@ -50,7 +50,7 @@
   $ sudo su gerrit2
 ----
 
-If you don't have root privileges you could skip this step and run gerrit
+If you don't have root privileges you could skip this step and run Gerrit
 as your own user as well.
 
 
@@ -58,7 +58,7 @@
 Download Gerrit
 ---------------
 
-It's time to download the archive that contains the gerrit web and ssh service.
+It's time to download the archive that contains the Gerrit web and ssh service.
 
 You can choose from different versions to download from here:
 
@@ -87,8 +87,8 @@
 When the init is complete, you can review your settings in the
 file `'$site_path/etc/gerrit.config'`.
 
-An important setting will be the canonicalWebUrl which will
-be needed later to access gerrit's web interface.
+An important setting is the canonicalWebUrl which will
+be needed later to access Gerrit's web interface.
 
 ----
   gerrit2@host:~$ cat ~/gerrit_testsite/etc/gerrit.config | grep canonical
@@ -216,7 +216,7 @@
 Your base Gerrit server is now running and you have a user that's ready
 to interact with it.  You now have two options, either you create a new
 test project to work with or you already have a git with history that
-you would like to import into gerrit and try out code review on.
+you would like to import into Gerrit and try out code review on.
 
 New project from scratch
 ~~~~~~~~~~~~~~~~~~~~~~~~
@@ -231,14 +231,14 @@
   user@host:~$
 ----
 
-This will create a repository that you could clone to work with.
+This will create a repository that you can clone to work with.
 
 Already existing project
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
 The other alternative is if you already have a git project that you
 want to try out Gerrit on.
-First you have to create the project, this is done via the SSH port:
+First you have to create the project.  This is done via the SSH port:
 
 ----
   user@host:~$ ssh -p 29418 user@localhost gerrit create-project --name demo-project
@@ -262,7 +262,7 @@
   user@host:~/my-project$
 ----
 
-This will create a repository that you could clone to work with.
+This will create a repository that you can clone to work with.
 
 
 My first change
@@ -294,7 +294,7 @@
 
 Usually when you push to a remote git, you push to the reference
 `'/refs/heads/branch'`, but when working with Gerrit you have to push to a
-virtual branch representing "code review before submittal to branch".
+virtual branch representing "code review before submission to branch".
 This virtual name space is known as /refs/for/<branch>
 
 ----
@@ -319,11 +319,11 @@
 ---------------------------
 
 This covers the scope of getting Gerrit started and your first change uploaded.
-It doesn't give any clue as to how the review workflow works, please find
+It doesn't give any clue as to how the review workflow works, please read
 link:http://source.android.com/submit-patches/workflow[Default Workflow] to
 learn more about the workflow of Gerrit.
 
-To read more on the installation of Gerrit please read link:install.html[the detailed
+To read more on the installation of Gerrit please see link:install.html[the detailed
 installation page].
 
 
diff --git a/Documentation/install.txt b/Documentation/install.txt
index 9cec83d..9926b8f 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -199,11 +199,10 @@
 ------------------
 
 Gerrit Code Review supports some site-specific customization options.
-For more information, see the related topic in this manual:
+For more information, see the related topics in this manual:
 
 * link:config-reverseproxy.html[Reverse Proxy]
 * link:config-sso.html[Single Sign-On Systems]
-* link:config-replication.html[Git Replication/Mirroring]
 * link:config-headerfooter.html[Site Header/Footer]
 * link:config-gitweb.html[Gitweb Integration]
 * link:config-gerrit.html[Other System Settings]
diff --git a/Documentation/intro-quick.txt b/Documentation/intro-quick.txt
index 3d5cbcb..25f5d5e 100644
--- a/Documentation/intro-quick.txt
+++ b/Documentation/intro-quick.txt
@@ -27,7 +27,7 @@
 simple for all committers on a project to ensure that changes are
 checked over before they're actually applied. Because of this Gerrit
 is equally useful where all users are trusted committers such as may
-the case with closed-source commercial development. Either way it's
+be the case with closed-source commercial development. Either way it's
 still desirable to have code reviewed to improve the quality and
 maintainability of the code. After all, if only one person has seen
 the code it may be a little difficult to maintain when that person
@@ -337,7 +337,7 @@
 Easy as that, we now have the change in our working copy to play with.
 You might be interested in what the numbers of the refspec mean.
 
-* The first *68* is the id if the change +mod 100+.  The only reason
+* The first *68* is the id of the change +mod 100+.  The only reason
 for this initial number is to reduce the number of files in any given
 directory within the git repository.
 * The second *68* is the full id of the change. You'll notice this in
@@ -379,7 +379,7 @@
 that can be done by different users, Submission is a third operation
 that can be limited down to another group of users.
 
-Activating the _Publish and Submit_ or _Submit Patch Set X_ button
+Clicking the _Publish and Submit_ or _Submit Patch Set X_ button
 will merge the change into the main part of the repository so that it
 becomes an accepted part of the project. After this anyone fetching
 the git repository will receive this change as a part of the master
diff --git a/Documentation/json.txt b/Documentation/json.txt
index b1dbc32..fac1424 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -11,22 +11,22 @@
 ------
 The Gerrit change being reviewed, or that was already reviewed.
 
-project:: Project path in Gerrit
+project:: Project path in Gerrit.
 
-branch:: Branch name within project
+branch:: Branch name within project.
 
-topic:: Topic name specified by the uploader for this change series
+topic:: Topic name specified by the uploader for this change series.
 
 id:: Change identifier, as scraped out of the Change-Id field in
 the commit message, or as assigned by the server if it was missing.
 
-number:: Change number (deprecated)
+number:: Change number (deprecated).
 
-subject:: Description of change
+subject:: Description of change.
 
-owner:: Owner in <<account,account attribute>>
+owner:: Owner in <<account,account attribute>>.
 
-url:: Canonical URL to reach this change
+url:: Canonical URL to reach this change.
 
 commitMessage:: The full commit message for the change.
 
@@ -53,9 +53,9 @@
 message based on the server's
 link:config-gerrit.html#trackingid[trackingid] sections.
 
-currentPatchSet:: Current <<patchset,patchset attribute>>.
+currentPatchSet:: Current <<patchSet,patchSet attribute>>.
 
-patchSets:: All <<patchset,patchset attribute>> for this change.
+patchSets:: All <<patchSet,patchSet attribute>> for this change.
 
 [[trackingid]]
 trackingid
@@ -76,8 +76,8 @@
 
 email:: User's preferred email address.
 
-[[patchset]]
-patchset
+[[patchSet]]
+patchSet
 --------
 Refers to a specific patchset within a <<change,change>>.
 
@@ -109,8 +109,8 @@
 
 by:: Reviewer of the patch set in <<account,account attribute>>.
 
-[[refupdate]]
-refupdate
+[[refUpdate]]
+refUpdate
 --------
 Information about a ref that was updated.
 
@@ -118,7 +118,7 @@
 
 newRev:: The new value the ref was updated to.
 
-project:: Project path in Gerrit
+project:: Project path in Gerrit.
 
 refName:: Ref name within project.
 
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 69018d8..4186026 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -18,6 +18,7 @@
 |Google Gson                | <<apache2,Apache License 2.0>>
 |Google Web Toolkit         | <<apache2,Apache License 2.0>>
 |Guice                      | <<apache2,Apache License 2.0>>
+|Guava Libraries            | <<apache2,Apache License 2.0>>
 |Apache Commons Codec       | <<apache2,Apache License 2.0>>
 |Apache Commons DBCP        | <<apache2,Apache License 2.0>>
 |Apache Commons Http Client | <<apache2,Apache License 2.0>>
@@ -33,7 +34,6 @@
 |Apache Xerces              | <<apache2,Apache License 2.0>>
 |OpenID4Java                | <<apache2,Apache License 2.0>>
 |Neko HTML                  | <<apache2,Apache License 2.0>>
-|Ehcache                    | <<apache2,Apache License 2.0>>
 |mime-util                  | <<apache2,Apache License 2.0>>
 |Jetty                      | <<apache2,Apache License 2.0>>, or link:http://www.eclipse.org/legal/epl-v10.html[EPL]
 |Prolog Cafe                | <<prolog_cafe,EPL or GPL>>
@@ -52,6 +52,7 @@
 |JSR 305                    | <<jsr305,New-Style BSD>>
 |dk.brics.automaton         | <<automaton,New-Style BSD>>
 |Java Concurrency in Practice Annotations | <<jcip,Create Commons Attribution License>>
+|pegdown                    | <<apache2,Apache License 2.0>>
 |======================================================================
 
 Cryptography Notice
diff --git a/Documentation/pgm-ExportReviewNotes.txt b/Documentation/pgm-ExportReviewNotes.txt
index 17cc862..b7670db 100644
--- a/Documentation/pgm-ExportReviewNotes.txt
+++ b/Documentation/pgm-ExportReviewNotes.txt
@@ -14,7 +14,7 @@
 -----------
 Scans every submitted change and creates an initial notes
 branch detailing the previous submission information for
-each merged changed.
+each merged change.
 
 This task can take quite some time, but can run in the background
 concurrently to the server if the database is MySQL or PostgreSQL.
diff --git a/Documentation/pgm-init.txt b/Documentation/pgm-init.txt
index ae5e471..57decdd 100644
--- a/Documentation/pgm-init.txt
+++ b/Documentation/pgm-init.txt
@@ -19,7 +19,7 @@
 for some basic setup prior to writing default configuration files
 into a newly created `$site_path`.
 
-If run an an existing `$site_path`, init will upgrade some resources
+If run in an existing `$site_path`, init will upgrade some resources
 as necessary.
 
 OPTIONS
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
index f07b9a9..1a359ac 100644
--- a/Documentation/rest-api.txt
+++ b/Documentation/rest-api.txt
@@ -32,7 +32,7 @@
 ----
 
 JSON responses are encoded using UTF-8 and use content type
-`application/json`. The JSON response body starts with magic prefix
+`application/json`. The JSON response body starts with a magic prefix
 line that must be stripped before feeding the rest of the response
 body to a JSON parser:
 
diff --git a/Documentation/user-changeid.txt b/Documentation/user-changeid.txt
index 124ec31..409bb32 100644
--- a/Documentation/user-changeid.txt
+++ b/Documentation/user-changeid.txt
@@ -4,7 +4,7 @@
 Description
 -----------
 
-Gerrit Code Review sometimes relies upon Change-Id lines in the
+Gerrit Code Review sometimes relies upon a Change-Id line at the
 bottom of a commit message to uniquely identify a change across all
 drafts of it.  By including a unique Change-Id in the commit message,
 Gerrit can automatically associate a new version of a change back
@@ -37,7 +37,7 @@
 the commit name, `29a6...`, as the change may have been amended or
 rebased to address reviewer comments since its initial inception.
 
-To avoid confusion with commit names, Change-Ids typically are with
+To avoid confusion with commit names, Change-Ids are typically prefixed with
 an uppercase `I`.
 
 Creation
@@ -125,7 +125,7 @@
 already uploaded to Gerrit Code Review, and thus has a corresponding
 change that reviewers have already examined and left comments on.
 If you aren't sure which lines Gerrit knows about, try copying and
-pasting the lines into the search box in the top-right.
+pasting the lines into the search box at the top-right of the web interface.
 
 If Gerrit already knows about more than one Change-Id, pick one
 to keep in the squashed commit message, and manually abandon the
diff --git a/Documentation/user-notify.txt b/Documentation/user-notify.txt
new file mode 100644
index 0000000..ae3c2d09
--- /dev/null
+++ b/Documentation/user-notify.txt
@@ -0,0 +1,127 @@
+Gerrit Code Review - Email Notifications
+========================================
+
+Description
+-----------
+
+Gerrit can automatically notify users by email when new changes are
+uploaded for review, after comments have been posted on a change,
+or after the change has been submitted to a branch.
+
+User Level Settings
+-------------------
+
+Individual users can configure email subscriptions by editing
+watched projects through Settings > Watched Projects with the web UI.
+
+Specific projects may be watched, or the special project
+`All-Projects` can be watched to watch all projects that
+are visible to the user.
+
+Change search expressions can be used to filter change notifications
+to specific subsets, for example `branch:master` to only see changes
+proposed for the master branch.
+
+Project Level Settings
+----------------------
+
+Project owners and site administrators can configure project level
+notifications, enabling Gerrit Code Review to automatically send
+emails to team mailing lists, or groups of users. Project settings
+are stored inside of the `refs/meta/config` branch of each Git
+repository, and are placed inside of the `project.config` file.
+
+To edit the project level notify settings, ensure the project owner
+has Push permission already granted for the `refs/meta/config`
+branch. Consult link:access-control.html[access controls] for
+details on how access permissions work.
+
+Initialize a temporary Git repository to edit the configuration:
+====
+  mkdir cfg_dir
+  cd cfg_dir
+  git init
+====
+
+Download the existing configuration from Gerrit:
+====
+  git fetch ssh://localhost:29418/project refs/meta/config
+  git checkout FETCH_HEAD
+====
+
+Enable notifications to an email address by adding to
+`project.config`, this can be done using the `git config` command:
+====
+  git config -f project.config --add notify.team.email team-address@example.com
+  git config -f project.config --add notify.team.email paranoid-manager@example.com
+====
+
+Examining the project.config file with any text editor should show
+a new notify section describing the email addresses to deliver to:
+----
+  [notify "team"]
+  	email = team-address@example.com
+  	email = paranoid-manager@example.com
+----
+
+Each notify section within a single project.config file must have a
+unique name. The section name itself does not matter and may later
+appear in the web UI. Naming a section after the email address or
+group it delivers to is typical. Multiple sections can be specified
+if different filters are needed.
+
+Commit the configuration change, and push it back:
+====
+  git commit -a -m "Notify team-address@example.com of changes"
+  git push ssh://localhost:29418/project HEAD:refs/meta/config
+====
+
+[[notify.name.email]]notify.<name>.email::
++
+List of email addresses to send matching notifications to. Each
+email address should be placed on its own line.
++
+Internal groups within Gerrit Code Review can also be named using
+`group NAME` syntax. If this format is used the group's UUID must
+also appear in the corresponding `groups` file. Gerrit will expand
+the group membership and BCC all current users.
+
+[[notify.name.type]]notify.<name>.type::
++
+Types of notifications to send. If not specified, all notifications
+are sent.
++
+* `new_changes`: Only newly created changes.
+* `all_comments`: Only comments on existing changes.
+* `submitted_changes`: Only changes that have been submitted.
+* `all`: All notifications.
+
++
+Like email, this variable may be a list of options.
+
+[[notify.name.filter]]notify.<name>.filter::
++
+link:user-search.html[Change search expression] to match changes that
+should be sent to the emails named in this section. Within a Git-style
+configuration file double quotes around complex operator values may
+need to be escaped, e.g. `filter = branch:\"^(maint|stable)-.*\"`.
+
+When sending email to a bare email address in a notify block, Gerrit
+Code Review ignores read access controls and assumes the administrator
+has set the filtering options correctly. Project owners can implement
+security filtering by adding the `visibleto:groupname` predicate to
+the filter expression, for example:
+
+====
+  [notify "Developers"]
+  	email = team-address@example.com
+  	filter = visibleto:Developers
+====
+
+When sending email to an internal group, the internal group's read
+access is automatically checked by Gerrit and therefore does not
+need to use the `visibleto:` operator in the filter.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 4fd6b2f..8a231fe 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -149,7 +149,7 @@
 [[tr,bug]]
 tr:'ID', bug:'ID'::
 +
-Search for changes whose commit message contains 'ID' and matched
+Search for changes whose commit message contains 'ID' and matches
 one or more of the
 link:config-gerrit.html#trackingid[trackingid sections]
 in the server's configuration file.  This is typically used to
@@ -166,7 +166,7 @@
 [[message]]
 message:'MESSAGE'::
 +
-Changes that matches 'MESSAGE' arbitrary string in body commit messages.
+Changes that match 'MESSAGE' arbitrary string in the commit message body.
 
 [[file]]
 file:^'REGEX'::
@@ -229,7 +229,7 @@
 
 is:open::
 +
-True if the change is other open or submitted, merge pending.
+True if the change is either open or submitted, merge pending.
 
 is:draft::
 +
@@ -246,7 +246,7 @@
 [[status]]
 status:open::
 +
-True if the change state is other 'review in progress' or 'submitted,
+True if the change state is either 'review in progress' or 'submitted,
 merge pending'.
 
 status:reviewed::
@@ -268,7 +268,7 @@
 
 status:abandoned::
 +
-Change has been abandoned by the change owner, or administrator.
+Change has been abandoned.
 
 
 Boolean Operators
@@ -304,7 +304,7 @@
 [[labels]]
 Labels
 ------
-Label operators can be used to match approval score given during
+Label operators can be used to match approval scores given during
 a code review.  The specific set of supported labels depends on
 the server configuration, however `CodeReview` and `Verified`
 are the default labels provided out of the box.
@@ -323,7 +323,7 @@
   of change list pages.  Example: `label:R` or `label:V`.
 
 A label name must be followed by a score, or an operator and a score.
-The easiest way to explain these are by example.
+The easiest way to explain this is by example.
 
 `label:CodeReview=2`::
 `label:CodeReview=+2`::
@@ -400,7 +400,7 @@
 
 draftby:'USER'::
 +
-Matches changes that 'USER' has left unpublished drafts on.
+Matches changes that 'USER' has left unpublished draft comments on.
 Since the drafts are unpublished, it is not possible to see the
 draft text, or even how many drafts there are. The special case
 of `draftby:self` will find changes where the caller has created
diff --git a/Documentation/user-submodules.txt b/Documentation/user-submodules.txt
index 3d14437..cfaf3e9 100644
--- a/Documentation/user-submodules.txt
+++ b/Documentation/user-submodules.txt
@@ -18,7 +18,7 @@
 any gitlinks and .gitmodules file with required info) and if so,
 a new submodule subscription is registered.
 
-When a new commit of a registered submodule is merged, gerrit
+When a new commit of a registered submodule is merged, Gerrit
 automatically updates the subscribers to the submodule with a new
 commit having the updated gitlinks.
 
@@ -31,7 +31,7 @@
 in the official git submodule command documentation.
 
 Imagine a repository called 'super' and another one called 'a'.
-Also consider 'a' available in a running gerrit instance on "server".
+Also consider 'a' available in a running Gerrit instance on "server".
 With this feature, one could attach 'a' inside of 'super' repository
 at path 'a' by executing the following command when being inside
 'super':
@@ -86,12 +86,12 @@
 gitlinks/.gitmodules file.
 
 The branch field of a submodule section is a custom git submodule
-feature for gerrit use. One should always be sure to fill it in
+feature for Gerrit use. One should always be sure to fill it in
 editing .gitmodules file after adding submodules to a super project,
-if it is the intention to make use of the gerrit feature introduced here.
+if it is the intention to make use of the Gerrit feature introduced here.
 
 Any git submodules which are added and not have the branch field
-available in the .gitmodules file will not be subscribed by gerrit
+available in the .gitmodules file will not be subscribed by Gerrit
 to automatically update the superproject.
 
 Detecting and Subscribing Submodules
@@ -114,7 +114,7 @@
 
 Imagine a superproject called 'super' having a branch called 'dev'
 having subscribed to a submodule 'a' on a branch 'dev-of-a'. When a commit
-is merged in branch 'dev-of-a' of 'a' project, gerrit automatically
+is merged in branch 'dev-of-a' of 'a' project, Gerrit automatically
 creates a new commit on branch 'dev' of 'super' updating the gitlink
 to point to the just merged commit.
 
@@ -123,11 +123,11 @@
 
 Gerrit will automatically update only the superprojects that added
 the submodules of urls of the running server (the one described in
-the canonical web url value in gerrit configuration file).
+the canonical web url value in Gerrit configuration file).
 
 The Gerrit instance administrator group should always certify to
 provide the canonical web url value in its configuration file. Users
-should certify to use the url value of the running gerrit instance to
+should certify to use the url value of the running Gerrit instance to
 add/subscribe submodules.
 
 Removing Subscriptions
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 8e05e72..67799e4 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -46,7 +46,7 @@
 
 [TIP]
 Users who frequently upload changes will also want to consider
-starting a `ssh-agent`, and adding their private key to the list
+starting an `ssh-agent`, and adding their private key to the list
 managed by the agent, to reduce the frequency of entering the
 key's passphrase.  Consult `man ssh-agent`, or your SSH client's
 documentation, for more details on configuration of the agent
@@ -57,7 +57,7 @@
 ~~~~~~~~~~~~~~~~~~~
 
 To verify your SSH key is working correctly, try using an SSH client
-to connect to Gerrit's SSHD port.  By default Gerrit is running on
+to connect to Gerrit's SSHD port.  By default Gerrit runs on
 port 29418, using the same hostname as the web server:
 
 ====
@@ -104,7 +104,7 @@
 Create Changes
 ~~~~~~~~~~~~~~
 
-To create new changes for review, simply push into the project's
+To create new changes for review, simply push to the project's
 magical `refs/for/'branch'` ref using any Git client tool:
 
 ====
diff --git a/ReleaseNotes/ReleaseNotes-2.4.txt b/ReleaseNotes/ReleaseNotes-2.4.txt
new file mode 100644
index 0000000..82f3ed4
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.4.txt
@@ -0,0 +1,257 @@
+Release notes for Gerrit 2.4
+============================
+
+Gerrit 2.4 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.4.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.4.war]
+
+Schema Change
+-------------
+*WARNING:* This release contains schema changes.  To upgrade:
+----
+  java -jar gerrit.war init -d site_path
+----
+
+*WARNING:* Upgrading to 2.4.x requires the server be first upgraded to 2.1.7 (or
+a later 2.1.x version), and then to 2.4.x.  If you are upgrading from 2.2.x.x or
+newer, you may ignore this warning and upgrade directly to 2.4.x.
+
+New Features
+------------
+
+Security
+~~~~~~~~
+
+* Restrict visibility to arbitrary user dashboards
++
+Administrators have some expectation when using the 'suggest.accounts'
+visibility restriction feature that users cannot get the names or
+email addresses for arbitrary accounts. In fact, because account IDs
+are sequential, it would be easy for an adversary to get personal
+information of all users on the server by requesting every user's
+dashboard.
++
+This includes changing the meaning of the 'suggest.accounts' config
+option to be a boolean indicating whether account suggestion should
+happen at all, which is now orthogonal to the account visibility
+restriction policy. We still recognize the old values for
+'suggest.accounts', with the slight behavior change that
+'suggest.accounts=OFF' now means that users cannot access the dashboards
+of any other users. Administrators who do not want this behavior can
+update their configuration.
+
+* Indicate that 'not found' may actually be a permission issue
+
+Web
+~~~
+
+* Add user preference to mark files reviewed automatically or manually
++
+Add a checkbox to the preferences header on the diff
+screen which allows a user to specify whether they
+want manual-reviewing enabled or disabled.  Previously,
+every file was auto marked reviewed when a user first
+displayed it.  The new manual mode prevents this auto
+marking and only marks a file reviewed when the user
+explicitly clicks on the reviewed checkbox.
+
+* Use 'Auto Merge' for merge commit's base comparison
++
+When reviewing a merge commit, the old wording in the version history dropdown
+of 'Base' doesn't really match Gerrit's behavior.  Updating this to use
+'Auto Merge' as suggested by Shawn Pearce on IRC.
+
+* issue 1035 Add rebase button to the change screen
++
+This change adds a rebase button along with the rest of
+the action buttons in the change page. When pressing the
+button, the most recent patch set will be rebased onto
+the tip of the destination branch or the latest patchset
+of the change we depend upon. A new patch set containing
+the rebased commit will be produced and added to the
+change.
++
+Rebasing of a change in web UI is restricted to change owner, submitter or
+those with the (new) 'rebase' permission.
+
+* Add a new permission 'rebase' to permit rebasing changes in the web UI
+
+* Make a user's dashboard visible if any of the changes are visible to the
+current user.
+
+* Change 'Loading ...' to say 'Working ...' as, often, there is more going on
+than just loading a response.
+
+Performance
+~~~~~~~~~~~
+
+* Asynchronously send email so it does not block the UI
+* Optimize queries for open/merged changes by project + branch
+
+Git
+~~~
+
+* Implement a multi-sub-task progress monitor for ReceiveCommits
+
+* Close corresponding change when pushing to 'refs/heads/*'
++
+Gerrit would not close the open changes with matching change-ids,
+when the user pushes commits directly to 'refs/heads/*'.
++
+This issue could be triggered for two reasons:
+
+. It is triggered when Gerrit detects no changes between the
+pushed commits and the current patchset on the open changes. This
+patch make sure that the matching open change is always closed when
+pushing to 'refs/heads/*', even if no visible changes is detected.
+
+. The same commit exists on another branch than the destination
+branch. This could trick gerrit into just "re-closing" the wrong
+change.
+
+* Run ReceiveCommits in a shared thread pool
++
+Since the work to ReceiveCommits may take a long, potentially unbounded
+amount of time, we would like to have it run in the background so it
+can be monitored for timeouts and cancelled, and have stalls reported
+to the user from the main thread.
+
+Search
+~~~~~~
+
+* Add the '--dependencies' option to the 'query' command.
++
+This option includes information about patch sets which depend on, or are
+needed by, each patch set.
+
+* Branch Operator: Support full branch names
++
+The search operator for branches required the provided value to be the
+short branch name that is shown in the web interface (without the
+'refs/heads/' prefix). Change the branch operator so that it also
+supports full branch names as value.
++
+It is intuive that searching with 'branch:master' and searching with
+'branch:refs/for/master' deliver the same result. So far
+'branch:refs/for/master' was the same as searching with
+'refs:refs/heads/refs/heads/master' which is unexpected for most users.
+
+* Add comment inclusion via '&comments=true' over HTTP
++
+With this change, we can fetch the comments on a patchset by sending a
+request to 'https://site/query?comments=true'
+
+Access Rights
+~~~~~~~~~~~~~
+
+* Added the 'emailReviewers' as a global capability.
++
+This replaces the 'emailOnlyAuthors' flag of account groups.
+
+Dev
+~~~
+
+* issue 1272 Add scripts to create release notes from git log
++
+These script generates a list of commits from git log between two given commits
+and outputs the asciidoc format containting list of commits subject and body.
+
+* Update URL for m2eclipse
++
+The project is now under the Eclipse Foundation umbrella.
+
+* Add missing ignore for m2e prefs in gerrit-ehcache
+
+* Add '--issues' and '--issue_numbers' options to the 'gitlog2asciidoc.py'
+
+Miscellaneous
+~~~~~~~~~~~~~
+
+* Remove perl from 'commit-msg' hook
++
+Removing perl from the commit-msg hook reduces the dependencies
+gerrit imposes on its users.
+
+* updating contrib 'trivial_rebase.py' for 2.2.2.1
+
+Upgrades
+--------
+
+* Updated to Guice 3.0.
+* Updated to gwtorm 1.4.
+* Update JGit to 1.3.0.201202151440-r.75-gff13648
+* Update to gwtjsonrpc 1.3
++
+The change also shrinks the built WAR from 38M to 23M
+by excluding the now unnecessary GWT server code.
+
+Bug Fixes
+---------
+
+* issue 904 Users who starred a change should receive all the emails about a change.
+
+* Fix: 'Diff All Side-by-Side' and 'Diff All Unified' buttons
++
+When pressing the 'Diff All Side-by-Side' or
+'Diff All Unified' button on the change screen, the
+opened browser windows/tabs shows diffs using "Base"
+as old version and the latest one as active patch set,
+regardless what has been set using the
+"Old Version History:" drop down menu and what is
+currently active patch set.
++
+Gerrit doesn't remember the base patch set in the URL,
+making it impossible to copy-and-paste the URL to
+co-workers to show them the same diff a user is
+looking at.
++
+This change fixes this behavior to make sure that
+the opened new browser windows shows diffs using the
+correct old patch set and active patch set.
+
+* Fix NPEs looking up groups by UUID in GroupCache
+
+* Fix default 'receive.timeout'
++
+This should be in milliseconds, not seconds. Set the default to be
+2 minutes in milliseconds and update the documentation to reflect
+that milliseconds are the default unit of time used here.
+
+* Fix 'development_become_any_account' redirects
+* issue 1299 Allow configuration of optional pattern for gitweb file history link
+* Use servlet context path during logout
+
+* issue 1353 Fix case check for project name so that symlinks work again
+* Fix merging of access sections
+* Fix inconsistent behaviour when replicating refs/meta/config
+* Fix duplicated results on status:open project:P branch:B
+
+Documentation
+-------------
+
+Access Rights
+~~~~~~~~~~~~~
+* Capabilities introduced
+* Kill and priority capabilities
+* Administrate server capability
+* Create account capability
+* Create group and project capability
+* Flush caches capability
+* Capability replication and view caches
+* Capability view conn. & queue
+* Example roles introduced
+* Developer example role
+* CI system example role
+* Integrator example role
+* Project owner example role
+* Administrator example role
+
+Miscellaneous
+~~~~~~~~~~~~~
+* User upload documentation: Replace changes
+* Add visible-to-all flag in the documentation for cmd-create-group
+* Add a contributing guideline for annotations
+* Add missing header for suggest.accounts documentation
+* Fix anchors for description of gitweb config parameters
+* Add missing section name to config-gerrit documentation
+* Fix documentation of ls-projects
diff --git a/ReleaseNotes/ReleaseNotes-2.5.txt b/ReleaseNotes/ReleaseNotes-2.5.txt
new file mode 100644
index 0000000..60c4f08
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.5.txt
@@ -0,0 +1,55 @@
+Release notes for Gerrit 2.5
+============================
+
+Gerrit 2.5 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.5.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.5.war]
+
+Upgrade Warnings
+----------------
+
+Replication
+~~~~~~~~~~~
+
+Gerrit 2.5 no longer includes replication support out of the box.
+Servers that reply upon `replication.config` to copy Git repository
+data to other locations must also install the replication plugin.
+
+Cache Configuration
+~~~~~~~~~~~~~~~~~~~
+
+Disk caches are now backed by individual H2 databases, rather than
+Ehcache's own private format. Administrators are encouraged to clear
+the `'$site_path'/cache` directory before starting the new server.
+
+The `cache.NAME.diskLimit` configuration variable is now expressed in
+bytes of disk used. This is a change from previous versions of Gerrit,
+which expressed the limit as the number of entries rather than bytes.
+Bytes of disk is a more accurate way to size what is held. Admins that
+set this variable must update their configurations, as the old values
+are too small. For example a setting of `diskLimit = 65535` will only
+store 64 KiB worth of data on disk and can no longer hold 65,000 patch
+sets. It is recommended to delete the diskLimit variable (if set) and
+rely on the built-in default of `128m`.
+
+The `cache.diff.memoryLimit` and `cache.diff_intraline.memoryLimit`
+configuration variables are now expressed in bytes of memory used,
+rather than number of entries in the cache. This is a change from
+previous versions of Gerrit and gives administrators more control over
+how memory is partioned within a server. Admins that set this variable
+must update their configurations, as the old values are too small.
+For example a setting of `memoryLimit = 1024` now means only 1 KiB of
+data (which may not even hold 1 patch set), not 1024 patch sets.  It
+is recommended to set these to `10m` for 10 MiB of memory, and
+increase as necessary.
+
+The `cache.NAME.maxAge` variable now means the maximum amount of time
+that can elapse between reads of the source data into the cache, no
+matter how often it is being accessed. In prior versions it meant how
+long an item could be held without being requested by a client before
+it was discarded. The new meaning of elapsed time before consulting
+the source data is more useful, as it enables a strict bound on how
+stale the cached data can be. This is especially useful for slave
+servers account and permission data, or the `ldap_groups` cache, where
+updates are often made to the source without telling Gerrit to reload
+the cache.
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 13da60a..30a85e8 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,6 +1,11 @@
 Gerrit Code Review - Release Notes
 ==================================
 
+[[2_4]]
+Version 2.4.x
+-------------
+* link:ReleaseNotes-2.4.html[2.4]
+
 [[2_3]]
 Version 2.3.x
 -------------
diff --git a/gerrit-ehcache/.gitignore b/gerrit-cache-h2/.gitignore
similarity index 83%
copy from gerrit-ehcache/.gitignore
copy to gerrit-cache-h2/.gitignore
index fe190c9..cb430b8 100644
--- a/gerrit-ehcache/.gitignore
+++ b/gerrit-cache-h2/.gitignore
@@ -1,6 +1,6 @@
 /target
 /.classpath
 /.project
-/.settings/org.eclipse.m2e.core.prefs
 /.settings/org.maven.ide.eclipse.prefs
-/gerrit-ehcache.iml
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-cache-h2.iml
diff --git a/gerrit-cache-h2/.settings/org.eclipse.core.resources.prefs b/gerrit-cache-h2/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..fc11c3f
--- /dev/null
+++ b/gerrit-cache-h2/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,5 @@
+#Thu Jul 28 11:02:36 PDT 2011
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/test/java=UTF-8
+encoding/<project>=UTF-8
diff --git a/gerrit-ehcache/.settings/org.eclipse.core.runtime.prefs b/gerrit-cache-h2/.settings/org.eclipse.core.runtime.prefs
similarity index 100%
rename from gerrit-ehcache/.settings/org.eclipse.core.runtime.prefs
rename to gerrit-cache-h2/.settings/org.eclipse.core.runtime.prefs
diff --git a/gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs b/gerrit-cache-h2/.settings/org.eclipse.jdt.core.prefs
similarity index 99%
rename from gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs
rename to gerrit-cache-h2/.settings/org.eclipse.jdt.core.prefs
index e89c048..470942d 100644
--- a/gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-cache-h2/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Thu Jan 19 12:55:44 PST 2012
+#Thu Jul 28 11:02:36 PDT 2011
 eclipse.preferences.version=1
 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
 org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
diff --git a/gerrit-ehcache/.settings/org.eclipse.jdt.ui.prefs b/gerrit-cache-h2/.settings/org.eclipse.jdt.ui.prefs
similarity index 100%
rename from gerrit-ehcache/.settings/org.eclipse.jdt.ui.prefs
rename to gerrit-cache-h2/.settings/org.eclipse.jdt.ui.prefs
diff --git a/gerrit-ehcache/pom.xml b/gerrit-cache-h2/pom.xml
similarity index 76%
rename from gerrit-ehcache/pom.xml
rename to gerrit-cache-h2/pom.xml
index f9117b9..4d4303c 100644
--- a/gerrit-ehcache/pom.xml
+++ b/gerrit-cache-h2/pom.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
-Copyright (C) 2010 The Android Open Source Project
+Copyright (C) 2012 The Android Open Source Project
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -25,23 +25,28 @@
     <version>2.5-SNAPSHOT</version>
   </parent>
 
-  <artifactId>gerrit-ehcache</artifactId>
-  <name>Gerrit Code Review - Ehcache Bindings</name>
+  <artifactId>gerrit-cache-h2</artifactId>
+  <name>Gerrit Code Review - Guava + H2 caching</name>
 
   <description>
-    Bindings to Ehcache
+    Implementation of caching backed by Guava and H2
   </description>
 
   <dependencies>
     <dependency>
-      <groupId>net.sf.ehcache</groupId>
-      <artifactId>ehcache-core</artifactId>
-    </dependency>
-
-    <dependency>
       <groupId>com.google.gerrit</groupId>
       <artifactId>gerrit-server</artifactId>
       <version>${project.version}</version>
     </dependency>
+
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>com.h2database</groupId>
+      <artifactId>h2</artifactId>
+    </dependency>
   </dependencies>
 </project>
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java
new file mode 100644
index 0000000..8bb0709
--- /dev/null
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.cache.h2;
+
+import com.google.common.base.Strings;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.cache.Weigher;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.cache.CacheBinding;
+import com.google.gerrit.server.cache.MemoryCacheFactory;
+import com.google.gerrit.server.cache.PersistentCacheFactory;
+import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.concurrent.TimeUnit;
+
+public class DefaultCacheFactory implements MemoryCacheFactory {
+  public static class Module extends LifecycleModule {
+    @Override
+    protected void configure() {
+      bind(DefaultCacheFactory.class);
+      bind(MemoryCacheFactory.class).to(DefaultCacheFactory.class);
+      bind(PersistentCacheFactory.class).to(H2CacheFactory.class);
+      listener().to(H2CacheFactory.class);
+    }
+  }
+
+  private final Config cfg;
+
+  @Inject
+  public DefaultCacheFactory(@GerritServerConfig Config config) {
+    this.cfg = config;
+  }
+
+  @Override
+  public <K, V> Cache<K, V> build(CacheBinding<K, V> def) {
+    return create(def, false).build();
+  }
+
+  @Override
+  public <K, V> LoadingCache<K, V> build(
+      CacheBinding<K, V> def,
+      CacheLoader<K, V> loader) {
+    return create(def, false).build(loader);
+  }
+
+  @SuppressWarnings("unchecked")
+  <K, V> CacheBuilder<K, V> create(
+      CacheBinding<K, V> def,
+      boolean unwrapValueHolder) {
+    CacheBuilder<K,V> builder = newCacheBuilder();
+    builder.recordStats();
+    builder.maximumWeight(cfg.getLong(
+        "cache", def.name(), "memoryLimit",
+        def.maximumWeight()));
+
+    Weigher<K, V> weigher = def.weigher();
+    if (weigher != null && unwrapValueHolder) {
+      final Weigher<K, V> impl = weigher;
+      weigher = (Weigher<K, V>) new Weigher<K, ValueHolder<V>> () {
+        @Override
+        public int weigh(K key, ValueHolder<V> value) {
+          return impl.weigh(key, value.value);
+        }
+      };
+    } else if (weigher == null) {
+      weigher = unitWeight();
+    }
+    builder.weigher(weigher);
+
+    Long age = def.expireAfterWrite(TimeUnit.SECONDS);
+    if (has(def.name(), "maxAge")) {
+      builder.expireAfterWrite(ConfigUtil.getTimeUnit(cfg,
+          "cache", def.name(), "maxAge",
+          age != null ? age : 0,
+          TimeUnit.SECONDS), TimeUnit.SECONDS);
+    } else if (age != null) {
+      builder.expireAfterWrite(age, TimeUnit.SECONDS);
+    }
+
+    return builder;
+  }
+
+  private boolean has(String name, String var) {
+    return !Strings.isNullOrEmpty(cfg.getString("cache", name, var));
+  }
+
+  @SuppressWarnings({"rawtypes", "unchecked"})
+  private static <K, V> CacheBuilder<K, V> newCacheBuilder() {
+    CacheBuilder builder = CacheBuilder.newBuilder();
+    return builder;
+  }
+
+  private static <K, V> Weigher<K, V> unitWeight() {
+    return new Weigher<K, V>() {
+      @Override
+      public int weigh(K key, V value) {
+        return 1;
+      }
+    };
+  }
+}
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
new file mode 100644
index 0000000..27da20f
--- /dev/null
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -0,0 +1,198 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.cache.h2;
+
+import com.google.common.base.Preconditions;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.cache.CacheBinding;
+import com.google.gerrit.server.cache.PersistentCacheFactory;
+import com.google.gerrit.server.cache.h2.H2CacheImpl.SqlStore;
+import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+@Singleton
+class H2CacheFactory implements PersistentCacheFactory, LifecycleListener {
+  static final Logger log = LoggerFactory.getLogger(H2CacheFactory.class);
+
+  private final DefaultCacheFactory defaultFactory;
+  private final Config config;
+   private final File cacheDir;
+  private final List<H2CacheImpl<?, ?>> caches;
+  private final ExecutorService executor;
+  private final ScheduledExecutorService cleanup;
+  private volatile boolean started;
+
+  @Inject
+  H2CacheFactory(
+      DefaultCacheFactory defaultCacheFactory,
+      @GerritServerConfig Config cfg,
+      SitePaths site) {
+    defaultFactory = defaultCacheFactory;
+    config = cfg;
+
+    File loc = site.resolve(cfg.getString("cache", null, "directory"));
+    if (loc == null) {
+      cacheDir = null;
+    } else if (loc.exists() || loc.mkdirs()) {
+      if (loc.canWrite()) {
+        log.info("Enabling disk cache " + loc.getAbsolutePath());
+        cacheDir = loc;
+      } else {
+        log.warn("Can't write to disk cache: " + loc.getAbsolutePath());
+        cacheDir = null;
+      }
+    } else {
+      log.warn("Can't create disk cache: " + loc.getAbsolutePath());
+      cacheDir = null;
+    }
+
+    caches = Lists.newLinkedList();
+
+    if (cacheDir != null) {
+      executor = Executors.newFixedThreadPool(
+          1,
+          new ThreadFactoryBuilder()
+            .setNameFormat("DiskCache-Store-%d")
+            .build());
+      cleanup = Executors.newScheduledThreadPool(
+          1,
+          new ThreadFactoryBuilder()
+            .setNameFormat("DiskCache-Prune-%d")
+            .setDaemon(true)
+            .build());
+    } else {
+      executor = null;
+      cleanup = null;
+    }
+  }
+
+  @Override
+  public void start() {
+    started = true;
+    if (executor != null) {
+      for (final H2CacheImpl<?, ?> cache : caches) {
+        executor.execute(new Runnable() {
+          @Override
+          public void run() {
+            cache.start();
+          }
+        });
+
+        cleanup.schedule(new Runnable() {
+          @Override
+          public void run() {
+            cache.prune(cleanup);
+          }
+        }, 30, TimeUnit.SECONDS);
+      }
+    }
+  }
+
+  @Override
+  public void stop() {
+    if (executor != null) {
+      try {
+        cleanup.shutdownNow();
+
+        List<Runnable> pending = executor.shutdownNow();
+        if (executor.awaitTermination(15, TimeUnit.MINUTES)) {
+          if (pending != null && !pending.isEmpty()) {
+            log.info(String.format("Finishing %d disk cache updates", pending.size()));
+            for (Runnable update : pending) {
+              update.run();
+            }
+          }
+        } else {
+          log.info("Timeout waiting for disk cache to close");
+        }
+      } catch (InterruptedException e) {
+        log.warn("Interrupted waiting for disk cache to shutdown");
+      }
+    }
+    for (H2CacheImpl<?, ?> cache : caches) {
+      cache.stop();
+    }
+  }
+
+  @SuppressWarnings({"unchecked", "rawtypes", "cast"})
+  @Override
+  public <K, V> Cache<K, V> build(CacheBinding<K, V> def) {
+    Preconditions.checkState(!started, "cache must be built before start");
+    long limit = config.getLong("cache", def.name(), "diskLimit", 128 << 20);
+
+    if (cacheDir == null || limit <= 0) {
+      return defaultFactory.build(def);
+    }
+
+    SqlStore<K, V> store = newSqlStore(def.name(), def.keyType(), limit);
+    H2CacheImpl<K, V> cache = new H2CacheImpl<K, V>(
+        executor, store, def.keyType(),
+        (Cache<K, ValueHolder<V>>) defaultFactory.create(def, true).build());
+    caches.add(cache);
+    return cache;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public <K, V> LoadingCache<K, V> build(
+      CacheBinding<K, V> def,
+      CacheLoader<K, V> loader) {
+    Preconditions.checkState(!started, "cache must be built before start");
+    long limit = config.getLong("cache", def.name(), "diskLimit", 128 << 20);
+
+    if (cacheDir == null || limit <= 0) {
+      return defaultFactory.build(def, loader);
+    }
+
+    SqlStore<K, V> store = newSqlStore(def.name(), def.keyType(), limit);
+    Cache<K, ValueHolder<V>> mem = (Cache<K, ValueHolder<V>>)
+        defaultFactory.create(def, true)
+        .build((CacheLoader<K, V>) new H2CacheImpl.Loader<K, V>(
+              executor, store, loader));
+    H2CacheImpl<K, V> cache = new H2CacheImpl<K, V>(
+        executor, store, def.keyType(), mem);
+    caches.add(cache);
+    return cache;
+  }
+
+  private <V, K> SqlStore<K, V> newSqlStore(
+      String name,
+      TypeLiteral<K> keyType,
+      long maxSize) {
+    File db = new File(cacheDir, name).getAbsoluteFile();
+    String url = "jdbc:h2:" + db.toURI().toString();
+    return new SqlStore<K, V>(url, keyType, maxSize);
+  }
+}
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
new file mode 100644
index 0000000..ad437b7
--- /dev/null
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -0,0 +1,709 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.gerrit.server.cache.h2;
+
+import com.google.common.cache.AbstractLoadingCache;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.CacheStats;
+import com.google.common.cache.LoadingCache;
+import com.google.common.hash.BloomFilter;
+import com.google.common.hash.Funnel;
+import com.google.common.hash.Funnels;
+import com.google.common.hash.PrimitiveSink;
+import com.google.inject.TypeLiteral;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import java.util.Map;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Hybrid in-memory and database backed cache built on H2.
+ * <p>
+ * This cache can be used as either a recall cache, or a loading cache if a
+ * CacheLoader was supplied to its constructor at build time. Before creating an
+ * entry the in-memory cache is checked for the item, then the database is
+ * checked, and finally the CacheLoader is used to construct the item. This is
+ * mostly useful for CacheLoaders that are computationally intensive, such as
+ * the PatchListCache.
+ * <p>
+ * Cache stores and invalidations are performed on a background thread, hiding
+ * the latency associated with serializing the key and value pairs and writing
+ * them to the database log.
+ * <p>
+ * A BloomFilter is used around the database to reduce the number of SELECTs
+ * issued against the database for new cache items that have not been seen
+ * before, a common operation for the PatchListCache. The BloomFilter is sized
+ * when the cache starts to be 64,000 entries or double the number of items
+ * currently in the database table.
+ * <p>
+ * This cache does not export its items as a ConcurrentMap.
+ *
+ * @see H2CacheFactory
+ */
+public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> {
+  private static final Logger log = LoggerFactory.getLogger(H2CacheImpl.class);
+
+  private final Executor executor;
+  private final SqlStore<K, V> store;
+  private final TypeLiteral<K> keyType;
+  private final Cache<K, ValueHolder<V>> mem;
+
+  H2CacheImpl(Executor executor,
+      SqlStore<K, V> store,
+      TypeLiteral<K> keyType,
+      Cache<K, ValueHolder<V>> mem) {
+    this.executor = executor;
+    this.store = store;
+    this.keyType = keyType;
+    this.mem = mem;
+  }
+
+  @Override
+  public V getIfPresent(Object objKey) {
+    if (!keyType.getRawType().isInstance(objKey)) {
+      return null;
+    }
+
+    @SuppressWarnings("unchecked")
+    K key = (K) objKey;
+
+    ValueHolder<V> h = mem.getIfPresent(key);
+    if (h != null) {
+      return h.value;
+    }
+
+    if (store.mightContain(key)) {
+      h = store.getIfPresent(key);
+      if (h != null) {
+        mem.put(key, h);
+        return h.value;
+      }
+    }
+    return null;
+  }
+
+  @Override
+  public V get(K key) throws ExecutionException {
+    if (mem instanceof LoadingCache) {
+      return ((LoadingCache<K, ValueHolder<V>>) mem).get(key).value;
+    }
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void put(final K key, V val) {
+    final ValueHolder<V> h = new ValueHolder<V>(val);
+    h.created = System.currentTimeMillis();
+    mem.put(key, h);
+    executor.execute(new Runnable() {
+      @Override
+      public void run() {
+        store.put(key, h);
+      }
+    });
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public void invalidate(final Object key) {
+    if (keyType.getRawType().isInstance(key) && store.mightContain((K) key)) {
+      executor.execute(new Runnable() {
+        @Override
+        public void run() {
+          store.invalidate((K) key);
+        }
+      });
+    }
+    mem.invalidate(key);
+  }
+
+  @Override
+  public void invalidateAll() {
+    store.invalidateAll();
+    mem.invalidateAll();
+  }
+
+  @Override
+  public long size() {
+    return mem.size();
+  }
+
+  @Override
+  public CacheStats stats() {
+    return mem.stats();
+  }
+
+  public DiskStats diskStats() {
+    return store.diskStats();
+  }
+
+  void start() {
+    store.open();
+  }
+
+  void stop() {
+    for (Map.Entry<K, ValueHolder<V>> e : mem.asMap().entrySet()) {
+      ValueHolder<V> h = e.getValue();
+      if (!h.clean) {
+        store.put(e.getKey(), h);
+      }
+    }
+    store.close();
+  }
+
+  void prune(final ScheduledExecutorService service) {
+    store.prune(mem);
+
+    Calendar cal = Calendar.getInstance();
+    cal.set(Calendar.HOUR_OF_DAY, 01);
+    cal.set(Calendar.MINUTE, 0);
+    cal.set(Calendar.SECOND, 0);
+    cal.set(Calendar.MILLISECOND, 0);
+    cal.add(Calendar.DAY_OF_MONTH, 1);
+
+    long delay = cal.getTimeInMillis() - System.currentTimeMillis();
+    service.schedule(new Runnable() {
+      @Override
+      public void run() {
+        prune(service);
+      }
+    }, delay, TimeUnit.MILLISECONDS);
+  }
+
+  public static class DiskStats {
+    long size;
+    long space;
+    long hitCount;
+    long missCount;
+
+    public long size() {
+      return size;
+    }
+
+    public long space() {
+      return space;
+    }
+
+    public long hitCount() {
+      return hitCount;
+    }
+
+    public long requestCount() {
+      return hitCount + missCount;
+    }
+  }
+
+  static class ValueHolder<V> {
+    final V value;
+    long created;
+    volatile boolean clean;
+
+    ValueHolder(V value) {
+      this.value = value;
+    }
+  }
+
+  static class Loader<K, V> extends CacheLoader<K, ValueHolder<V>> {
+    private final Executor executor;
+    private final SqlStore<K, V> store;
+    private final CacheLoader<K, V> loader;
+
+    Loader(Executor executor, SqlStore<K, V> store, CacheLoader<K, V> loader) {
+      this.executor = executor;
+      this.store = store;
+      this.loader = loader;
+    }
+
+    @Override
+    public ValueHolder<V> load(final K key) throws Exception {
+      if (store.mightContain(key)) {
+        ValueHolder<V> h = store.getIfPresent(key);
+        if (h != null) {
+          return h;
+        }
+      }
+
+      final ValueHolder<V> h = new ValueHolder<V>(loader.load(key));
+      h.created = System.currentTimeMillis();
+      executor.execute(new Runnable() {
+        @Override
+        public void run() {
+          store.put(key, h);
+        }
+      });
+      return h;
+    }
+  }
+
+  private static class KeyType<K> {
+    String columnType() {
+      return "OTHER";
+    }
+
+    @SuppressWarnings("unchecked")
+    K get(ResultSet rs, int col) throws SQLException {
+      return (K) rs.getObject(col);
+    }
+
+    void set(PreparedStatement ps, int col, K value) throws SQLException {
+      ps.setObject(col, value);
+    }
+
+    Funnel<K> funnel() {
+      return new Funnel<K>() {
+        @Override
+        public void funnel(K from, PrimitiveSink into) {
+          try {
+            ObjectOutputStream ser =
+                new ObjectOutputStream(new SinkOutputStream(into));
+            ser.writeObject(from);
+            ser.flush();
+          } catch (IOException err) {
+            throw new RuntimeException("Cannot hash as Serializable", err);
+          }
+        }
+      };
+    }
+
+    @SuppressWarnings("unchecked")
+    static <K> KeyType<K> create(TypeLiteral<K> type) {
+      if (type.getRawType() == String.class) {
+        return (KeyType<K>) STRING;
+      }
+      return (KeyType<K>) OTHER;
+    }
+
+    static final KeyType<?> OTHER = new KeyType<Object>();
+    static final KeyType<String> STRING = new KeyType<String>() {
+      @Override
+      String columnType() {
+        return "VARCHAR(4096)";
+      }
+
+      @Override
+      String get(ResultSet rs, int col) throws SQLException {
+        return rs.getString(col);
+      }
+
+      @Override
+      void set(PreparedStatement ps, int col, String value)
+          throws SQLException {
+        ps.setString(col, value);
+      }
+
+      @SuppressWarnings("unchecked")
+      @Override
+      Funnel<String> funnel() {
+        Funnel<?> s = Funnels.stringFunnel();
+        return (Funnel<String>) s;
+      }
+    };
+  }
+
+  static class SqlStore<K, V> {
+    private final String url;
+    private final KeyType<K> keyType;
+    private final long maxSize;
+    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) {
+      this.url = jdbcUrl;
+      this.keyType = KeyType.create(keyType);
+      this.maxSize = maxSize;
+
+      int cores = Runtime.getRuntime().availableProcessors();
+      int keep = Math.min(cores, 16);
+      this.handles = new ArrayBlockingQueue<SqlHandle>(keep);
+    }
+
+    synchronized void open() {
+      if (bloomFilter == null) {
+        bloomFilter = buildBloomFilter();
+      }
+    }
+
+    void close() {
+      SqlHandle h;
+      while ((h = handles.poll()) != null) {
+        h.close();
+      }
+    }
+
+    boolean mightContain(K key) {
+      BloomFilter<K> b = bloomFilter;
+      if (b == null) {
+        synchronized (this) {
+          b = bloomFilter;
+          if (b == null) {
+            b = buildBloomFilter();
+            bloomFilter = b;
+          }
+        }
+      }
+      return b == null || b.mightContain(key);
+    }
+
+    private BloomFilter<K> buildBloomFilter() {
+      SqlHandle c = null;
+      try {
+        c = acquire();
+        Statement s = c.conn.createStatement();
+        try {
+          ResultSet r;
+          if (estimatedSize <= 0) {
+            r = s.executeQuery("SELECT COUNT(*) FROM data");
+            try {
+              estimatedSize = r.next() ? r.getInt(1) : 0;
+            } finally {
+              r.close();
+            }
+          }
+
+          BloomFilter<K> b = newBloomFilter();
+          r = s.executeQuery("SELECT k FROM data");
+          try {
+            while (r.next()) {
+              b.put(keyType.get(r, 1));
+            }
+          } finally {
+            r.close();
+          }
+          return b;
+        } finally {
+          s.close();
+        }
+      } catch (SQLException e) {
+        log.warn("Cannot build BloomFilter for " + url, e);
+        c = close(c);
+        return null;
+      } finally {
+        release(c);
+      }
+    }
+
+    ValueHolder<V> getIfPresent(K key) {
+      SqlHandle c = null;
+      try {
+        c = acquire();
+        if (c.get == null) {
+          c.get = c.conn.prepareStatement("SELECT v FROM data WHERE k=?");
+        }
+        keyType.set(c.get, 1, key);
+        ResultSet r = c.get.executeQuery();
+        try {
+          if (!r.next()) {
+            missCount.incrementAndGet();
+            return null;
+          }
+
+          @SuppressWarnings("unchecked")
+          V val = (V) r.getObject(1);
+          ValueHolder<V> h = new ValueHolder<V>(val);
+          h.clean = true;
+          hitCount.incrementAndGet();
+          touch(c, key);
+          return h;
+        } finally {
+          r.close();
+          c.get.clearParameters();
+        }
+      } catch (SQLException e) {
+        log.warn("Cannot read cache " + url + " for " + key, e);
+        c = close(c);
+        return null;
+      } finally {
+        release(c);
+      }
+    }
+
+    private void touch(SqlHandle c, K key) throws SQLException {
+      if (c.touch == null) {
+        c.touch =c.conn.prepareStatement("UPDATE data SET accessed=? WHERE k=?");
+      }
+      try {
+        c.touch.setTimestamp(1, new Timestamp(System.currentTimeMillis()));
+        keyType.set(c.touch, 2, key);
+        c.touch.executeUpdate();
+      } finally {
+        c.touch.clearParameters();
+      }
+    }
+
+    void put(K key, ValueHolder<V> holder) {
+      if (holder.clean) {
+        return;
+      }
+
+      BloomFilter<K> b = bloomFilter;
+      if (b != null) {
+        b.put(key);
+        bloomFilter = b;
+      }
+
+      SqlHandle c = null;
+      try {
+        c = acquire();
+        if (c.put == null) {
+          c.put = c.conn.prepareStatement("MERGE INTO data VALUES(?,?,?,?)");
+        }
+        try {
+          keyType.set(c.put, 1, key);
+          c.put.setObject(2, holder.value);
+          c.put.setTimestamp(3, new Timestamp(holder.created));
+          c.put.setTimestamp(4, new Timestamp(System.currentTimeMillis()));
+          c.put.executeUpdate();
+          holder.clean = true;
+        } finally {
+          c.put.clearParameters();
+        }
+      } catch (SQLException e) {
+        log.warn("Cannot put into cache " + url, e);
+        c = close(c);
+      } finally {
+        release(c);
+      }
+    }
+
+    void invalidate(K key) {
+      SqlHandle c = null;
+      try {
+        c = acquire();
+        invalidate(c, key);
+      } catch (SQLException e) {
+        log.warn("Cannot invalidate cache " + url, e);
+        c = close(c);
+      } finally {
+        release(c);
+      }
+    }
+
+    private void invalidate(SqlHandle c, K key) throws SQLException {
+      if (c.invalidate == null) {
+        c.invalidate = c.conn.prepareStatement("DELETE FROM data WHERE k=?");
+      }
+      try {
+        keyType.set(c.invalidate, 1, key);
+        c.invalidate.executeUpdate();
+      } finally {
+        c.invalidate.clearParameters();
+      }
+    }
+
+    void invalidateAll() {
+      SqlHandle c = null;
+      try {
+        c = acquire();
+        Statement s = c.conn.createStatement();
+        try {
+          s.executeUpdate("DELETE FROM data");
+        } finally {
+          s.close();
+        }
+        bloomFilter = newBloomFilter();
+      } catch (SQLException e) {
+        log.warn("Cannot invalidate cache " + url, e);
+        c = close(c);
+      } finally {
+        release(c);
+      }
+    }
+
+    void prune(Cache<K, ?> mem) {
+      SqlHandle c = null;
+      try {
+        c = acquire();
+        Statement s = c.conn.createStatement();
+        try {
+          long used = 0;
+          ResultSet r = s.executeQuery("SELECT"
+              + " SUM(OCTET_LENGTH(k) + OCTET_LENGTH(v))"
+              + " FROM data");
+          try {
+            used = r.next() ? r.getLong(1) : 0;
+          } finally {
+            r.close();
+          }
+          if (used <= maxSize) {
+            return;
+          }
+
+          r = s.executeQuery("SELECT"
+              + " k"
+              + ",OCTET_LENGTH(k) + OCTET_LENGTH(v)"
+              + " FROM data"
+              + " ORDER BY accessed");
+          try {
+            while (maxSize < used && r.next()) {
+              K key = keyType.get(r, 1);
+              if (mem.getIfPresent(key) != null) {
+                touch(c, key);
+              } else {
+                invalidate(c, key);
+                used -= r.getLong(2);
+              }
+            }
+          } finally {
+            r.close();
+          }
+        } finally {
+          s.close();
+        }
+      } catch (SQLException e) {
+        log.warn("Cannot prune cache " + url, e);
+        c = close(c);
+      } finally {
+        release(c);
+      }
+    }
+
+    DiskStats diskStats() {
+      DiskStats d = new DiskStats();
+      d.hitCount = hitCount.get();
+      d.missCount = missCount.get();
+      SqlHandle c = null;
+      try {
+        c = acquire();
+        Statement s = c.conn.createStatement();
+        try {
+          ResultSet r = s.executeQuery("SELECT"
+              + " COUNT(*)"
+              + ",SUM(OCTET_LENGTH(k) + OCTET_LENGTH(v))"
+              + " FROM data");
+          try {
+            if (r.next()) {
+              d.size = r.getLong(1);
+              d.space = r.getLong(2);
+            }
+          } finally {
+            r.close();
+          }
+        } finally {
+          s.close();
+        }
+      } catch (SQLException e) {
+        log.warn("Cannot get DiskStats for " + url, e);
+        c = close(c);
+      } finally {
+        release(c);
+      }
+      return d;
+    }
+
+    private SqlHandle acquire() throws SQLException {
+      SqlHandle h = handles.poll();
+      return h != null ? h : new SqlHandle(url, keyType);
+    }
+
+    private void release(SqlHandle h) {
+      if (h != null && !handles.offer(h)) {
+        h.close();
+      }
+    }
+
+    private SqlHandle close(SqlHandle h) {
+      if (h != null) {
+        h.close();
+      }
+      return null;
+    }
+
+    private BloomFilter<K> newBloomFilter() {
+      int cnt = Math.max(64 * 1024, 2 * estimatedSize);
+      return BloomFilter.create(keyType.funnel(), cnt);
+    }
+  }
+
+  static class SqlHandle {
+    private final String url;
+    Connection conn;
+    PreparedStatement get;
+    PreparedStatement put;
+    PreparedStatement touch;
+    PreparedStatement invalidate;
+
+    SqlHandle(String url, KeyType<?> type) throws SQLException {
+      this.url = url;
+      this.conn = org.h2.Driver.load().connect(url, null);
+      Statement stmt = conn.createStatement();
+      try {
+        stmt.execute("CREATE TABLE IF NOT EXISTS data"
+          + "(k " + type.columnType() + " NOT NULL PRIMARY KEY HASH"
+          + ",v OTHER NOT NULL"
+          + ",created TIMESTAMP NOT NULL"
+          + ",accessed TIMESTAMP NOT NULL"
+          + ")");
+      } finally {
+        stmt.close();
+      }
+    }
+
+    void close() {
+      get = closeStatement(get);
+      put = closeStatement(put);
+      touch = closeStatement(touch);
+      invalidate = closeStatement(invalidate);
+
+      if (conn != null) {
+        try {
+          conn.close();
+        } catch (SQLException e) {
+          log.warn("Cannot close connection to " + url, e);
+        } finally {
+          conn = null;
+        }
+      }
+    }
+
+    private PreparedStatement closeStatement(PreparedStatement ps) {
+      if (ps != null) {
+        try {
+          ps.close();
+        } catch (SQLException e) {
+          log.warn("Cannot close statement for " + url, e);
+        }
+      }
+      return null;
+    }
+  }
+
+  private static class SinkOutputStream extends OutputStream {
+    private final PrimitiveSink sink;
+
+    SinkOutputStream(PrimitiveSink sink) {
+      this.sink = sink;
+    }
+
+    @Override
+    public void write(int b) {
+      sink.putByte((byte)b);
+    }
+
+    @Override
+    public void write(byte[] b, int p, int n) {
+      sink.putBytes(b, p, n);
+    }
+  }
+}
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 25f4f22a..10b1924 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
@@ -14,13 +14,11 @@
 
 package com.google.gerrit.common;
 
-import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gwtorm.client.KeyUtil;
 
 public class PageLinks {
@@ -42,6 +40,7 @@
   public static final String ADMIN_GROUPS = "/admin/groups/";
   public static final String ADMIN_PROJECTS = "/admin/projects/";
   public static final String ADMIN_CREATE_PROJECT = "/admin/create-project/";
+  public static final String ADMIN_PLUGINS = "/admin/plugins/";
 
   public static String toChange(final ChangeInfo c) {
     return toChange(c.getId());
@@ -60,15 +59,8 @@
   }
 
   public static String toAccountQuery(final String fullname) {
-    return "/q/owner:\"" + KeyUtil.encode(fullname) + "\"," + TOP;
-  }
-
-  public static String toAccountDashboard(final AccountInfo acct) {
-    return toAccountDashboard(acct.getId());
-  }
-
-  public static String toAccountDashboard(final Account.Id acct) {
-    return "/dashboard/" + acct.toString();
+    String query = op("owner", fullname) + " status:open";
+    return toChangeQuery(query, TOP);
   }
 
   public static String toChangeQuery(final String query) {
@@ -76,8 +68,7 @@
   }
 
   public static String toChangeQuery(String query, String page) {
-    query = KeyUtil.encode(query).replaceAll("%3[Aa]", ":");
-    return "/q/" + query + "," + page;
+    return "/q/" + KeyUtil.encode(query) + "," + page;
   }
 
   public static String projectQuery(Project.NameKey proj, Status status) {
@@ -95,11 +86,18 @@
     }
   }
 
-  public static String op(String name, String value) {
-    if (value.indexOf(' ') >= 0) {
-      return name + ":\"" + value + "\"";
+  public static String op(String op, String value) {
+    if (isSingleWord(value)) {
+      return op + ":" + value;
     }
-    return name + ":" + value;
+    return op + ":\"" + value + "\"";
+  }
+
+  private static boolean isSingleWord(String value) {
+    if (value.startsWith("-")) {
+      return false;
+    }
+    return value.matches("[^\u0000-\u0020!\"#$%&'():;?\\[\\]{}~]+");
   }
 
   protected PageLinks() {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
index 04f0ecd..cd64b0a 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
@@ -97,7 +97,7 @@
       if (dst != null) {
         dst.mergeFrom(src);
       } else {
-        permissions.add(dst);
+        permissions.add(src);
       }
     }
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java
index f646bc6..0ddd239 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java
@@ -15,33 +15,14 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
 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 java.util.Set;
-
 @RpcImpl(version = Version.V2_0)
 public interface ChangeListService extends RemoteJsonService {
-  /** Get all changes which match an arbitrary query string. */
-  void allQueryPrev(String query, String pos, int limit,
-      AsyncCallback<SingleListChangeInfo> callback);
-
-  /** Get all changes which match an arbitrary query string. */
-  void allQueryNext(String query, String pos, int limit,
-      AsyncCallback<SingleListChangeInfo> callback);
-
-  /** Get the data to show AccountDashboardScreen for an account. */
-  void forAccount(Account.Id id, AsyncCallback<AccountDashboardInfo> callback);
-
-  /** Get the ids of all changes starred by the caller. */
-  @SignInRequired
-  void myStarredChangeIds(AsyncCallback<Set<Change.Id>> callback);
-
   /**
    * Add and/or remove changes from the set of starred changes of the caller.
    *
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
index 456ffb4..07a8534 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
@@ -16,9 +16,10 @@
 
 import com.google.gerrit.common.auth.openid.OpenIdProviderPattern;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gwtexpui.safehtml.client.RegexFindReplace;
 
 import java.util.List;
@@ -38,6 +39,7 @@
   protected String gitDaemonUrl;
   protected String gitHttpUrl;
   protected String sshdAddress;
+  protected String editFullNameUrl;
   protected Project.NameKey wildProject;
   protected ApprovalTypes approvalTypes;
   protected Set<Account.FieldName> editableAccountFields;
@@ -54,6 +56,14 @@
     registerUrl = u;
   }
 
+  public String getEditFullNameUrl() {
+    return editFullNameUrl;
+  }
+
+  public void setEditFullNameUrl(String u) {
+    editFullNameUrl = u;
+  }
+
   public String getHttpPasswordUrl() {
     return httpPasswordUrl;
   }
@@ -199,4 +209,13 @@
   public void setAnonymousCowardName(final String anonymousCowardName) {
     this.anonymousCowardName = anonymousCowardName;
   }
+
+  public boolean siteHasUsernames() {
+    if (getAuthType() == AuthType.CUSTOM_EXTENSION
+        && getHttpPasswordUrl() != null
+        && !canEdit(FieldName.USER_NAME)) {
+      return false;
+    }
+    return true;
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java
index f385e27..ffa1e3e 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java
@@ -24,7 +24,6 @@
 import com.google.gwtjsonrpc.common.VoidResult;
 import com.google.gwtjsonrpc.common.RpcImpl.Version;
 
-import java.util.List;
 import java.util.Set;
 
 @RpcImpl(version = Version.V2_0)
@@ -60,14 +59,6 @@
       AsyncCallback<VoidResult> callback);
 
   @SignInRequired
-  void changeExternalGroup(AccountGroup.Id groupId,
-      AccountGroup.ExternalNameKey bindTo, AsyncCallback<VoidResult> callback);
-
-  @SignInRequired
-  void searchExternalGroups(String searchFilter,
-      AsyncCallback<List<AccountGroup.ExternalNameKey>> callback);
-
-  @SignInRequired
   void addGroupMember(AccountGroup.Id groupId, String nameOrEmail,
       AsyncCallback<GroupDetail> callback);
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java
new file mode 100644
index 0000000..828bf24
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common.data;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+/**
+ * Group methods exposed by the GroupBackend.
+ */
+public class GroupDescription {
+  /**
+   * The Basic information required to be exposed by any Group.
+   */
+  public interface Basic {
+    /** @return the non-null UUID of the group. */
+    AccountGroup.UUID getGroupUUID();
+
+    /** @return the non-null name of the group. */
+    String getName();
+
+    /** @return whether the group is visible to all accounts. */
+    boolean isVisibleToAll();
+  }
+
+  /**
+   * The extended information exposed by internal groups backed by an
+   * AccountGroup.
+   */
+  public interface Internal extends Basic {
+    /** @return the backing AccountGroup. */
+    AccountGroup getAccountGroup();
+  }
+
+  private GroupDescription() {
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
new file mode 100644
index 0000000..e0bc7d8
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common.data;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utility class for building GroupDescription objects.
+ */
+public class GroupDescriptions {
+
+  @Nullable
+  public static AccountGroup toAccountGroup(GroupDescription.Basic group) {
+    if (group instanceof GroupDescription.Internal) {
+      return ((GroupDescription.Internal) group).getAccountGroup();
+    }
+    return null;
+  }
+
+  public static GroupDescription.Internal forAccountGroup(final AccountGroup group) {
+    return new GroupDescription.Internal() {
+      @Override
+      public AccountGroup.UUID getGroupUUID() {
+        return group.getGroupUUID();
+      }
+
+      @Override
+      public String getName() {
+        return group.getName();
+      }
+
+      @Override
+      public boolean isVisibleToAll() {
+        return group.isVisibleToAll();
+      }
+
+      @Override
+      public AccountGroup getAccountGroup() {
+        return group;
+      }
+    };
+  }
+
+  private GroupDescriptions() {
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
index 65723f7..01c7985 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
@@ -26,7 +26,7 @@
   public AccountGroup group;
   public List<AccountGroupMember> members;
   public List<AccountGroupInclude> includes;
-  public AccountGroup ownerGroup;
+  public GroupReference ownerGroup;
   public boolean canModify;
 
   public GroupDetail() {
@@ -52,7 +52,7 @@
     includes = i;
   }
 
-  public void setOwnerGroup(AccountGroup g) {
+  public void setOwnerGroup(GroupReference g) {
     ownerGroup = g;
   }
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
index f05d1b9..c261fdd 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
@@ -23,6 +23,10 @@
     return new GroupReference(group.getGroupUUID(), group.getName());
   }
 
+  public static GroupReference forGroup(GroupDescription.Basic group) {
+    return new GroupReference(group.getGroupUUID(), group.getName());
+  }
+
   protected String uuid;
   protected String name;
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
index 68676cf..2a70d6c 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.common.data;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -62,28 +63,9 @@
       raw.append(pattern.substring(i, b));
       ops.add(new Constant(pattern.substring(i, b)));
 
-      String expr = pattern.substring(b + 2, e);
-      String parameterName = "";
-      List<Function> functions = new ArrayList<Function>();
-      if (!expr.contains(".")) {
-        parameterName = expr;
-      } else {
-        int firstDot = expr.indexOf('.');
-        parameterName = expr.substring(0, firstDot);
-        String actionsStr = expr.substring(firstDot + 1);
-        String[] actions = actionsStr.split("\\.");
+      // "${parameter[.functions...]}" -> "parameter[.functions...]"
+      final Parameter p = new Parameter(pattern.substring(b + 2, e));
 
-        for (String action : actions) {
-          Function function = FUNCTIONS.get(action);
-          if (function == null) {
-            function = NOOP;
-          }
-          functions.add(function);
-        }
-      }
-
-      final Parameter p =
-          new Parameter(parameterName, Collections.unmodifiableList(functions));
       raw.append("{" + prs.size() + "}");
       prs.add(p);
       ops.add(p);
@@ -184,9 +166,25 @@
     private final String name;
     private final List<Function> functions;
 
-    Parameter(final String name, final List<Function> functions) {
-      this.name = name;
-      this.functions = functions;
+    Parameter(final String parameter) {
+      // "parameter[.functions...]" -> (parameter, functions...)
+      final List<String> names = Arrays.asList(parameter.split("\\."));
+      final List<Function> functs = new ArrayList<Function>(names.size());
+
+      if (names.isEmpty()) {
+        name = "";
+      } else {
+        name = names.get(0);
+
+        for (String fname : names.subList(1, names.size())) {
+          final Function function = FUNCTIONS.get(fname);
+          if (function != null) {
+            functs.add(function);
+          }
+        }
+      }
+
+      functions = Collections.unmodifiableList(functs);
     }
 
     @Override
@@ -207,12 +205,6 @@
   }
 
   private static final Map<String, Function> FUNCTIONS = initFunctions();
-  private static final Function NOOP = new Function() {
-    @Override
-    String apply(String a) {
-      return a;
-    }
-  };
 
   private static Map<String, Function> initFunctions() {
     final HashMap<String, Function> m = new HashMap<String, Function>();
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 20261de..4067349 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
@@ -20,6 +20,7 @@
 
 /** A single permission within an {@link AccessSection} of a project. */
 public class Permission implements Comparable<Permission> {
+  public static final String ABANDON = "abandon";
   public static final String CREATE = "create";
   public static final String FORGE_AUTHOR = "forgeAuthor";
   public static final String FORGE_COMMITTER = "forgeCommitter";
@@ -40,6 +41,7 @@
     NAMES_LC = new ArrayList<String>();
     NAMES_LC.add(OWNER.toLowerCase());
     NAMES_LC.add(READ.toLowerCase());
+    NAMES_LC.add(ABANDON.toLowerCase());
     NAMES_LC.add(CREATE.toLowerCase());
     NAMES_LC.add(FORGE_AUTHOR.toLowerCase());
     NAMES_LC.add(FORGE_COMMITTER.toLowerCase());
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java
index 1b504b0..df6728e 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java
@@ -28,10 +28,7 @@
 
 @RpcImpl(version = Version.V2_0)
 public interface ProjectAdminService extends RemoteJsonService {
-  void visibleProjects(AsyncCallback<ProjectList> callback);
-
   void visibleProjectDetails(AsyncCallback<List<ProjectDetail>> callback);
-  void suggestParentCandidates(AsyncCallback<List<Project>> callback);
 
   void projectDetail(Project.NameKey projectName,
       AsyncCallback<ProjectDetail> callback);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectList.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectList.java
deleted file mode 100644
index 8511460..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectList.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License
-
-package com.google.gerrit.common.data;
-
-import com.google.gerrit.reviewdb.client.Project;
-
-import java.util.List;
-
-public class ProjectList {
-  protected List<Project> projects;
-  protected boolean canCreateProject;
-
-  public ProjectList() {
-  }
-
-  public List<Project> getProjects() {
-    return projects;
-  }
-
-  public void setProjects(List<Project> projects) {
-    this.projects = projects;
-  }
-
-  public boolean canCreateProject() {
-    return canCreateProject;
-  }
-
-  public void setCanCreateProject(boolean canCreateProject) {
-    this.canCreateProject = canCreateProject;
-  }
-}
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 28cf49b..c0bf818 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
@@ -63,6 +63,9 @@
       /** Review operation invalid because change is closed. */
       CHANGE_IS_CLOSED,
 
+      /** Review operation invalid because change is not abandoned. */
+      CHANGE_NOT_ABANDONED,
+
       /** Not permitted to publish this draft patch set */
       PUBLISH_NOT_PERMITTED,
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
index 5049ba4..365f6a9 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
@@ -68,6 +68,12 @@
       NEED,
 
       /**
+       * The label may be set, but it's neither necessary for submission
+       * nor does it block submission if set.
+       */
+      MAY,
+
+      /**
        * The label is required for submission, but is impossible to complete.
        * The likely cause is access has not been granted correctly by the
        * project owner or site administrator.
@@ -78,5 +84,34 @@
     public String label;
     public Status status;
     public Account.Id appliedBy;
+
+    @Override
+    public String toString() {
+      StringBuilder sb = new StringBuilder();
+      sb.append(label).append(": ").append(status);
+      if (appliedBy != null) {
+        sb.append(" by ").append(appliedBy);
+      }
+      return sb.toString();
+    }
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append(status);
+    if (status == Status.RULE_ERROR && errorMessage != null) {
+      sb.append('(').append(errorMessage).append(')');
+    }
+    sb.append('[');
+    if (labels != null) {
+      String delimiter = "";
+      for (Label label : labels) {
+        sb.append(delimiter).append(label);
+        delimiter = ", ";
+      }
+    }
+    sb.append(']');
+    return sb.toString();
   }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
index d52a724..7205b74 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
@@ -29,9 +29,16 @@
   void suggestAccount(String query, Boolean enabled, int limit,
       AsyncCallback<List<AccountInfo>> callback);
 
+  /**
+   * @see #suggestAccountGroup(com.google.gerrit.reviewdb.client.Project.NameKey, String, int, AsyncCallback)
+   */
+  @Deprecated
   void suggestAccountGroup(String query, int limit,
       AsyncCallback<List<GroupReference>> callback);
 
+  void suggestAccountGroupForProject(Project.NameKey project, String query,
+      int limit, AsyncCallback<List<GroupReference>> callback);
+
   /**
    * @see #suggestChangeReviewer(com.google.gerrit.reviewdb.client.Change.Id, String, int, AsyncCallback)
    */
diff --git a/gerrit-ehcache/.settings/org.eclipse.core.resources.prefs b/gerrit-ehcache/.settings/org.eclipse.core.resources.prefs
deleted file mode 100644
index 82eb859..0000000
--- a/gerrit-ehcache/.settings/org.eclipse.core.resources.prefs
+++ /dev/null
@@ -1,3 +0,0 @@
-#Tue Sep 02 16:59:24 PDT 2008
-eclipse.preferences.version=1
-encoding/<project>=UTF-8
diff --git a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java
deleted file mode 100644
index f4e85ba..0000000
--- a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java
+++ /dev/null
@@ -1,272 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.ehcache;
-
-import static java.util.concurrent.TimeUnit.MINUTES;
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import com.google.gerrit.lifecycle.LifecycleListener;
-import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.CachePool;
-import com.google.gerrit.server.cache.CacheProvider;
-import com.google.gerrit.server.cache.EntryCreator;
-import com.google.gerrit.server.cache.EvictionPolicy;
-import com.google.gerrit.server.cache.ProxyCache;
-import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-import net.sf.ehcache.CacheManager;
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.config.CacheConfiguration;
-import net.sf.ehcache.config.Configuration;
-import net.sf.ehcache.config.DiskStoreConfiguration;
-import net.sf.ehcache.store.MemoryStoreEvictionPolicy;
-
-import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Pool of all declared caches created by {@link CacheModule}s. */
-@Singleton
-public class EhcachePoolImpl implements CachePool {
-  private static final Logger log =
-      LoggerFactory.getLogger(EhcachePoolImpl.class);
-
-  public static class Module extends LifecycleModule {
-    @Override
-    protected void configure() {
-      bind(CachePool.class).to(EhcachePoolImpl.class);
-      bind(EhcachePoolImpl.class);
-      listener().to(EhcachePoolImpl.Lifecycle.class);
-    }
-  }
-
-  public static class Lifecycle implements LifecycleListener {
-    private final EhcachePoolImpl cachePool;
-
-    @Inject
-    Lifecycle(final EhcachePoolImpl cachePool) {
-      this.cachePool = cachePool;
-    }
-
-    @Override
-    public void start() {
-      cachePool.start();
-    }
-
-    @Override
-    public void stop() {
-      cachePool.stop();
-    }
-  }
-
-  private final Config config;
-  private final SitePaths site;
-
-  private final Object lock = new Object();
-  private final Map<String, CacheProvider<?, ?>> caches;
-  private CacheManager manager;
-
-  @Inject
-  EhcachePoolImpl(@GerritServerConfig final Config cfg, final SitePaths site) {
-    this.config = cfg;
-    this.site = site;
-    this.caches = new HashMap<String, CacheProvider<?, ?>>();
-  }
-
-  @SuppressWarnings({"rawtypes", "unchecked"})
-  private void start() {
-    synchronized (lock) {
-      if (manager != null) {
-        throw new IllegalStateException("Cache pool has already been started");
-      }
-
-      try {
-        System.setProperty("net.sf.ehcache.skipUpdateCheck", "" + true);
-      } catch (SecurityException e) {
-        // Ignore it, the system is just going to ping some external page
-        // using a background thread and there's not much we can do about
-        // it now.
-      }
-
-      manager = new CacheManager(new Factory().toConfiguration());
-      for (CacheProvider<?, ?> p : caches.values()) {
-        Ehcache eh = manager.getEhcache(p.getName());
-        EntryCreator<?, ?> c = p.getEntryCreator();
-        if (c != null) {
-          p.bind(new PopulatingCache(eh, c));
-        } else {
-          p.bind(new SimpleCache(eh));
-        }
-      }
-    }
-  }
-
-  private void stop() {
-    synchronized (lock) {
-      if (manager != null) {
-        manager.shutdown();
-      }
-    }
-  }
-
-  /** <i>Discouraged</i> Get the underlying cache descriptions, for statistics. */
-  public CacheManager getCacheManager() {
-    synchronized (lock) {
-      return manager;
-    }
-  }
-
-  public <K, V> ProxyCache<K, V> register(final CacheProvider<K, V> provider) {
-    synchronized (lock) {
-      if (manager != null) {
-        throw new IllegalStateException("Cache pool has already been started");
-      }
-
-      final String n = provider.getName();
-      if (caches.containsKey(n) && caches.get(n) != provider) {
-        throw new IllegalStateException("Cache \"" + n + "\" already defined");
-      }
-      caches.put(n, provider);
-      return new ProxyCache<K, V>();
-    }
-  }
-
-  private class Factory {
-    private static final int MB = 1024 * 1024;
-    private final Configuration mgr = new Configuration();
-
-    Configuration toConfiguration() {
-      configureDiskStore();
-      configureDefaultCache();
-
-      for (CacheProvider<?, ?> p : caches.values()) {
-        final String name = p.getName();
-        final CacheConfiguration c = newCache(name);
-        c.setMemoryStoreEvictionPolicyFromObject(toPolicy(p.evictionPolicy()));
-
-        c.setMaxElementsInMemory(getInt(name, "memorylimit", p.memoryLimit()));
-
-        c.setTimeToIdleSeconds(0);
-        c.setTimeToLiveSeconds(getSeconds(name, "maxage", p.maxAge()));
-        c.setEternal(c.getTimeToLiveSeconds() == 0);
-
-        if (p.disk() && mgr.getDiskStoreConfiguration() != null) {
-          c.setMaxElementsOnDisk(getInt(name, "disklimit", p.diskLimit()));
-
-          int v = c.getDiskSpoolBufferSizeMB() * MB;
-          v = getInt(name, "diskbuffer", v) / MB;
-          c.setDiskSpoolBufferSizeMB(Math.max(1, v));
-          c.setOverflowToDisk(c.getMaxElementsOnDisk() > 0);
-          c.setDiskPersistent(c.getMaxElementsOnDisk() > 0);
-        }
-
-        mgr.addCache(c);
-      }
-
-      return mgr;
-    }
-
-    private MemoryStoreEvictionPolicy toPolicy(final EvictionPolicy policy) {
-      switch (policy) {
-        case LFU:
-          return MemoryStoreEvictionPolicy.LFU;
-
-        case LRU:
-          return MemoryStoreEvictionPolicy.LRU;
-
-        default:
-          throw new IllegalArgumentException("Unsupported " + policy);
-      }
-    }
-
-    private int getInt(String n, String s, int d) {
-      return config.getInt("cache", n, s, d);
-    }
-
-    private long getSeconds(String n, String s, long d) {
-      d = MINUTES.convert(d, SECONDS);
-      long m = ConfigUtil.getTimeUnit(config, "cache", n, s, d, MINUTES);
-      return SECONDS.convert(m, MINUTES);
-    }
-
-    private void configureDiskStore() {
-      boolean needDisk = false;
-      for (CacheProvider<?, ?> p : caches.values()) {
-        if (p.disk()) {
-          needDisk = true;
-          break;
-        }
-      }
-      if (!needDisk) {
-        return;
-      }
-
-      File loc = site.resolve(config.getString("cache", null, "directory"));
-      if (loc == null) {
-      } else if (loc.exists() || loc.mkdirs()) {
-        if (loc.canWrite()) {
-          final DiskStoreConfiguration c = new DiskStoreConfiguration();
-          c.setPath(loc.getAbsolutePath());
-          mgr.addDiskStore(c);
-          log.info("Enabling disk cache " + loc.getAbsolutePath());
-        } else {
-          log.warn("Can't write to disk cache: " + loc.getAbsolutePath());
-        }
-      } else {
-        log.warn("Can't create disk cache: " + loc.getAbsolutePath());
-      }
-    }
-
-    private CacheConfiguration newConfiguration() {
-      CacheConfiguration c = new CacheConfiguration();
-
-      c.setMaxElementsInMemory(1024);
-      c.setMemoryStoreEvictionPolicyFromObject(MemoryStoreEvictionPolicy.LFU);
-
-      c.setTimeToIdleSeconds(0);
-      c.setTimeToLiveSeconds(0 /* infinite */);
-      c.setEternal(true);
-
-      if (mgr.getDiskStoreConfiguration() != null) {
-        c.setMaxElementsOnDisk(16384);
-        c.setOverflowToDisk(false);
-        c.setDiskPersistent(false);
-
-        c.setDiskSpoolBufferSizeMB(5);
-        c.setDiskExpiryThreadIntervalSeconds(60 * 60);
-      }
-      return c;
-    }
-
-    private void configureDefaultCache() {
-      mgr.setDefaultCacheConfiguration(newConfiguration());
-    }
-
-    private CacheConfiguration newCache(final String name) {
-      CacheConfiguration c = newConfiguration();
-      c.setName(name);
-      return c;
-    }
-  }
-}
diff --git a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/PopulatingCache.java b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/PopulatingCache.java
deleted file mode 100644
index f5c6c45..0000000
--- a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/PopulatingCache.java
+++ /dev/null
@@ -1,114 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.ehcache;
-
-import com.google.gerrit.server.cache.Cache;
-import com.google.gerrit.server.cache.EntryCreator;
-
-import net.sf.ehcache.CacheException;
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.Element;
-import net.sf.ehcache.constructs.blocking.CacheEntryFactory;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * A decorator for {@link Cache} which automatically constructs missing entries.
- * <p>
- * On a cache miss {@link EntryCreator#createEntry(Object)} is invoked, allowing
- * the application specific subclass to compute the entry and return it for
- * caching. During a miss the cache takes a lock related to the missing key,
- * ensuring that at most one thread performs the creation work, and other
- * threads wait for the result. Concurrent creations are possible if two
- * different keys miss and hash to different locks in the internal lock table.
- *
- * @param <K> type of key used to name cache entries.
- * @param <V> type of value stored within a cache entry.
- */
-class PopulatingCache<K, V> implements Cache<K, V> {
-  private static final Logger log =
-      LoggerFactory.getLogger(PopulatingCache.class);
-
-  private final net.sf.ehcache.constructs.blocking.SelfPopulatingCache self;
-  private final EntryCreator<K, V> creator;
-
-  PopulatingCache(Ehcache s, EntryCreator<K, V> entryCreator) {
-    creator = entryCreator;
-    final CacheEntryFactory f = new CacheEntryFactory() {
-      @SuppressWarnings("unchecked")
-      @Override
-      public Object createEntry(Object key) throws Exception {
-        return creator.createEntry((K) key);
-      }
-    };
-    self = new net.sf.ehcache.constructs.blocking.SelfPopulatingCache(s, f);
-  }
-
-  /**
-   * Get the element from the cache, or {@link EntryCreator#missing(Object)} if not found.
-   * <p>
-   * The {@link EntryCreator#missing(Object)} method is only invoked if:
-   * <ul>
-   * <li>{@code key == null}, in which case the application should return a
-   * suitable return value that callers can accept, or throw a RuntimeException.
-   * <li>{@code createEntry(key)} threw an exception, in which case the entry
-   * was not stored in the cache. An entry was recorded in the application log,
-   * but a return value is still required.
-   * <li>The cache has been shutdown, and access is forbidden.
-   * </ul>
-   *
-   * @param key key to locate.
-   * @return either the cached entry, or {@code missing(key)} if not found.
-   */
-  @SuppressWarnings("unchecked")
-  public V get(final K key) {
-    if (key == null) {
-      return creator.missing(key);
-    }
-
-    final Element m;
-    try {
-      m = self.get(key);
-    } catch (IllegalStateException err) {
-      log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
-      return creator.missing(key);
-    } catch (CacheException err) {
-      log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
-      return creator.missing(key);
-    }
-    return m != null ? (V) m.getObjectValue() : creator.missing(key);
-  }
-
-  public void remove(final K key) {
-    if (key != null) {
-      self.remove(key);
-    }
-  }
-
-  /** Remove all cached items, forcing them to be created again on demand. */
-  public void removeAll() {
-    self.removeAll();
-  }
-
-  public void put(K key, V value) {
-    self.put(new Element(key, value));
-  }
-
-  @Override
-  public String toString() {
-    return "Cache[" + self.getName() + "]";
-  }
-}
diff --git a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SimpleCache.java b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SimpleCache.java
deleted file mode 100644
index e4428e3..0000000
--- a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SimpleCache.java
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.ehcache;
-
-import com.google.gerrit.server.cache.Cache;
-
-import net.sf.ehcache.CacheException;
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.Element;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * A fast in-memory and/or on-disk based cache.
- *
- * @type <K> type of key used to lookup entries in the cache.
- * @type <V> type of value stored within each cache entry.
- */
-final class SimpleCache<K, V> implements Cache<K, V> {
-  private static final Logger log = LoggerFactory.getLogger(SimpleCache.class);
-
-  private final Ehcache self;
-
-  SimpleCache(final Ehcache self) {
-    this.self = self;
-  }
-
-  Ehcache getEhcache() {
-    return self;
-  }
-
-  @SuppressWarnings("unchecked")
-  public V get(final K key) {
-    if (key == null) {
-      return null;
-    }
-    final Element m;
-    try {
-      m = self.get(key);
-    } catch (IllegalStateException err) {
-      log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
-      return null;
-    } catch (CacheException err) {
-      log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
-      return null;
-    }
-    return m != null ? (V) m.getObjectValue() : null;
-  }
-
-  public void put(final K key, final V value) {
-    self.put(new Element(key, value));
-  }
-
-  public void remove(final K key) {
-    if (key != null) {
-      self.remove(key);
-    }
-  }
-
-  public void removeAll() {
-    self.removeAll();
-  }
-
-  @Override
-  public String toString() {
-    return "Cache[" + self.getName() + "]";
-  }
-}
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
index 0209f3f..ff672d5 100644
--- a/gerrit-extension-api/pom.xml
+++ b/gerrit-extension-api/pom.xml
@@ -56,6 +56,8 @@
         <artifactId>maven-shade-plugin</artifactId>
         <configuration>
           <createSourcesJar>true</createSourcesJar>
+          <shadedArtifactAttached>true</shadedArtifactAttached>
+          <shadedClassifierName>all</shadedClassifierName>
         </configuration>
         <executions>
           <execution>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.java
new file mode 100644
index 0000000..bf2b09a
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.annotations;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Local path where a plugin can store its own private data.
+ * <p>
+ * A plugin or extension may receive this string by Guice injection to discover
+ * a directory where it can store configuration or other data that is private:
+ *
+ * <pre>
+ * @Inject
+ * MyType(@PluginData java.io.File myDir) {
+ *   new FileInputStream(new File(myDir, &quot;my.config&quot;));
+ * }
+ * </pre>
+ */
+@Target({ElementType.PARAMETER, ElementType.FIELD})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface PluginData {
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/RequiresCapability.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
similarity index 85%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/RequiresCapability.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
index cc41a79..382f4ea 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/RequiresCapability.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.sshd;
+package com.google.gerrit.extensions.annotations;
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
@@ -21,7 +21,8 @@
 import java.lang.annotation.Target;
 
 /**
- * Annotation on {@link SshCommand} declaring a capability must be granted.
+ * Annotation on {@link SshCommand} or {@link RestApiServlet} declaring a
+ * capability must be granted.
  */
 @Target({ElementType.TYPE})
 @Retention(RUNTIME)
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
new file mode 100644
index 0000000..438500d
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+import java.util.List;
+
+/** Notified when one or more references are modified. */
+@ExtensionPoint
+public interface GitReferenceUpdatedListener {
+  public interface Update {
+    String getRefName();
+  }
+
+  public interface Event {
+    String getProjectName();
+    List<Update> getUpdates();
+  }
+
+  void onGitReferenceUpdated(Event event);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/LifecycleListener.java
similarity index 87%
rename from gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleListener.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/LifecycleListener.java
index e6b06ef..93da347 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/LifecycleListener.java
@@ -12,11 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.lifecycle;
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
 
 import java.util.EventListener;
 
 /** Listener interested in server startup and shutdown events. */
+@ExtensionPoint
 public interface LifecycleListener extends EventListener {
   /** Invoke when the server is starting. */
   public void start();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/NewProjectCreatedListener.java
similarity index 62%
copy from gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/NewProjectCreatedListener.java
index 204d777..7eed7d4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/NewProjectCreatedListener.java
@@ -12,12 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.extensions.events;
 
-public class IncompleteUserInfoException extends Exception {
-  private static final long serialVersionUID = 1L;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
 
-  public IncompleteUserInfoException(final String userName, final String missingInfo) {
-    super("For the user \"" + userName + "\" " + missingInfo + " is not set.");
+
+/** Notified whenever a project is created on the master. */
+@ExtensionPoint
+public interface NewProjectCreatedListener {
+  public interface Event {
+    String getProjectName();
+    String getHeadName();
   }
+
+  void onNewProjectCreated(Event event);
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
index 8cac117..40bbb80 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
@@ -16,6 +16,7 @@
 
 import com.google.inject.Binder;
 import com.google.inject.Key;
+import com.google.inject.Provider;
 import com.google.inject.Scopes;
 import com.google.inject.TypeLiteral;
 import com.google.inject.util.Types;
@@ -34,9 +35,9 @@
  * <p>
  * Maps index their members by plugin name and export name.
  * <p>
- * DynamicMaps are always mapped as singletons in Guice, and only may contain
- * singletons, as providers are resolved to an instance before the member is
- * added to the map.
+ * DynamicMaps are always mapped as singletons in Guice. Maps store Providers
+ * internally, and resolve the provider to an instance on demand. This enables
+ * registrations to decide between singleton and non-singleton members.
  */
 public abstract class DynamicMap<T> {
   /**
@@ -82,10 +83,13 @@
         .in(Scopes.SINGLETON);
   }
 
-  final ConcurrentMap<NamePair, T> items;
+  final ConcurrentMap<NamePair, Provider<T>> items;
 
   DynamicMap() {
-    items = new ConcurrentHashMap<NamePair, T>(16, 0.75f, 1);
+    items = new ConcurrentHashMap<NamePair, Provider<T>>(
+        16 /* initial size */,
+        0.75f /* load factor */,
+        1 /* concurrency level of 1, load/unload is single threaded */);
   }
 
   /**
@@ -95,9 +99,12 @@
    * @param exportName name the plugin exports the item as.
    * @return the implementation. Null if the plugin is not running, or if the
    *         plugin does not export this name.
+   * @throws ProvisionException if the registered provider is unable to obtain
+   *         an instance of the requested implementation.
    */
   public T get(String pluginName, String exportName) {
-    return items.get(new NamePair(pluginName, exportName));
+    Provider<T> p = items.get(new NamePair(pluginName, exportName));
+    return p != null ? p.get() : null;
   }
 
   /**
@@ -119,9 +126,9 @@
    * @param pluginName name of the plugin.
    * @return items exported by a plugin, keyed by the export name.
    */
-  public SortedMap<String, T> byPlugin(String pluginName) {
-    SortedMap<String, T> r = new TreeMap<String, T>();
-    for (Map.Entry<NamePair, T> e : items.entrySet()) {
+  public SortedMap<String, Provider<T>> byPlugin(String pluginName) {
+    SortedMap<String, Provider<T>> r = new TreeMap<String, Provider<T>>();
+    for (Map.Entry<NamePair, Provider<T>> e : items.entrySet()) {
       if (e.getKey().pluginName.equals(pluginName)) {
         r.put(e.getKey().exportName, e.getValue());
       }
@@ -147,7 +154,8 @@
     public boolean equals(Object other) {
       if (other instanceof NamePair) {
         NamePair np = (NamePair) other;
-        return pluginName.equals(np.pluginName) && exportName.equals(np.exportName);
+        return pluginName.equals(np.pluginName)
+            && exportName.equals(np.exportName);
       }
       return false;
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java
index d771d13..c6e4701 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java
@@ -38,7 +38,9 @@
     List<Binding<T>> bindings = injector.findBindingsByType(type);
     if (bindings != null) {
       for (Binding<T> b : bindings) {
-        m.put("gerrit", b.getKey(), b.getProvider().get());
+        if (b.getKey().getAnnotation() != null) {
+          m.put("gerrit", b.getKey(), b.getProvider());
+        }
       }
     }
     return m;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
index 7f46ad4..ec34887 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -16,11 +16,13 @@
 
 import com.google.inject.Binder;
 import com.google.inject.Key;
+import com.google.inject.Provider;
 import com.google.inject.Scopes;
 import com.google.inject.TypeLiteral;
 import com.google.inject.binder.LinkedBindingBuilder;
 import com.google.inject.internal.UniqueAnnotations;
 import com.google.inject.name.Named;
+import com.google.inject.util.Providers;
 import com.google.inject.util.Types;
 
 import java.util.Collection;
@@ -32,9 +34,9 @@
 /**
  * A set of members that can be modified as plugins reload.
  * <p>
- * DynamicSets are always mapped as singletons in Guice, and only may contain
- * singletons, as providers are resolved to an instance before the member is
- * added to the set.
+ * DynamicSets are always mapped as singletons in Guice. Sets store Providers
+ * internally, and resolve the provider to an instance on demand. This enables
+ * registrations to decide between singleton and non-singleton members.
  */
 public class DynamicSet<T> implements Iterable<T> {
   /**
@@ -125,22 +127,29 @@
     return binder.bind(type).annotatedWith(name);
   }
 
-  private final CopyOnWriteArrayList<AtomicReference<T>> items;
+  private final CopyOnWriteArrayList<AtomicReference<Provider<T>>> items;
 
-  DynamicSet(Collection<AtomicReference<T>> base) {
-    items = new CopyOnWriteArrayList<AtomicReference<T>>(base);
+  DynamicSet(Collection<AtomicReference<Provider<T>>> base) {
+    items = new CopyOnWriteArrayList<AtomicReference<Provider<T>>>(base);
   }
 
   @Override
   public Iterator<T> iterator() {
-    final Iterator<AtomicReference<T>> itr = items.iterator();
+    final Iterator<AtomicReference<Provider<T>>> itr = items.iterator();
     return new Iterator<T>() {
       private T next;
 
       @Override
       public boolean hasNext() {
         while (next == null && itr.hasNext()) {
-          next = itr.next().get();
+          Provider<T> p = itr.next().get();
+          if (p != null) {
+            try {
+              next = p.get();
+            } catch (RuntimeException e) {
+              // TODO Log failed member of DynamicSet.
+            }
+          }
         }
         return next != null;
       }
@@ -169,7 +178,18 @@
    * @return handle to remove the item at a later point in time.
    */
   public RegistrationHandle add(final T item) {
-    final AtomicReference<T> ref = new AtomicReference<T>(item);
+    return add(Providers.of(item));
+  }
+
+  /**
+   * Add one new element to the set.
+   *
+   * @param item the item to add to the collection. Must not be null.
+   * @return handle to remove the item at a later point in time.
+   */
+  public RegistrationHandle add(final Provider<T> item) {
+    final AtomicReference<Provider<T>> ref =
+        new AtomicReference<Provider<T>>(item);
     items.add(ref);
     return new RegistrationHandle() {
       @Override
@@ -191,18 +211,20 @@
    * @return a handle that can remove this item later, or hot-swap the item
    *         without it ever leaving the collection.
    */
-  public ReloadableRegistrationHandle<T> add(Key<T> key, T item) {
-    AtomicReference<T> ref = new AtomicReference<T>(item);
+  public ReloadableRegistrationHandle<T> add(Key<T> key, Provider<T> item) {
+    AtomicReference<Provider<T>> ref = new AtomicReference<Provider<T>>(item);
     items.add(ref);
     return new ReloadableHandle(ref, key, item);
   }
 
   private class ReloadableHandle implements ReloadableRegistrationHandle<T> {
-    private final AtomicReference<T> ref;
+    private final AtomicReference<Provider<T>> ref;
     private final Key<T> key;
-    private final T item;
+    private final Provider<T> item;
 
-    ReloadableHandle(AtomicReference<T> ref, Key<T> key, T item) {
+    ReloadableHandle(AtomicReference<Provider<T>> ref,
+        Key<T> key,
+        Provider<T> item) {
       this.ref = ref;
       this.key = key;
       this.item = item;
@@ -221,7 +243,7 @@
     }
 
     @Override
-    public ReloadableHandle replace(Key<T> newKey, T newItem) {
+    public ReloadableHandle replace(Key<T> newKey, Provider<T> newItem) {
       if (ref.compareAndSet(item, newItem)) {
         return new ReloadableHandle(ref, newKey, newItem);
       }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
index 694fbd8..6c21553 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
@@ -19,6 +19,7 @@
 import com.google.inject.Injector;
 import com.google.inject.Provider;
 import com.google.inject.TypeLiteral;
+import com.google.inject.internal.UniqueAnnotations;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -26,6 +27,8 @@
 import java.util.concurrent.atomic.AtomicReference;
 
 class DynamicSetProvider<T> implements Provider<DynamicSet<T>> {
+  private static final Class<?> UNIQUE_ANNOTATION =
+      UniqueAnnotations.create().getClass();
   private final TypeLiteral<T> type;
 
   @Inject
@@ -39,7 +42,7 @@
     return new DynamicSet<T>(find(injector, type));
   }
 
-  private static <T> List<AtomicReference<T>> find(
+  private static <T> List<AtomicReference<Provider<T>>> find(
       Injector src,
       TypeLiteral<T> type) {
     List<Binding<T>> bindings = src.findBindingsByType(type);
@@ -47,10 +50,16 @@
     if (cnt == 0) {
       return Collections.emptyList();
     }
-    List<AtomicReference<T>> r = new ArrayList<AtomicReference<T>>(cnt);
+    List<AtomicReference<Provider<T>>> r = newList(cnt);
     for (Binding<T> b : bindings) {
-      r.add(new AtomicReference<T>(b.getProvider().get()));
+      if (b.getKey().getAnnotation() != null) {
+        r.add(new AtomicReference<Provider<T>>(b.getProvider()));
+      }
     }
     return r;
   }
+
+  private static <T> List<AtomicReference<Provider<T>>> newList(int cnt) {
+    return new ArrayList<AtomicReference<Provider<T>>>(cnt);
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
index 0ce4014..3558794 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.annotations.Export;
 import com.google.inject.Key;
+import com.google.inject.Provider;
 
 /** <b>DO NOT USE</b> */
 public class PrivateInternals_DynamicMapImpl<T> extends DynamicMap<T> {
@@ -32,7 +33,7 @@
    */
   public RegistrationHandle put(
       String pluginName, String exportName,
-      final T item) {
+      final Provider<T> item) {
     final NamePair key = new NamePair(pluginName, exportName);
     items.put(key, item);
     return new RegistrationHandle() {
@@ -57,7 +58,7 @@
    */
   public ReloadableRegistrationHandle<T> put(
       String pluginName, Key<T> key,
-      T item) {
+      Provider<T> item) {
     String exportName = ((Export) key.getAnnotation()).value();
     NamePair np = new NamePair(pluginName, exportName);
     items.put(np, item);
@@ -67,9 +68,9 @@
   private class ReloadableHandle implements ReloadableRegistrationHandle<T> {
     private final NamePair np;
     private final Key<T> key;
-    private final T item;
+    private final Provider<T> item;
 
-    ReloadableHandle(NamePair np, Key<T> key, T item) {
+    ReloadableHandle(NamePair np, Key<T> key, Provider<T> item) {
       this.np = np;
       this.key = key;
       this.item = item;
@@ -86,7 +87,7 @@
     }
 
     @Override
-    public ReloadableHandle replace(Key<T> newKey, T newItem) {
+    public ReloadableHandle replace(Key<T> newKey, Provider<T> newItem) {
       if (items.replace(np, item, newItem)) {
         return new ReloadableHandle(np, newKey, newItem);
       }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
new file mode 100644
index 0000000..66dd45d
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
@@ -0,0 +1,175 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.registration;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.inject.Binding;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.TypeLiteral;
+
+import java.lang.reflect.ParameterizedType;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** <b>DO NOT USE</b> */
+public class PrivateInternals_DynamicTypes {
+  public static Map<TypeLiteral<?>, DynamicSet<?>> dynamicSetsOf(Injector src) {
+    Map<TypeLiteral<?>, DynamicSet<?>> m = newHashMap();
+    for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+      TypeLiteral<?> type = e.getKey().getTypeLiteral();
+      if (type.getRawType() == DynamicSet.class) {
+        ParameterizedType p = (ParameterizedType) type.getType();
+        m.put(TypeLiteral.get(p.getActualTypeArguments()[0]),
+            (DynamicSet<?>) e.getValue().getProvider().get());
+      }
+    }
+    if (m.isEmpty()) {
+      return Collections.emptyMap();
+    }
+    return Collections.unmodifiableMap(m);
+  }
+
+  public static Map<TypeLiteral<?>, DynamicMap<?>> dynamicMapsOf(Injector src) {
+    Map<TypeLiteral<?>, DynamicMap<?>> m = newHashMap();
+    for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+      TypeLiteral<?> type = e.getKey().getTypeLiteral();
+      if (type.getRawType() == DynamicMap.class) {
+        ParameterizedType p = (ParameterizedType) type.getType();
+        m.put(TypeLiteral.get(p.getActualTypeArguments()[0]),
+            (DynamicMap<?>) e.getValue().getProvider().get());
+      }
+    }
+    if (m.isEmpty()) {
+      return Collections.emptyMap();
+    }
+    return Collections.unmodifiableMap(m);
+  }
+
+  public static List<RegistrationHandle> attachSets(
+      Injector src,
+      Map<TypeLiteral<?>, DynamicSet<?>> sets) {
+    if (src == null || sets == null || sets.isEmpty()) {
+      return Collections.emptyList();
+    }
+
+    List<RegistrationHandle> handles = new ArrayList<RegistrationHandle>(4);
+    try {
+      for (Map.Entry<TypeLiteral<?>, DynamicSet<?>> e : sets.entrySet()) {
+        @SuppressWarnings("unchecked")
+        TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+        @SuppressWarnings("unchecked")
+        DynamicSet<Object> set = (DynamicSet<Object>) e.getValue();
+
+        for (Binding<Object> b : bindings(src, type)) {
+          if (b.getKey().getAnnotation() != null) {
+            handles.add(set.add(b.getKey(), b.getProvider()));
+          }
+        }
+      }
+    } catch (RuntimeException e) {
+      remove(handles);
+      throw e;
+    } catch (Error e) {
+      remove(handles);
+      throw e;
+    }
+    return handles;
+  }
+
+  public static List<RegistrationHandle> attachMaps(
+      Injector src,
+      String groupName,
+      Map<TypeLiteral<?>, DynamicMap<?>> maps) {
+    if (src == null || maps == null || maps.isEmpty()) {
+      return Collections.emptyList();
+    }
+
+    List<RegistrationHandle> handles = new ArrayList<RegistrationHandle>(4);
+    try {
+      for (Map.Entry<TypeLiteral<?>, DynamicMap<?>> e : maps.entrySet()) {
+        @SuppressWarnings("unchecked")
+        TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+        @SuppressWarnings("unchecked")
+        PrivateInternals_DynamicMapImpl<Object> set =
+            (PrivateInternals_DynamicMapImpl<Object>) e.getValue();
+
+        for (Binding<Object> b : bindings(src, type)) {
+          if (b.getKey().getAnnotation() != null) {
+            handles.add(set.put(groupName, b.getKey(), b.getProvider()));
+          }
+        }
+      }
+    } catch (RuntimeException e) {
+      remove(handles);
+      throw e;
+    } catch (Error e) {
+      remove(handles);
+      throw e;
+    }
+    return handles;
+  }
+
+  public static LifecycleListener registerInParentInjectors() {
+    return new LifecycleListener() {
+      private List<RegistrationHandle> handles;
+
+      @Inject
+      private Injector self;
+
+      @Override
+      public void start() {
+        handles = new ArrayList<RegistrationHandle>(4);
+        Injector parent = self.getParent();
+        while (parent != null) {
+          handles.addAll(attachSets(self, dynamicSetsOf(parent)));
+          handles.addAll(attachMaps(self, "gerrit", dynamicMapsOf(parent)));
+          parent = parent.getParent();
+        }
+        if (handles.isEmpty()) {
+          handles = null;
+        }
+      }
+
+      @Override
+      public void stop() {
+        remove(handles);
+        handles = null;
+      }
+    };
+  }
+
+  private static void remove(List<RegistrationHandle> handles) {
+    if (handles != null) {
+      for (RegistrationHandle handle : handles) {
+        handle.remove();
+      }
+    }
+  }
+
+  private static <K,V> Map<K, V> newHashMap() {
+    return new HashMap<K,V>();
+  }
+
+  private static <T> List<Binding<T>> bindings(Injector src, TypeLiteral<T> type) {
+    return src.findBindingsByType(type);
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java
index b7d78c9..7284296 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java
@@ -15,9 +15,10 @@
 package com.google.gerrit.extensions.registration;
 
 import com.google.inject.Key;
+import com.google.inject.Provider;
 
 public interface ReloadableRegistrationHandle<T> extends RegistrationHandle {
   public Key<T> getKey();
 
-  public RegistrationHandle replace(Key<T> key, T item);
+  public RegistrationHandle replace(Key<T> key, Provider<T> item);
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java
new file mode 100644
index 0000000..3d2df21
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.systemstatus;
+
+/** Exports current server information to an extension. */
+public interface ServerInformation {
+  /** Current state of the server. */
+  public enum State {
+    /**
+     * The server is starting up, and network connections are not yet being
+     * accepted. Plugins or extensions starting during this time are starting
+     * for the first time in this process.
+     */
+    STARTUP,
+
+    /**
+     * The server is running and handling requests. Plugins starting during this
+     * state may be reloading, or being installed into a running system.
+     */
+    RUNNING,
+
+    /**
+     * The server is attempting a graceful halt of operations and will exit (or
+     * be killed by the operating system) soon.
+     */
+    SHUTDOWN;
+  }
+
+  State getState();
+}
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 40ffc7d..2e33fd8 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
@@ -17,6 +17,7 @@
 import static com.google.gerrit.common.PageLinks.ADMIN_CREATE_PROJECT;
 import static com.google.gerrit.common.PageLinks.ADMIN_GROUPS;
 import static com.google.gerrit.common.PageLinks.ADMIN_PROJECTS;
+import static com.google.gerrit.common.PageLinks.ADMIN_PLUGINS;
 import static com.google.gerrit.common.PageLinks.MINE;
 import static com.google.gerrit.common.PageLinks.REGISTER;
 import static com.google.gerrit.common.PageLinks.SETTINGS;
@@ -48,6 +49,7 @@
 import com.google.gerrit.client.admin.AccountGroupScreen;
 import com.google.gerrit.client.admin.CreateProjectScreen;
 import com.google.gerrit.client.admin.GroupListScreen;
+import com.google.gerrit.client.admin.PluginListScreen;
 import com.google.gerrit.client.admin.ProjectAccessScreen;
 import com.google.gerrit.client.admin.ProjectBranchesScreen;
 import com.google.gerrit.client.admin.ProjectInfoScreen;
@@ -621,6 +623,10 @@
         } else if (matchPrefix("/admin/projects/", token)) {
           Gerrit.display(token, selectProject());
 
+        } else if (matchPrefix(ADMIN_PLUGINS, token)
+            || matchExact("/admin/plugins", token)) {
+          Gerrit.display(token, new PluginListScreen());
+
         } else if (matchExact(ADMIN_CREATE_PROJECT, token)
             || matchExact("/admin/create-project", token)) {
           Gerrit.display(token, new CreateProjectScreen());
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 fd6ba2d..5c2a3190 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
@@ -266,7 +266,7 @@
     }
   }
 
-  private static String loginRedirect(String token) {
+  public static String loginRedirect(String token) {
     if (token == null) {
       token = "";
     } else if (token.startsWith("/")) {
@@ -323,6 +323,7 @@
     Cookies.removeCookie("GerritAccount");
   }
 
+  @Override
   public void onModuleLoad() {
     UserAgent.assertNotInIFrame();
 
@@ -332,6 +333,7 @@
         e = URL.encodeQueryString(e);
         e = fixPathImpl(e);
         e = fixColonImpl(e);
+        e = fixDoubleQuote(e);
         return e;
       }
 
@@ -345,6 +347,9 @@
 
       private native String fixColonImpl(String path)
       /*-{ return path.replace(/%3A/g, ":"); }-*/;
+
+      private native String fixDoubleQuote(String path)
+      /*-{ return path.replace(/%22/g, '"'); }-*/;
     });
 
     initHostname();
@@ -572,6 +577,7 @@
       m = new LinkMenuBar();
       addLink(m, C.menuGroups(), PageLinks.ADMIN_GROUPS);
       addLink(m, C.menuProjects(), PageLinks.ADMIN_PROJECTS);
+      addLink(m, C.menuPlugins(), PageLinks.ADMIN_PLUGINS);
       menuLeft.add(m, C.menuAdmin());
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
index f716814..43afe1e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
@@ -72,6 +72,7 @@
   String menuPeople();
   String menuGroups();
   String menuProjects();
+  String menuPlugins();
 
   String menuDocumentation();
   String menuDocumentationIndex();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
index 8e3ca6c..277c380 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
@@ -55,6 +55,7 @@
 menuPeople = People
 menuGroups = Groups
 menuProjects = Projects
+menuPlugins = Plugins
 
 menuDocumentation = Documentation
 menuDocumentationIndex = Index
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
index 574f58e..490db59 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
@@ -169,6 +169,7 @@
   String patchSetUserIdentity();
   String patchSizeCell();
   String permalink();
+  String pluginsTable();
   String posscore();
   String projectAdminApprovalCategoryRangeLine();
   String projectAdminApprovalCategoryValue();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index b68fbfd..d374d35 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -57,6 +57,8 @@
   String buttonClearPassword();
   String buttonGeneratePassword();
   String linkObtainPassword();
+  String linkEditFullName();
+  String linkReloadContact();
   String invalidUserName();
   String invalidUserEmail();
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index 0c658d6..8b32174 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -38,6 +38,8 @@
 buttonClearPassword = Clear Password
 buttonGeneratePassword = Generate Password
 linkObtainPassword = Obtain Password
+linkEditFullName = Edit
+linkReloadContact = Reload
 invalidUserName = Username must contain only letters, numbers, _, - or .
 invalidUserEmail = Email format is wrong.
 sshKeyInvalid = Invalid Key
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
index 4e0b3b2..4fbe7a0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
@@ -18,17 +18,18 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.OnEditEnabler;
+import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.ContactInformation;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ChangeHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.i18n.client.LocaleInfo;
-import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
@@ -41,6 +42,7 @@
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwtexpui.user.client.AutoCenterDialogBox;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -102,12 +104,37 @@
     }
 
     int row = 0;
-    if (!Gerrit.getConfig().canEdit(FieldName.USER_NAME)) {
+    if (!Gerrit.getConfig().canEdit(FieldName.USER_NAME)
+        && Gerrit.getConfig().siteHasUsernames()) {
       infoPlainText.resizeRows(infoPlainText.getRowCount() + 1);
       row(infoPlainText, row++, Util.C.userName(), new UsernameField());
     }
 
-    row(infoPlainText, row++, Util.C.contactFieldFullName(), nameTxt);
+    if (!canEditFullName()) {
+      FlowPanel nameLine = new FlowPanel();
+      nameLine.add(nameTxt);
+      if (Gerrit.getConfig().getEditFullNameUrl() != null) {
+        Button edit = new Button(Util.C.linkEditFullName());
+        edit.addClickHandler(new ClickHandler() {
+          @Override
+          public void onClick(ClickEvent event) {
+            Window.open(Gerrit.getConfig().getEditFullNameUrl(), "_blank", null);
+          }
+        });
+        nameLine.add(edit);
+      }
+      Button reload = new Button(Util.C.linkReloadContact());
+      reload.addClickHandler(new ClickHandler() {
+        @Override
+        public void onClick(ClickEvent event) {
+          Window.Location.replace(Gerrit.loginRedirect(PageLinks.SETTINGS_CONTACT));
+        }
+      });
+      nameLine.add(reload);
+      row(infoPlainText, row++, Util.C.contactFieldFullName(), nameLine);
+    } else {
+      row(infoPlainText, row++, Util.C.contactFieldFullName(), nameTxt);
+    }
     row(infoPlainText, row++, Util.C.contactFieldEmail(), emailLine);
 
     infoPlainText.getCellFormatter().addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
index 165168d..ae04e0a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
@@ -38,21 +38,24 @@
       fieldIdx = 1;
     }
 
-    info = new Grid(5, 2);
+    info = new Grid((Gerrit.getConfig().siteHasUsernames() ? 1 : 0) + 4, 2);
     info.setStyleName(Gerrit.RESOURCES.css().infoBlock());
     info.addStyleName(Gerrit.RESOURCES.css().accountInfoBlock());
     add(info);
 
-    infoRow(0, Util.C.userName());
-    infoRow(1, Util.C.fullName());
-    infoRow(2, Util.C.preferredEmail());
-    infoRow(3, Util.C.registeredOn());
-    infoRow(4, Util.C.accountId());
+    int row = 0;
+    if (Gerrit.getConfig().siteHasUsernames()) {
+      infoRow(row++, Util.C.userName());
+    }
+    infoRow(row++, Util.C.fullName());
+    infoRow(row++, Util.C.preferredEmail());
+    infoRow(row++, Util.C.registeredOn());
+    infoRow(row++, Util.C.accountId());
 
     final CellFormatter fmt = info.getCellFormatter();
     fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
     fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
-    fmt.addStyleName(4, 0, Gerrit.RESOURCES.css().bottomheader());
+    fmt.addStyleName(row - 1, 0, Gerrit.RESOURCES.css().bottomheader());
   }
 
   @Override
@@ -69,10 +72,13 @@
   }
 
   void display(final Account account) {
-    info.setWidget(0, fieldIdx, new UsernameField());
-    info.setText(1, fieldIdx, account.getFullName());
-    info.setText(2, fieldIdx, account.getPreferredEmail());
-    info.setText(3, fieldIdx, mediumFormat(account.getRegisteredOn()));
-    info.setText(4, fieldIdx, account.getId().toString());
+    int row = 0;
+    if (Gerrit.getConfig().siteHasUsernames()) {
+      info.setWidget(row++, fieldIdx, new UsernameField());
+    }
+    info.setText(row++, fieldIdx, account.getFullName());
+    info.setText(row++, fieldIdx, account.getPreferredEmail());
+    info.setText(row++, fieldIdx, mediumFormat(account.getRegisteredOn()));
+    info.setText(row++, fieldIdx, account.getId().toString());
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
index 1334d87..8d4a767 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
@@ -28,11 +28,8 @@
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.logical.shared.ResizeEvent;
-import com.google.gwt.event.logical.shared.ResizeHandler;
 import com.google.gwt.event.logical.shared.SelectionEvent;
 import com.google.gwt.event.logical.shared.SelectionHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
@@ -49,8 +46,7 @@
 
 import java.util.List;
 
-public class MyWatchedProjectsScreen extends SettingsScreen implements
-    ResizeHandler {
+public class MyWatchedProjectsScreen extends SettingsScreen {
   private Button addNew;
   private HintTextBox nameBox;
   private SuggestBox nameTxt;
@@ -61,16 +57,10 @@
   private Button close;
   private ProjectsTable projectsTab;
   private Button delSel;
-
   private PopupPanel.PositionCallback popupPosition;
-  private HandlerRegistration regWindowResize;
-
-  private int preferredPopupWidth = -1;
-
   private boolean submitOnSelection;
   private boolean firstPopupLoad = true;
   private boolean popingUp;
-
   private ScrollPanel sp;
 
   @Override
@@ -78,9 +68,7 @@
     super.onInitUI();
     createWidgets();
 
-
     /* top table */
-
     final Grid grid = new Grid(2, 2);
     grid.setStyleName(Gerrit.RESOURCES.css().infoBlock());
     grid.setText(0, 0, Util.C.watchedProjectName());
@@ -105,33 +93,27 @@
 
 
     /* bottom table */
-
     add(watchesTab);
     add(delSel);
 
 
     /* popup */
-
     final FlowPanel pfp = new FlowPanel();
     sp = new ScrollPanel(projectsTab);
+    sp.setSize("100%", "100%");
     pfp.add(sp);
     pfp.add(close);
     popup.setWidget(pfp);
-
+    popup.setHeight("100%");
     popupPosition = new PopupPanel.PositionCallback() {
+
       public void setPosition(int offsetWidth, int offsetHeight) {
-        if (preferredPopupWidth == -1) {
-          preferredPopupWidth = offsetWidth;
-        }
-
         int top = grid.getAbsoluteTop() - 50; // under page header
-
         // Try to place it to the right of everything else, but not
         // right justified
-        int left = 5 + Math.max(
-                         grid.getAbsoluteLeft() + grid.getOffsetWidth(),
-                   watchesTab.getAbsoluteLeft() + watchesTab.getOffsetWidth() );
-
+        int left =
+            5 + Math.max(grid.getAbsoluteLeft() + grid.getOffsetWidth(),
+                watchesTab.getAbsoluteLeft() + watchesTab.getOffsetWidth());
         if (top + offsetHeight > Window.getClientHeight()) {
           top = Window.getClientHeight() - offsetHeight;
         }
@@ -153,16 +135,6 @@
     };
   }
 
-  @Override
-  public void onResize(final ResizeEvent event) {
-    sp.setSize("100%","100%");
-
-    // For some reason keeping track of preferredWidth keeps the width better,
-    // but using 100% for height works better.
-    popup.setHeight("100%");
-    popupPosition.setPosition(preferredPopupWidth, popup.getOffsetHeight());
-  }
-
   protected void createWidgets() {
     nameBox = new HintTextBox();
     nameTxt = new SuggestBox(new ProjectNameSuggestOracle(), nameBox);
@@ -293,23 +265,13 @@
       GlobalKey.addApplication(popup, new HidePopupPanelCommand(0,
           KeyCodes.KEY_ESCAPE, popup));
       projectsTab.setRegisterKeys(true);
-
       projectsTab.finishDisplay();
-
-      if (regWindowResize == null) {
-        regWindowResize = Window.addResizeHandler(this);
-      }
-
       popingUp = false;
     }
   }
 
   protected void closePopup() {
     popup.hide();
-    if (regWindowResize != null) {
-      regWindowResize.removeHandler();
-      regWindowResize = null;
-    }
   }
 
   protected void doAddNew() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
index 72ac1a4..93739c2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
@@ -276,7 +276,8 @@
   private class PermissionEditorSource extends EditorSource<PermissionEditor> {
     @Override
     public PermissionEditor create(int index) {
-      PermissionEditor subEditor = new PermissionEditor(readOnly, value);
+      PermissionEditor subEditor =
+          new PermissionEditor(projectAccess.getProjectName(), readOnly, value);
       permissionContainer.insert(subEditor, index);
       return subEditor;
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
index eaf564f..ea7c04b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
@@ -27,17 +27,10 @@
 import com.google.gwt.event.dom.client.ChangeHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwt.user.client.ui.Panel;
 import com.google.gwt.user.client.ui.SuggestBox;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
@@ -45,8 +38,6 @@
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwtjsonrpc.common.VoidResult;
 
-import java.util.List;
-
 public class AccountGroupInfoScreen extends AccountGroupScreen {
   private CopyableLabel groupUUIDLabel;
 
@@ -64,12 +55,6 @@
   private ListBox typeSelect;
   private Button saveType;
 
-  private Panel externalPanel;
-  private Label externalName;
-  private NpTextBox externalNameFilter;
-  private Button externalNameSearch;
-  private Grid externalMatches;
-
   private CheckBox visibleToAllCheckBox;
   private Button saveGroupOptions;
 
@@ -86,8 +71,6 @@
     initDescription();
     initGroupOptions();
     initGroupType();
-
-    initExternal();
   }
 
   private void enableForm(final boolean canModify) {
@@ -95,8 +78,6 @@
     ownerTxtBox.setEnabled(canModify);
     descTxt.setEnabled(canModify);
     typeSelect.setEnabled(canModify);
-    externalNameFilter.setEnabled(canModify);
-    externalNameSearch.setEnabled(canModify);
     visibleToAllCheckBox.setEnabled(canModify);
   }
 
@@ -243,7 +224,6 @@
     typeSelect = new ListBox();
     typeSelect.setStyleName(Gerrit.RESOURCES.css().groupTypeSelectListBox());
     typeSelect.addItem(Util.C.groupType_INTERNAL(), AccountGroup.Type.INTERNAL.name());
-    typeSelect.addItem(Util.C.groupType_LDAP(), AccountGroup.Type.LDAP.name());
     typeSelect.addChangeHandler(new ChangeHandler() {
       @Override
       public void onChange(ChangeEvent event) {
@@ -279,54 +259,12 @@
     add(fp);
   }
 
-  private void initExternal() {
-    externalName = new Label();
-
-    externalNameFilter = new NpTextBox();
-    externalNameFilter.setStyleName(Gerrit.RESOURCES.css()
-        .groupExternalNameFilterTextBox());
-    externalNameFilter.setVisibleLength(30);
-    externalNameFilter.addKeyPressHandler(new KeyPressHandler() {
-      @Override
-      public void onKeyPress(final KeyPressEvent event) {
-        if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-          doExternalSearch();
-        }
-      }
-    });
-
-    externalNameSearch = new Button(Gerrit.C.searchButton());
-    externalNameSearch.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(ClickEvent event) {
-        doExternalSearch();
-      }
-    });
-
-    externalMatches = new Grid();
-    externalMatches.setStyleName(Gerrit.RESOURCES.css().infoTable());
-    externalMatches.setVisible(false);
-
-    final FlowPanel searchLine = new FlowPanel();
-    searchLine.add(externalNameFilter);
-    searchLine.add(externalNameSearch);
-
-    externalPanel = new VerticalPanel();
-    externalPanel.add(new SmallHeading(Util.C.headingExternalGroup()));
-    externalPanel.add(externalName);
-    externalPanel.add(searchLine);
-    externalPanel.add(externalMatches);
-    add(externalPanel);
-  }
-
   private void setType(final AccountGroup.Type newType) {
     final boolean system = newType == AccountGroup.Type.SYSTEM;
 
     typeSystem.setVisible(system);
     typeSelect.setVisible(!system);
     saveType.setVisible(!system);
-    externalPanel.setVisible(newType == AccountGroup.Type.LDAP);
-    externalNameFilter.setText(groupNameTxt.getText());
 
     if (!system) {
       for (int i = 0; i < typeSelect.getItemCount(); i++) {
@@ -367,77 +305,6 @@
         });
   }
 
-  private void doExternalSearch() {
-    externalNameFilter.setEnabled(false);
-    externalNameSearch.setEnabled(false);
-    Util.GROUP_SVC.searchExternalGroups(externalNameFilter.getText(),
-        new GerritCallback<List<AccountGroup.ExternalNameKey>>() {
-          @Override
-          public void onSuccess(List<AccountGroup.ExternalNameKey> result) {
-            try {
-              final CellFormatter fmt = externalMatches.getCellFormatter();
-
-              if (result.isEmpty()) {
-                externalMatches.resize(1, 1);
-                externalMatches.setText(0, 0, Util.C.errorNoMatchingGroups());
-                fmt.setStyleName(0, 0, Gerrit.RESOURCES.css().header());
-                return;
-              }
-
-              externalMatches.resize(1 + result.size(), 2);
-
-              externalMatches.setText(0, 0, Util.C.columnGroupName());
-              externalMatches.setText(0, 1, "");
-              fmt.setStyleName(0, 0, Gerrit.RESOURCES.css().header());
-              fmt.setStyleName(0, 1, Gerrit.RESOURCES.css().header());
-
-              for (int row = 0; row < result.size(); row++) {
-                final AccountGroup.ExternalNameKey key = result.get(row);
-                final Button b = new Button(Util.C.buttonSelectGroup());
-                b.addClickHandler(new ClickHandler() {
-                  @Override
-                  public void onClick(ClickEvent event) {
-                    setExternalGroup(key);
-                  }
-                });
-                externalMatches.setText(1 + row, 0, key.get());
-                externalMatches.setWidget(1 + row, 1, b);
-                fmt.setStyleName(1 + row, 1, Gerrit.RESOURCES.css().rightmost());
-              }
-            } finally {
-              externalMatches.setVisible(true);
-              externalNameFilter.setEnabled(true);
-              externalNameSearch.setEnabled(true);
-            }
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            externalNameFilter.setEnabled(true);
-            externalNameSearch.setEnabled(true);
-            super.onFailure(caught);
-          }
-        });
-  }
-
-  private void setExternalGroup(final AccountGroup.ExternalNameKey key) {
-    externalMatches.setVisible(false);
-
-    Util.GROUP_SVC.changeExternalGroup(getGroupId(), key,
-        new GerritCallback<VoidResult>() {
-          @Override
-          public void onSuccess(VoidResult result) {
-            externalName.setText(key.get());
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            externalMatches.setVisible(true);
-            super.onFailure(caught);
-          }
-        });
-  }
-
   @Override
   protected void display(final GroupDetail groupDetail) {
     final AccountGroup group = groupDetail.group;
@@ -446,19 +313,12 @@
     if (groupDetail.ownerGroup != null) {
       ownerTxt.setText(groupDetail.ownerGroup.getName());
     } else {
-      ownerTxt.setText(Util.M.deletedGroup(group.getOwnerGroupId().get()));
+      ownerTxt.setText(Util.M.deletedReference(group.getOwnerGroupUUID().get()));
     }
     descTxt.setText(group.getDescription());
 
     visibleToAllCheckBox.setValue(group.isVisibleToAll());
 
-    switch (group.getType()) {
-      case LDAP:
-        externalName.setText(group.getExternalNameKey() != null ? group
-            .getExternalNameKey().get() : Util.C.noGroupSelected());
-        break;
-    }
-
     setType(group.getType());
 
     enableForm(groupDetail.canModify);
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 d049ff6..49bb5dc3 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
@@ -104,6 +104,12 @@
   String projectAdminTabBranches();
   String projectAdminTabAccess();
 
+  String plugins();
+  String pluginTabInstalled();
+
+  String columnPluginName();
+  String columnPluginVersion();
+
   String noGroupSelected();
   String errorNoMatchingGroups();
   String errorNoGitRepository();
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 4330513..89fc195 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
@@ -84,6 +84,11 @@
 projectAdminTabBranches = Branches
 projectAdminTabAccess = Access
 
+plugins = Plugins
+pluginTabInstalled = Installed
+columnPluginName = Plugin Name
+columnPluginVersion = Version
+
 noGroupSelected = (No group selected)
 errorNoMatchingGroups = No Matching Groups
 errorNoGitRepository = No Git Repository
@@ -93,6 +98,7 @@
 
 # Permission Names
 permissionNames = \
+	abandon, \
 	create, \
 	forgeAuthor, \
 	forgeCommitter, \
@@ -104,6 +110,7 @@
 	read, \
 	rebase, \
 	submit
+abandon = Abandon
 create = Create Reference
 forgeAuthor = Forge Author Identity
 forgeCommitter = Forge Committer Identity
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
index 9da9c22..4ddc9a8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
 import com.google.gerrit.client.ui.RPCSuggestOracle;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.editor.client.LeafValueEditor;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
@@ -140,4 +141,8 @@
   public void setAccessKey(char key) {
     suggestBox.setAccessKey(key);
   }
+
+  public void setProject(Project.NameKey projectName) {
+    oracle.setProject(projectName);
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
index d10afd1..2c43233 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
@@ -98,20 +99,25 @@
   @UiField
   DivElement deleted;
 
+  private final Project.NameKey projectName;
   private final boolean readOnly;
   private final AccessSection section;
   private Permission value;
   private PermissionRange.WithDefaults validRange;
   private boolean isDeleted;
 
-  public PermissionEditor(boolean readOnly, AccessSection section) {
+  public PermissionEditor(Project.NameKey projectName,
+      boolean readOnly,
+      AccessSection section) {
     this.readOnly = readOnly;
     this.section = section;
+    this.projectName = projectName;
 
     normalName = new ValueLabel<String>(PermissionNameRenderer.INSTANCE);
     deletedName = new ValueLabel<String>(PermissionNameRenderer.INSTANCE);
 
     initWidget(uiBinder.createAndBindUi(this));
+    groupToAdd.setProject(projectName);
     rules = ListEditor.of(new RuleEditorSource());
 
     exclusiveGroup.setEnabled(!readOnly);
@@ -223,7 +229,8 @@
       // If the oracle didn't get to complete a UUID, resolve it now.
       //
       addRule.setEnabled(false);
-      SuggestUtil.SVC.suggestAccountGroup(ref.getName(), 1,
+      SuggestUtil.SVC.suggestAccountGroupForProject(
+          projectName, ref.getName(), 1,
           new GerritCallback<List<GroupReference>>() {
             @Override
             public void onSuccess(List<GroupReference> result) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
index e4cced7..5dd8b3c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.DivElement;
 import com.google.gwt.dom.client.SpanElement;
@@ -178,16 +179,21 @@
   @Override
   public void setValue(PermissionRule value) {
     GroupReference ref = value.getGroup();
-    if (ref.getUUID() != null) {
+
+    boolean link;
+    if (ref.getUUID() != null && AccountGroup.isInternalGroup(ref.getUUID())) {
+      groupNameLink.setText(ref.getName());
       groupNameLink.setTargetHistoryToken(Dispatcher.toGroup(ref.getUUID()));
+      link = true;
+    } else {
+      groupNameSpan.setInnerText(ref.getName());
+      groupNameSpan.setTitle(ref.getUUID() != null ? ref.getUUID().get() : "");
+      link = false;
     }
 
-    groupNameLink.setText(ref.getName());
-    groupNameSpan.setInnerText(ref.getName());
     deletedGroupName.setInnerText(ref.getName());
-
-    groupNameLink.setVisible(ref.getUUID() != null);
-    UIObject.setVisible(groupNameSpan, ref.getUUID() == null);
+    groupNameLink.setVisible(link);
+    UIObject.setVisible(groupNameSpan, !link);
   }
 
   @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java
new file mode 100644
index 0000000..7019a06
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java
@@ -0,0 +1,92 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.admin;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.plugins.PluginInfo;
+import com.google.gerrit.client.plugins.PluginMap;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.FancyFlexTable;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Panel;
+
+public class PluginListScreen extends PluginScreen {
+
+  private Panel pluginPanel;
+  private PluginTable pluginTable;
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    initPluginList();
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    PluginMap.all(new ScreenLoadCallback<PluginMap>(this) {
+      @Override
+      protected void preDisplay(final PluginMap result) {
+        pluginTable.display(result);
+      }
+    });
+  }
+
+  private void initPluginList() {
+    pluginTable = new PluginTable();
+    pluginTable.addStyleName(Gerrit.RESOURCES.css().pluginsTable());
+
+    pluginPanel = new FlowPanel();
+    pluginPanel.setWidth("500px");
+    pluginPanel.add(pluginTable);
+    add(pluginPanel);
+  }
+
+  private class PluginTable extends FancyFlexTable<PluginInfo> {
+    PluginTable() {
+      table.setText(0, 1, Util.C.columnPluginName());
+      table.setText(0, 2, Util.C.columnPluginVersion());
+
+      final FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
+      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
+    }
+
+    void display(final PluginMap plugins) {
+      while (1 < table.getRowCount()) {
+        table.removeRow(table.getRowCount() - 1);
+      }
+
+      for (final PluginInfo p : plugins.values().asList()) {
+        final int row = table.getRowCount();
+        table.insertRow(row);
+        applyDataRowStyle(row);
+        populate(row, p);
+      }
+    }
+
+    void populate(final int row, final PluginInfo plugin) {
+      table.setText(row, 1, plugin.name());
+      table.setText(row, 2, plugin.version());
+
+      final FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell());
+      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
+
+      setRowItem(row, plugin);
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginScreen.java
new file mode 100644
index 0000000..72cd7f9
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginScreen.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.admin;
+
+import static com.google.gerrit.common.PageLinks.ADMIN_PLUGINS;
+
+import com.google.gerrit.client.ui.MenuScreen;
+
+public abstract class PluginScreen extends MenuScreen {
+
+  public PluginScreen() {
+    setRequiresSignIn(true);
+
+    link(Util.C.pluginTabInstalled(), ADMIN_PLUGINS);
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    setPageTitle(Util.C.plugins());
+    display();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
index 1ed919b..923a63e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ProjectAccess;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.GWT;
@@ -33,6 +34,8 @@
 import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwtexpui.globalkey.client.NpTextArea;
 
+import java.util.Collections;
+
 public class ProjectAccessScreen extends ProjectScreen {
   interface Binder extends UiBinder<HTMLPanel, ProjectAccessScreen> {
   }
@@ -110,6 +113,8 @@
 
   @UiHandler("edit")
   void onEdit(ClickEvent event) {
+    resetEditors();
+
     edit.setEnabled(false);
     cancel1.setVisible(true);
     UIObject.setVisible(commitTools, true);
@@ -117,6 +122,18 @@
     driver.edit(access);
   }
 
+  private void resetEditors() {
+    // Push an empty instance through the driver before pushing the real
+    // data. This will force GWT to delete and recreate the editors, which
+    // is required to build initialize them as editable vs. read-only.
+    ProjectAccess mock = new ProjectAccess();
+    mock.setProjectName(access.getProjectName());
+    mock.setRevision(access.getRevision());
+    mock.setLocal(Collections.<AccessSection> emptyList());
+    mock.setOwnerOf(Collections.<String> emptySet());
+    driver.edit(mock);
+  }
+
   @UiHandler(value={"cancel1", "cancel2"})
   void onCancel(ClickEvent event) {
     Gerrit.display(PageLinks.toProjectAcceess(getProjectKey()));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
index be892a1..05dd5d9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.client.rpc.NativeList;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Account;
 
 import java.util.Collections;
@@ -50,13 +49,14 @@
 
     outgoing.setTitleText(Util.C.outgoingReviews());
     incoming.setTitleText(Util.C.incomingReviews());
+    incoming.setHighlightUnreviewed(true);
     closed.setTitleText(Util.C.recentlyClosed());
 
     table.addSection(outgoing);
     table.addSection(incoming);
     table.addSection(closed);
     add(table);
-    table.setSavePointerId(PageLinks.toAccountDashboard(ownerId));
+    table.setSavePointerId("owner:" + ownerId);
   }
 
   @Override
@@ -112,8 +112,7 @@
       }
     }
 
-    Collections.sort(out.asList(), compare());
-    Collections.sort(in.asList(), compare());
+    Collections.sort(out.asList(), outComparator());
 
     table.updateColumnsForLabels(out, in, done);
     outgoing.display(out);
@@ -122,18 +121,11 @@
     table.finishDisplay();
   }
 
-  private Comparator<ChangeInfo> compare() {
+  private Comparator<ChangeInfo> outComparator() {
     return new Comparator<ChangeInfo>() {
       @Override
       public int compare(ChangeInfo a, ChangeInfo b) {
-        int cmp = a.project().compareTo(b.project());
-        if (cmp != 0) return cmp;
-        cmp = a.branch().compareTo(b.branch());
-        if (cmp != 0) return cmp;
-
-        String at = a.topic() != null ? a.topic() : "";
-        String bt = b.topic() != null ? b.topic() : "";
-        cmp = at.compareTo(bt);
+        int cmp = a.created().compareTo(b.created());
         if (cmp != 0) return cmp;
         return a._number() - b._number();
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
index c0c9ce8..20df1df 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
@@ -182,6 +182,9 @@
               break;
             }
 
+            case MAY:
+              break;
+
             case NEED:
             case IMPOSSIBLE:
               if (reportedMissing.add(lbl.label)) {
@@ -204,10 +207,12 @@
             continue;
           }
           String labelName = legacyType.getCategory().getLabelName();
-          if (psa.getValue() == legacyType.getMax().getValue()) {
-            ad.approved(labelName);
-          } else if (psa.getValue() == legacyType.getMin().getValue()) {
-            ad.rejected(labelName);
+          if (psa.getValue() != 0 ) {
+            if (psa.getValue() == legacyType.getMax().getValue()) {
+              ad.approved(labelName);
+            } else if (psa.getValue() == legacyType.getMin().getValue()) {
+              ad.rejected(labelName);
+            }
           }
           if (!columns.contains(labelName)) {
             columns.add(labelName);
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 06c8e61..0c8e03e 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
@@ -33,6 +33,18 @@
     return new Change.Id(_number());
   }
 
+  public final Timestamp created() {
+    Timestamp ts = _get_cts();
+    if (ts == null) {
+      ts = JavaSqlTimestamp_JsonSerializer.parseTimestamp(createdRaw());
+      _set_cts(ts);
+    }
+    return ts;
+  }
+
+  private final native Timestamp _get_cts() /*-{ return this._cts; }-*/;
+  private final native void _set_cts(Timestamp ts) /*-{ this._cts = ts; }-*/;
+
   public final Timestamp updated() {
     return JavaSqlTimestamp_JsonSerializer.parseTimestamp(updatedRaw());
   }
@@ -56,8 +68,10 @@
   private final native String statusRaw() /*-{ return this.status; }-*/;
   public final native String subject() /*-{ return this.subject; }-*/;
   public final native AccountInfo owner() /*-{ return this.owner; }-*/;
+  private final native String createdRaw() /*-{ return this.created; }-*/;
   private final native String updatedRaw() /*-{ return this.updated; }-*/;
   public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
+  public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
   public final native String _sortkey() /*-{ return this._sortkey; }-*/;
   private final native JavaScriptObject labels0() /*-{ return this.labels; }-*/;
   public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/;
@@ -81,6 +95,8 @@
         return SubmitRecord.Label.Status.OK;
       } else if (rejected() != null) {
         return SubmitRecord.Label.Status.REJECT;
+      } else if (optional()) {
+        return SubmitRecord.Label.Status.MAY;
       } else {
         return SubmitRecord.Label.Status.NEED;
       }
@@ -92,6 +108,7 @@
 
     public final native AccountInfo recommended() /*-{ return this.recommended; }-*/;
     public final native AccountInfo disliked() /*-{ return this.disliked; }-*/;
+    public final native boolean optional() /*-{ return this.optional ? true : false; }-*/;
     final native short _value()
     /*-{
       if (this.value) return this.value;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
index 15c1150..f761270 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
@@ -291,18 +291,28 @@
     addComments(detail);
 
     // If any dependency change is still open, or is outdated,
+    // or the change is needed by a change that is new or submitted,
     // show our dependency list.
     //
     boolean depsOpen = false;
     int outdated = 0;
-    if (!detail.getChange().getStatus().isClosed()
-        && detail.getDependsOn() != null) {
-      for (final ChangeInfo ci : detail.getDependsOn()) {
-        if (! ci.isLatest()) {
-          depsOpen = true;
-          outdated++;
-        } else if (ci.getStatus() != Change.Status.MERGED) {
-          depsOpen = true;
+    if (!detail.getChange().getStatus().isClosed()) {
+      if (detail.getDependsOn() != null) {
+        for (final ChangeInfo ci : detail.getDependsOn()) {
+          if (!ci.isLatest()) {
+            depsOpen = true;
+            outdated++;
+          } else if (ci.getStatus() != Change.Status.MERGED) {
+            depsOpen = true;
+          }
+        }
+      }
+      if (detail.getNeededBy() != null) {
+        for (final ChangeInfo ci : detail.getNeededBy()) {
+          if ((ci.getStatus() == Change.Status.NEW) ||
+              (ci.getStatus() == Change.Status.SUBMITTED)) {
+            depsOpen = true;
+          }
         }
       }
     }
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 1b9db39..fc2b418 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
@@ -26,16 +26,19 @@
 import com.google.gerrit.client.ui.ProjectLink;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.dom.client.Element;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTMLTable.Cell;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.InlineLabel;
+import com.google.gwt.user.client.ui.UIObject;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -188,7 +191,8 @@
     }
   }
 
-  private void populateChangeRow(final int row, final ChangeInfo c) {
+  private void populateChangeRow(final int row, final ChangeInfo c,
+      boolean highlightUnreviewed) {
     if (Gerrit.isSignedIn()) {
       table.setWidget(row, C_STAR, StarredChanges.createIcon(
           c.legacy_id(),
@@ -284,10 +288,13 @@
       }
     }
 
-    // TODO(sop): Highlight changes I haven't reviewed on my dashboard.
-    // final Element tr = DOM.getParent(fmt.getElement(row, 0));
-    // UIObject.setStyleName(tr, Gerrit.RESOURCES.css().needsReview(),
-    // !haveReview && highlightUnreviewed);
+    boolean needHighlight = false;
+    if (highlightUnreviewed && !c.reviewed()) {
+      needHighlight = true;
+    }
+    final Element tr = DOM.getParent(fmt.getElement(row, 0));
+    UIObject.setStyleName(tr, Gerrit.RESOURCES.css().needsReview(),
+        needHighlight);
 
     setRowItem(row, c);
   }
@@ -368,6 +375,11 @@
     int titleRow = -1;
     int dataBegin;
     int rows;
+    private boolean highlightUnreviewed;
+
+    public void setHighlightUnreviewed(boolean value) {
+      this.highlightUnreviewed = value;
+    }
 
     public void setTitleText(final String text) {
       titleText = text;
@@ -399,7 +411,8 @@
         rows++;
       }
       for (int i = 0; i < sz; i++) {
-        parent.populateChangeRow(dataBegin + i, changeList.get(i));
+        parent.populateChangeRow(dataBegin + i, changeList.get(i),
+            highlightUnreviewed);
       }
     }
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
index 8b86d50..00baf28 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
@@ -27,9 +27,9 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.Patch;
@@ -232,9 +232,8 @@
           .anonymousDownload("HTTP"), r.toString()));
     }
 
-    if (Gerrit.getConfig().getSshdAddress() != null && Gerrit.isSignedIn()
-        && Gerrit.getUserAccount().getUserName() != null
-        && Gerrit.getUserAccount().getUserName().length() > 0
+    if (Gerrit.getConfig().getSshdAddress() != null
+        && hasUserName()
         && (allowedSchemes.contains(DownloadScheme.SSH) ||
             allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
       String sshAddr = Gerrit.getConfig().getSshdAddress();
@@ -256,13 +255,12 @@
       urls.add(new DownloadUrlLink(DownloadScheme.SSH, "SSH", r.toString()));
     }
 
-    if (Gerrit.isSignedIn() && Gerrit.getUserAccount().getUserName() != null
-        && Gerrit.getUserAccount().getUserName().length() > 0
-        && (allowedSchemes.contains(DownloadScheme.HTTP) ||
-            allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
+    if ((hasUserName() || siteReliesOnHttp())
+        && (allowedSchemes.contains(DownloadScheme.HTTP)
+            || allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
       final StringBuilder r = new StringBuilder();
       if (Gerrit.getConfig().getGitHttpUrl() != null
-          && changeDetail.isAllowsAnonymous()) {
+          && (changeDetail.isAllowsAnonymous() || siteReliesOnHttp())) {
         r.append(Gerrit.getConfig().getGitHttpUrl());
       } else {
         String base = hostPageUrl;
@@ -372,6 +370,18 @@
     infoTable.setWidget(R_DOWNLOAD, 1, fp);
   }
 
+  private static boolean siteReliesOnHttp() {
+    return Gerrit.getConfig().getGitHttpUrl() != null
+        && Gerrit.getConfig().getAuthType() == AuthType.CUSTOM_EXTENSION
+        && !Gerrit.getConfig().siteHasUsernames();
+  }
+
+  private static boolean hasUserName() {
+    return Gerrit.isSignedIn()
+        && Gerrit.getUserAccount().getUserName() != null
+        && Gerrit.getUserAccount().getUserName().length() > 0;
+  }
+
   private void displayUserIdentity(final int row, final UserIdentity who) {
     if (who == null) {
       infoTable.clearCell(row, 1);
@@ -381,10 +391,9 @@
     final FlowPanel fp = new FlowPanel();
     fp.setStyleName(Gerrit.RESOURCES.css().patchSetUserIdentity());
     if (who.getName() != null) {
-      final Account.Id aId = who.getAccount();
-      if (aId != null) {
-        fp.add(new InlineHyperlink(who.getName(), PageLinks.toAccountQuery(who
-            .getName())));
+      if (who.getAccount() != null) {
+        fp.add(new InlineHyperlink(who.getName(),
+            PageLinks.toAccountQuery(who.getName())));
       } else {
         final InlineLabel lbl = new InlineLabel(who.getName());
         lbl.setStyleName(Gerrit.RESOURCES.css().accountName());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
index 52c37c3..9a1ca43 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
@@ -1111,6 +1111,9 @@
 .accountInfoBlock {
   margin-bottom: 10px;
 }
+.accountInfoBlock .gwt-Button {
+  margin-left: 10px;
+}
 .accountContactPrivacyDetails {
   margin-left: 10px;
   margin-top: 5px;
@@ -1383,3 +1386,7 @@
   font-style: italic;
   padding: 2px 6px 1px;
 }
+
+/** PluginListScreen **/
+.pluginsTable {
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
index ffc8960..77d8659 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
@@ -297,6 +297,7 @@
   @Override
   protected void onLoad() {
     super.onLoad();
+
     if (patchSetDetail == null) {
       Util.DETAIL_SVC.patchSetDetail(idSideB,
           new GerritCallback<PatchSetDetail>() {
@@ -368,6 +369,9 @@
     final int rpcseq = ++rpcSequence;
     lastScript = null;
     settingsPanel.setEnabled(false);
+    if (isFirst && fileList != null) {
+      fileList.movePointerTo(patchKey);
+    }
     PatchUtil.DETAIL_SVC.patchScript(patchKey, idSideA, idSideB, //
         settingsPanel.getValue(), new ScreenLoadCallback<PatchScript>(this) {
           @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginInfo.java
similarity index 65%
copy from gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
copy to gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginInfo.java
index 204d777..454c97b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginInfo.java
@@ -12,12 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.client.plugins;
 
-public class IncompleteUserInfoException extends Exception {
-  private static final long serialVersionUID = 1L;
+import com.google.gwt.core.client.JavaScriptObject;
 
-  public IncompleteUserInfoException(final String userName, final String missingInfo) {
-    super("For the user \"" + userName + "\" " + missingInfo + " is not set.");
+public class PluginInfo extends JavaScriptObject {
+
+  public final native String name() /*-{ return this.name; }-*/;
+  public final native String version() /*-{ return this.version; }-*/;
+
+  protected PluginInfo() {
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginMap.java
new file mode 100644
index 0000000..0f2ab4c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginMap.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.plugins;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+
+/** Plugins available from {@code /plugins/}. */
+public class PluginMap extends NativeMap<PluginInfo> {
+  public static void all(AsyncCallback<PluginMap> callback) {
+    new RestApi("/plugins/")
+        .send(NativeMap.copyKeysIntoChildren(callback));
+  }
+
+  protected PluginMap() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java
index 3d99c9e..a6c609c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java
@@ -36,7 +36,8 @@
     if (parser == null) {
       parser = bestJsonParser();
     }
-    return parse0(parser, json);
+    // javac generics bug
+    return Natives.<T>parse0(parser, json);
   }
 
   private static native <T extends JavaScriptObject>
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 bd69092..e1fb883 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
@@ -124,7 +124,8 @@
 
         T data;
         try {
-          data = Natives.parseJSON(json.substring(JSON_MAGIC.length()));
+          // javac generics bug
+          data = Natives.<T>parseJSON(json.substring(JSON_MAGIC.length()));
         } catch (RuntimeException e) {
           cb.onFailure(new RemoteJsonException("Invalid JSON"));
           return;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
index 885f53b..5da00cd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.user.client.ui.SuggestOracle;
 import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
 
@@ -31,11 +32,14 @@
   private Map<String, AccountGroup.UUID> priorResults =
       new HashMap<String, AccountGroup.UUID>();
 
+  private Project.NameKey projectName;
+
   @Override
   public void onRequestSuggestions(final Request req, final Callback callback) {
     RpcStatus.hide(new Runnable() {
       public void run() {
-        SuggestUtil.SVC.suggestAccountGroup(req.getQuery(), req.getLimit(),
+        SuggestUtil.SVC.suggestAccountGroupForProject(
+            projectName, req.getQuery(), req.getLimit(),
             new GerritCallback<List<GroupReference>>() {
               public void onSuccess(final List<GroupReference> result) {
                 priorResults.clear();
@@ -52,6 +56,10 @@
     });
   }
 
+  public void setProject(Project.NameKey projectName) {
+    this.projectName = projectName;
+  }
+
   private static class AccountGroupSuggestion implements
       SuggestOracle.Suggestion {
     private final GroupReference info;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
index a4f4509..790102c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.QueryScreen;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.common.data.AccountInfoCache;
@@ -31,24 +30,22 @@
     return ai != null ? new AccountLink(ai) : null;
   }
 
-  private final String query;
-
   public AccountLink(final AccountInfo ai) {
-    this(FormatUtil.name(ai), ai);
-  }
-
-  public AccountLink(final String text, final AccountInfo ai) {
-    super(text, PageLinks.toAccountQuery(FormatUtil.name(ai)));
+    super(FormatUtil.name(ai), PageLinks.toAccountQuery(owner(ai)));
     setTitle(FormatUtil.nameEmail(ai));
-    this.query = "owner:\"" + FormatUtil.name(ai) + "\"";
   }
 
-  private Screen createScreen() {
-    return QueryScreen.forQuery(query);
+  private static String owner(AccountInfo ai) {
+    if (ai.getPreferredEmail() != null) {
+      return ai.getPreferredEmail();
+    } else if (ai.getFullName() != null) {
+      return ai.getFullName();
+    }
+    return "" + ai.getId().get();
   }
 
   @Override
   public void go() {
-    Gerrit.display(getTargetHistoryToken(), createScreen());
+    Gerrit.display(getTargetHistoryToken());
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index fb30a4d..151a6d9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -26,14 +26,11 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AuthResult;
-import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EvictionPolicy;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Provider;
-import com.google.inject.TypeLiteral;
 import com.google.inject.servlet.RequestScoped;
 
 import javax.servlet.http.Cookie;
@@ -49,13 +46,9 @@
     return new CacheModule() {
       @Override
       protected void configure() {
-        final String cacheName = WebSessionManager.CACHE_NAME;
-        final TypeLiteral<Cache<Key, Val>> type =
-            new TypeLiteral<Cache<Key, Val>>() {};
-        disk(type, cacheName) //
-            .memoryLimit(1024) // reasonable default for many sites
-            .maxAge(MAX_AGE_MINUTES, MINUTES) // expire sessions if they are inactive
-            .evictionPolicy(EvictionPolicy.LRU) // keep most recently used
+        persist(WebSessionManager.CACHE_NAME, String.class, Val.class)
+            .maximumWeight(1024) // reasonable default for many sites
+            .expireAfterWrite(MAX_AGE_MINUTES, MINUTES) // expire sessions if they are inactive
         ;
         bind(WebSessionManager.class);
         bind(WebSession.class)
@@ -170,7 +163,7 @@
   /** Set the user account for this current request only. */
   public void setUserAccountId(Account.Id id) {
     key = new Key("id:" + id);
-    val = new Val(id, 0, false, null, "");
+    val = new Val(id, 0, false, null, "", 0);
   }
 
   public void logout() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
index 1953480..f92f13d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
@@ -93,10 +93,12 @@
       case LDAP:
       case LDAP_BIND:
         config.setRegisterUrl(cfg.getString("auth", null, "registerurl"));
+        config.setEditFullNameUrl(cfg.getString("auth", null, "editFullNameUrl"));
         break;
 
       case CUSTOM_EXTENSION:
         config.setRegisterUrl(cfg.getString("auth", null, "registerurl"));
+        config.setEditFullNameUrl(cfg.getString("auth", null, "editFullNameUrl"));
         config.setHttpPasswordUrl(cfg.getString("auth", null, "httpPasswordUrl"));
         break;
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index c36df04a..1c9c521 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.httpd;
 
+import com.google.common.cache.Cache;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.git.AsyncReceiveCommits;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -99,11 +99,11 @@
       install(new CacheModule() {
         @Override
         protected void configure() {
-          TypeLiteral<Cache<AdvertisedObjectsCacheKey, Set<ObjectId>>> cache =
-              new TypeLiteral<Cache<AdvertisedObjectsCacheKey, Set<ObjectId>>>() {};
-          core(cache, ID_CACHE)
-            .memoryLimit(4096)
-            .maxAge(10, TimeUnit.MINUTES);
+          cache(ID_CACHE,
+              AdvertisedObjectsCacheKey.class,
+              new TypeLiteral<Set<ObjectId>>() {})
+            .maximumWeight(4096)
+            .expireAfterWrite(10, TimeUnit.MINUTES);
         }
       });
     }
@@ -167,7 +167,12 @@
       }
       req.setAttribute(ATT_CONTROL, pc);
 
-      return manager.openRepository(pc.getProject().getNameKey());
+      try {
+        return manager.openRepository(pc.getProject().getNameKey());
+      } catch (IOException e) {
+        throw new RepositoryNotFoundException(
+            pc.getProject().getNameKey().get(), e);
+      }
     }
   }
 
@@ -315,12 +320,12 @@
 
       if (isGet) {
         rc.advertiseHistory();
-        cache.remove(cacheKey);
+        cache.invalidate(cacheKey);
       } else {
-        Set<ObjectId> ids = cache.get(cacheKey);
+        Set<ObjectId> ids = cache.getIfPresent(cacheKey);
         if (ids != null) {
           rp.getAdvertisedObjects().addAll(ids);
-          cache.remove(cacheKey);
+          cache.invalidate(cacheKey);
         }
       }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpIdentifiedUserProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpIdentifiedUserProvider.java
deleted file mode 100644
index 6c420a5..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpIdentifiedUserProvider.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd;
-
-import com.google.gerrit.common.errors.NotSignedInException;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-
-class HttpIdentifiedUserProvider implements Provider<IdentifiedUser> {
-  private final Provider<CurrentUser> currUserProvider;
-
-  @Inject
-  HttpIdentifiedUserProvider(Provider<CurrentUser> currUserProvider) {
-    this.currUserProvider = currUserProvider;
-  }
-
-  @Override
-  public IdentifiedUser get() {
-    CurrentUser user = currUserProvider.get();
-    if (user instanceof IdentifiedUser) {
-      return (IdentifiedUser) user;
-    }
-    throw new ProvisionException(NotSignedInException.MESSAGE,
-        new NotSignedInException());
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCurrentUserProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
similarity index 81%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCurrentUserProvider.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
index c87a143..8ef826b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCurrentUserProvider.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
@@ -15,19 +15,19 @@
 package com.google.gerrit.httpd;
 
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.util.RequestContext;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
-class HttpCurrentUserProvider implements Provider<CurrentUser> {
+class HttpRequestContext implements RequestContext {
   private final WebSession session;
 
   @Inject
-  HttpCurrentUserProvider(final WebSession session) {
+  HttpRequestContext(final WebSession session) {
     this.session = session;
   }
 
   @Override
-  public CurrentUser get() {
+  public CurrentUser getCurrentUser() {
     return session.getCurrentUser();
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestCleanupFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestContextFilter.java
similarity index 61%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestCleanupFilter.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestContextFilter.java
index 0e6a567..b46505f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestCleanupFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestContextFilter.java
@@ -15,9 +15,13 @@
 package com.google.gerrit.httpd;
 
 import com.google.gerrit.server.RequestCleanup;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.inject.Inject;
+import com.google.inject.Module;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import com.google.inject.servlet.ServletModule;
 
 import java.io.IOException;
 
@@ -30,12 +34,27 @@
 
 /** Executes any pending {@link RequestCleanup} at the end of a request. */
 @Singleton
-class RequestCleanupFilter implements Filter {
+public class RequestContextFilter implements Filter {
+  public static Module module() {
+    return new ServletModule() {
+      @Override
+      protected void configureServlets() {
+        filter("/*").through(RequestContextFilter.class);
+      }
+    };
+  }
+
   private final Provider<RequestCleanup> cleanup;
+  private final Provider<HttpRequestContext> requestContext;
+  private final ThreadLocalRequestContext local;
 
   @Inject
-  RequestCleanupFilter(final Provider<RequestCleanup> r) {
+  RequestContextFilter(final Provider<RequestCleanup> r,
+      final Provider<HttpRequestContext> c,
+      final ThreadLocalRequestContext l) {
     cleanup = r;
+    requestContext = c;
+    local = l;
   }
 
   @Override
@@ -50,10 +69,15 @@
   public void doFilter(final ServletRequest request,
       final ServletResponse response, final FilterChain chain)
       throws IOException, ServletException {
+    RequestContext old = local.setContext(requestContext.get());
     try {
-      chain.doFilter(request, response);
+      try {
+        chain.doFilter(request, response);
+      } finally {
+        cleanup.get().run();
+      }
     } finally {
-      cleanup.get().run();
+      local.setContext(old);
     }
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java
index 8105e25..ff41ad8 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java
@@ -15,10 +15,14 @@
 package com.google.gerrit.httpd;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.util.cli.CmdLineParser;
-import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.gwtjsonrpc.common.JsonConstants;
+import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 import org.kohsuke.args4j.CmdLineException;
 import org.slf4j.Logger;
@@ -62,12 +66,24 @@
     }
   }
 
+  private final Provider<CurrentUser> currentUser;
+
+  @Inject
+  protected RestApiServlet(final Provider<CurrentUser> currentUser) {
+    this.currentUser = currentUser;
+  }
+
   @Override
   protected void service(HttpServletRequest req, HttpServletResponse res)
       throws ServletException, IOException {
     noCache(res);
     try {
+      checkRequiresCapability();
       super.service(req, res);
+    } catch (RequireCapabilityException err) {
+      res.setStatus(HttpServletResponse.SC_FORBIDDEN);
+      noCache(res);
+      sendText(req, res, err.getMessage());
     } catch (Error err) {
       handleError(err, req, res);
     } catch (RuntimeException err) {
@@ -75,6 +91,20 @@
     }
   }
 
+  private void checkRequiresCapability() throws RequireCapabilityException {
+    RequiresCapability rc = getClass().getAnnotation(RequiresCapability.class);
+    if (rc != null) {
+      CurrentUser user = currentUser.get();
+      CapabilityControl ctl = user.getCapabilities();
+      if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
+        String msg = String.format(
+            "fatal: %s does not have \"%s\" capability.",
+            user.getUserName(), rc.value());
+        throw new RequireCapabilityException(msg);
+      }
+    }
+  }
+
   private static void noCache(HttpServletResponse res) {
     res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
     res.setHeader("Pragma", "no-cache");
@@ -175,4 +205,11 @@
       return true;
     }
   }
+
+  @SuppressWarnings("serial") // Never serialized or thrown out of this class.
+  private static class RequireCapabilityException extends Exception {
+    public RequireCapabilityException(String msg) {
+      super(msg);
+    }
+  }
 }
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 9e69946..a712f9b 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
@@ -26,6 +26,7 @@
 import com.google.gerrit.httpd.rpc.account.AccountCapabilitiesServlet;
 import com.google.gerrit.httpd.rpc.change.DeprecatedChangeQueryServlet;
 import com.google.gerrit.httpd.rpc.change.ListChangesServlet;
+import com.google.gerrit.httpd.rpc.plugin.ListPluginsServlet;
 import com.google.gerrit.httpd.rpc.project.ListProjectsServlet;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
@@ -95,6 +96,7 @@
     filter("/a/*").through(RequireIdentifiedUserFilter.class);
     serveRegex("^/(?:a/)?accounts/self/capabilities$").with(AccountCapabilitiesServlet.class);
     serveRegex("^/(?:a/)?changes/$").with(ListChangesServlet.class);
+    serveRegex("^/(?:a/)?plugins/$").with(ListPluginsServlet.class);
     serveRegex("^/(?:a/)?projects/(.*)?$").with(ListProjectsServlet.class);
 
     if (cfg.deprecatedQuery) {
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 0f223c9..0d14b79 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd;
 
 import static com.google.inject.Scopes.SINGLETON;
+import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
 
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.httpd.auth.become.BecomeAnyAccountLoginServlet;
@@ -23,8 +24,8 @@
 import com.google.gerrit.httpd.auth.ldap.LdapAuthModule;
 import com.google.gerrit.httpd.gitweb.GitWebModule;
 import com.google.gerrit.httpd.rpc.UiRpcModule;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.CmdLineParserModule;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.ChangeUserName;
@@ -38,7 +39,6 @@
 import com.google.gerrit.server.contact.ContactStoreProvider;
 import com.google.gerrit.server.util.GuiceRequestScopePropagator;
 import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
@@ -76,13 +76,8 @@
 
   @Override
   protected void configure() {
-    install(new ServletModule() {
-      @Override
-      protected void configureServlets() {
-        filter("/*").through(RequestCleanupFilter.class);
-      }
-    });
     bind(RequestScopePropagator.class).to(GuiceRequestScopePropagator.class);
+    bind(HttpRequestContext.class);
 
     if (wantSSL) {
       install(new RequireSslFilter.Module());
@@ -139,13 +134,17 @@
     bind(ChangeUserName.CurrentUser.class);
     factory(ChangeUserName.Factory.class);
     factory(ClearPassword.Factory.class);
-    factory(CmdLineParser.Factory.class);
+    install(new CmdLineParserModule());
     factory(GeneratePassword.Factory.class);
 
     bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
         HttpRemotePeerProvider.class).in(RequestScoped.class);
 
-    bind(CurrentUser.class).toProvider(HttpCurrentUserProvider.class);
-    bind(IdentifiedUser.class).toProvider(HttpIdentifiedUserProvider.class);
+    install(new LifecycleModule() {
+      @Override
+      protected void configure() {
+        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 55d0ca5..4b4edf4 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
@@ -26,9 +26,9 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
 
+import com.google.common.cache.Cache;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
@@ -43,6 +43,7 @@
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
 import java.security.SecureRandom;
+import java.util.concurrent.TimeUnit;
 
 @Singleton
 class WebSessionManager {
@@ -54,11 +55,11 @@
 
   private final long sessionMaxAgeMillis;
   private final SecureRandom prng;
-  private final Cache<Key, Val> self;
+  private final Cache<String, Val> self;
 
   @Inject
   WebSessionManager(@GerritServerConfig Config cfg,
-      @Named(CACHE_NAME) final Cache<Key, Val> cache) {
+      @Named(CACHE_NAME) final Cache<String, Val> cache) {
     prng = new SecureRandom();
     self = cache;
 
@@ -75,7 +76,7 @@
       prng.nextBytes(rnd);
 
       buf = new ByteArrayOutputStream(3 + nonceLen);
-      writeVarInt32(buf, (int) Key.serialVersionUID);
+      writeVarInt32(buf, (int) Val.serialVersionUID);
       writeVarInt32(buf, who.get());
       writeBytes(buf, rnd);
 
@@ -104,7 +105,9 @@
     final long halfAgeRefresh = sessionMaxAgeMillis >>> 1;
     final long minRefresh = MILLISECONDS.convert(1, HOURS);
     final long refresh = Math.min(halfAgeRefresh, minRefresh);
-    final long refreshCookieAt = now() + refresh;
+    final long now = now();
+    final long refreshCookieAt = now + refresh;
+    final long expiresAt = now + sessionMaxAgeMillis;
 
     if (xsrfToken == null) {
       // If we don't yet have a token for this session, establish one.
@@ -115,8 +118,9 @@
       xsrfToken = CookieBase64.encode(rnd);
     }
 
-    Val val = new Val(who, refreshCookieAt, remember, lastLogin, xsrfToken);
-    self.put(key, val);
+    Val val = new Val(who, refreshCookieAt, remember,
+        lastLogin, xsrfToken, expiresAt);
+    self.put(key.token, val);
     return val;
   }
 
@@ -137,16 +141,19 @@
   }
 
   Val get(final Key key) {
-    return self.get(key);
+    Val val = self.getIfPresent(key.token);
+    if (val != null && val.expiresAt <= now()) {
+      self.invalidate(key.token);
+      return null;
+    }
+    return val;
   }
 
   void destroy(final Key key) {
-    self.remove(key);
+    self.invalidate(key.token);
   }
 
-  static final class Key implements Serializable {
-    static final long serialVersionUID = 2L;
-
+  static final class Key  {
     private transient String token;
 
     Key(final String t) {
@@ -166,33 +173,28 @@
     public boolean equals(Object obj) {
       return obj instanceof Key && token.equals(((Key) obj).token);
     }
-
-    private void writeObject(final ObjectOutputStream out) throws IOException {
-      writeString(out, token);
-    }
-
-    private void readObject(final ObjectInputStream in) throws IOException {
-      token = readString(in);
-    }
   }
 
   static final class Val implements Serializable {
-    static final long serialVersionUID = Key.serialVersionUID;
+    static final long serialVersionUID = 2L;
 
     private transient Account.Id accountId;
     private transient long refreshCookieAt;
     private transient boolean persistentCookie;
     private transient AccountExternalId.Key externalId;
     private transient String xsrfToken;
+    private transient long expiresAt;
 
     Val(final Account.Id accountId, final long refreshCookieAt,
         final boolean persistentCookie, final AccountExternalId.Key externalId,
-        final String xsrfToken) {
+        final String xsrfToken,
+        final long expiresAt) {
       this.accountId = accountId;
       this.refreshCookieAt = refreshCookieAt;
       this.persistentCookie = persistentCookie;
       this.externalId = externalId;
       this.xsrfToken = xsrfToken;
+      this.expiresAt = expiresAt;
     }
 
     Account.Id getAccountId() {
@@ -233,6 +235,9 @@
       writeVarInt32(out, 5);
       writeString(out, xsrfToken);
 
+      writeVarInt32(out, 6);
+      writeFixInt64(out, expiresAt);
+
       writeVarInt32(out, 0);
     }
 
@@ -257,10 +262,16 @@
           case 5:
             xsrfToken = readString(in);
             continue;
+          case 6:
+            expiresAt = readFixInt64(in);
+            continue;
           default:
             throw new IOException("Unknown tag found in object: " + tag);
         }
       }
+      if (expiresAt == 0) {
+        expiresAt = refreshCookieAt + TimeUnit.HOURS.toMillis(2);
+      }
     }
   }
 }
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 2e5001b..bb47b8b 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,6 +14,7 @@
 
 package com.google.gerrit.httpd.plugins;
 
+import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.plugins.ModuleGenerator;
 import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.plugins.StartPluginListener;
@@ -21,6 +22,8 @@
 import com.google.inject.servlet.ServletModule;
 
 public class HttpPluginModule extends ServletModule {
+  static final String PLUGIN_RESOURCES = "plugin_resources";
+
   @Override
   protected void configureServlets() {
     bind(HttpPluginServlet.class);
@@ -36,5 +39,14 @@
 
     bind(ModuleGenerator.class)
       .to(HttpAutoRegisterModuleGenerator.class);
+
+    install(new CacheModule() {
+      @Override
+      protected void configure() {
+        cache(PLUGIN_RESOURCES, ResourceKey.class, Resource.class)
+          .maximumWeight(2 << 20)
+          .weigher(ResourceWeigher.class);
+      }
+    });
   }
 }
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 23dbaac..47eae99 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
@@ -15,31 +15,46 @@
 package com.google.gerrit.httpd.plugins;
 
 import com.google.common.base.Strings;
+import com.google.common.cache.Cache;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.server.MimeUtilFileTypeRegistry;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.documentation.MarkdownFormatter;
 import com.google.gerrit.server.plugins.Plugin;
 import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.plugins.StartPluginListener;
+import com.google.gerrit.server.ssh.SshInfo;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import com.google.inject.name.Named;
 import com.google.inject.servlet.GuiceFilter;
 
-import eu.medsea.mimeutil.MimeType;
-
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.util.RawParseUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Enumeration;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ConcurrentMap;
 import java.util.jar.Attributes;
 import java.util.jar.JarEntry;
 import java.util.jar.JarFile;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import javax.servlet.FilterChain;
 import javax.servlet.ServletConfig;
@@ -54,19 +69,47 @@
 @Singleton
 class HttpPluginServlet extends HttpServlet
     implements StartPluginListener, ReloadPluginListener {
+  private static final int SMALL_RESOURCE = 128 * 1024;
   private static final long serialVersionUID = 1L;
   private static final Logger log
       = LoggerFactory.getLogger(HttpPluginServlet.class);
 
   private final MimeUtilFileTypeRegistry mimeUtil;
+  private final Provider<String> webUrl;
+  private final Cache<ResourceKey, Resource> resourceCache;
+  private final String sshHost;
+  private final int sshPort;
+
   private List<Plugin> pending = Lists.newArrayList();
   private String base;
   private final ConcurrentMap<String, PluginHolder> plugins
       = Maps.newConcurrentMap();
 
   @Inject
-  HttpPluginServlet(MimeUtilFileTypeRegistry mimeUtil) {
+  HttpPluginServlet(MimeUtilFileTypeRegistry mimeUtil,
+      @CanonicalWebUrl Provider<String> webUrl,
+      @Named(HttpPluginModule.PLUGIN_RESOURCES) Cache<ResourceKey, Resource> cache,
+      @GerritServerConfig Config cfg,
+      SshInfo sshInfo) {
     this.mimeUtil = mimeUtil;
+    this.webUrl = webUrl;
+    this.resourceCache = cache;
+
+    String sshHost = "review.example.com";
+    int sshPort = 29418;
+    if (!sshInfo.getHostKeys().isEmpty()) {
+      String host = sshInfo.getHostKeys().get(0).getHost();
+      int c = host.lastIndexOf(':');
+      if (0 <= c) {
+        sshHost = host.substring(0, c);
+        sshPort = Integer.parseInt(host.substring(c+1));
+      } else {
+        sshHost = host;
+        sshPort = 22;
+      }
+    }
+    this.sshHost = sshHost;
+    this.sshPort = sshPort;
   }
 
   @Override
@@ -167,52 +210,306 @@
   private void onDefault(PluginHolder holder,
       HttpServletRequest req,
       HttpServletResponse res) throws IOException {
-    String uri = req.getRequestURI();
-    String ctx = req.getContextPath();
-    String file = uri.substring(ctx.length() + 1);
-    if (file.startsWith("Documentation/") || file.startsWith("static/")) {
-      JarFile jar = holder.plugin.getJarFile();
-      JarEntry entry = jar.getJarEntry(file);
-      if (entry != null && entry.getSize() > 0) {
-        sendResource(jar, entry, res);
-        return;
-      }
+    if (!"GET".equals(req.getMethod()) && !"HEAD".equals(req.getMethod())) {
+      noCache(res);
+      res.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+      return;
     }
 
-    noCache(res);
-    res.sendError(HttpServletResponse.SC_NOT_FOUND);
+    String uri = req.getRequestURI();
+    String ctx = req.getContextPath();
+    if (uri.length() <= ctx.length()) {
+      Resource.NOT_FOUND.send(req, res);
+      return;
+    }
+
+    String file = uri.substring(ctx.length() + 1);
+    ResourceKey key = new ResourceKey(holder.plugin, file);
+    Resource rsc = resourceCache.getIfPresent(key);
+    if (rsc != null) {
+      rsc.send(req, res);
+      return;
+    }
+
+    if ("".equals(file)) {
+      res.sendRedirect(uri + "Documentation/index.html");
+    } else if (file.startsWith("static/")) {
+      JarFile jar = holder.plugin.getJarFile();
+      JarEntry entry = jar.getJarEntry(file);
+      if (exists(entry)) {
+        sendResource(jar, entry, key, res);
+      } else {
+        resourceCache.put(key, Resource.NOT_FOUND);
+        Resource.NOT_FOUND.send(req, res);
+      }
+    } else if (file.equals("Documentation")) {
+      res.sendRedirect(uri + "/index.html");
+    } else if (file.startsWith("Documentation/") && file.endsWith("/")) {
+      res.sendRedirect(uri + "index.html");
+    } else if (file.startsWith("Documentation/")) {
+      JarFile jar = holder.plugin.getJarFile();
+      JarEntry entry = jar.getJarEntry(file);
+      if (!exists(entry)) {
+        entry = findSource(jar, file);
+      }
+      if (!exists(entry) && file.endsWith("/index.html")) {
+        String pfx = file.substring(0, file.length() - "index.html".length());
+        sendAutoIndex(jar, pfx, holder.plugin.getName(), key, res);
+      } else if (exists(entry) && entry.getName().endsWith(".md")) {
+        sendMarkdownAsHtml(jar, entry, holder.plugin.getName(), key, res);
+      } else if (exists(entry)) {
+        sendResource(jar, entry, key, res);
+      } else {
+        resourceCache.put(key, Resource.NOT_FOUND);
+        Resource.NOT_FOUND.send(req, res);
+      }
+    } else {
+      resourceCache.put(key, Resource.NOT_FOUND);
+      Resource.NOT_FOUND.send(req, res);
+    }
   }
 
-  private void sendResource(JarFile jar, JarEntry entry, HttpServletResponse res)
+  private void sendAutoIndex(JarFile jar,
+      String prefix, String pluginName,
+      ResourceKey cacheKey, HttpServletResponse res) throws IOException {
+    List<JarEntry> cmds = Lists.newArrayList();
+    List<JarEntry> docs = Lists.newArrayList();
+    Enumeration<JarEntry> entries = jar.entries();
+    while (entries.hasMoreElements()) {
+      JarEntry entry = entries.nextElement();
+      String name = entry.getName();
+      long size = entry.getSize();
+      if (name.startsWith(prefix)
+          && (name.endsWith(".md")
+              || name.endsWith(".html"))
+          && 0 < size && size <= SMALL_RESOURCE) {
+        if (name.substring(prefix.length()).startsWith("cmd-")) {
+          cmds.add(entry);
+        } else {
+          docs.add(entry);
+        }
+      }
+    }
+    Collections.sort(cmds, new Comparator<JarEntry>() {
+      @Override
+      public int compare(JarEntry a, JarEntry b) {
+        return a.getName().compareTo(b.getName());
+      }
+    });
+    Collections.sort(docs, new Comparator<JarEntry>() {
+      @Override
+      public int compare(JarEntry a, JarEntry b) {
+        return a.getName().compareTo(b.getName());
+      }
+    });
+
+    StringBuilder md = new StringBuilder();
+    md.append(String.format("# Plugin %s #\n", pluginName));
+    md.append("\n");
+    appendPluginInfoTable(md, jar.getManifest().getMainAttributes());
+
+    if (!docs.isEmpty()) {
+      md.append("## Documentation ##\n");
+      for(JarEntry entry : docs) {
+        String rsrc = entry.getName().substring(prefix.length());
+        String title;
+        if (rsrc.endsWith(".html")) {
+          title = rsrc.substring(0, rsrc.length() - 5).replace('-', ' ');
+        } else if (rsrc.endsWith(".md")) {
+          title = extractTitleFromMarkdown(jar, entry);
+          if (Strings.isNullOrEmpty(title)) {
+            title = rsrc.substring(0, rsrc.length() - 3).replace('-', ' ');
+          }
+          rsrc = rsrc.substring(0, rsrc.length() - 3) + ".html";
+        } else {
+          title = rsrc.replace('-', ' ');
+        }
+        md.append(String.format("* [%s](%s)\n", title, rsrc));
+      }
+      md.append("\n");
+    }
+
+    if (!cmds.isEmpty()) {
+      md.append("## Commands ##\n");
+      for(JarEntry entry : cmds) {
+        String rsrc = entry.getName().substring(prefix.length());
+        String title;
+        if (rsrc.endsWith(".html")) {
+          title = rsrc.substring(4, rsrc.length() - 5).replace('-', ' ');
+        } else if (rsrc.endsWith(".md")) {
+          title = extractTitleFromMarkdown(jar, entry);
+          if (Strings.isNullOrEmpty(title)) {
+            title = rsrc.substring(4, rsrc.length() - 3).replace('-', ' ');
+          }
+          rsrc = rsrc.substring(0, rsrc.length() - 3) + ".html";
+        } else {
+          title = rsrc.substring(4).replace('-', ' ');
+        }
+        md.append(String.format("* [%s](%s)\n", title, rsrc));
+      }
+      md.append("\n");
+    }
+
+    sendMarkdownAsHtml(md.toString(), pluginName, cacheKey, res);
+  }
+
+  private void sendMarkdownAsHtml(String md, String pluginName,
+      ResourceKey cacheKey, HttpServletResponse res)
+      throws UnsupportedEncodingException, IOException {
+    Map<String, String> macros = Maps.newHashMap();
+    macros.put("PLUGIN", pluginName);
+    macros.put("SSH_HOST", sshHost);
+    macros.put("SSH_PORT", "" + sshPort);
+    String url = webUrl.get();
+    if (Strings.isNullOrEmpty(url)) {
+      url = "http://review.example.com/";
+    }
+    macros.put("URL", url);
+
+    Matcher m = Pattern.compile("(\\\\)?@([A-Z_]+)@").matcher(md);
+    StringBuffer sb = new StringBuffer();
+    while (m.find()) {
+      String key = m.group(2);
+      String val = macros.get(key);
+      if (m.group(1) != null) {
+        m.appendReplacement(sb, "@" + key + "@");
+      } else if (val != null) {
+        m.appendReplacement(sb, val);
+      } else {
+        m.appendReplacement(sb, "@" + key + "@");
+      }
+    }
+    m.appendTail(sb);
+
+    byte[] html = new MarkdownFormatter()
+      .markdownToDocHtml(sb.toString(), "UTF-8");
+    resourceCache.put(cacheKey, new SmallResource(html)
+        .setContentType("text/html")
+        .setCharacterEncoding("UTF-8"));
+    res.setContentType("text/html");
+    res.setCharacterEncoding("UTF-8");
+    res.setContentLength(html.length);
+    res.getOutputStream().write(html);
+  }
+
+  private static void appendPluginInfoTable(StringBuilder html, Attributes main) {
+    if (main != null) {
+      String t = main.getValue(Attributes.Name.IMPLEMENTATION_TITLE);
+      String n = main.getValue(Attributes.Name.IMPLEMENTATION_VENDOR);
+      String v = main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
+      String u = main.getValue(Attributes.Name.IMPLEMENTATION_URL);
+      String a = main.getValue("Gerrit-ApiVersion");
+
+      html.append("<table class=\"plugin_info\">");
+      if (!Strings.isNullOrEmpty(t)) {
+        html.append("<tr><th>Name</th><td>")
+            .append(t)
+            .append("</td></tr>\n");
+      }
+      if (!Strings.isNullOrEmpty(n)) {
+        html.append("<tr><th>Vendor</th><td>")
+            .append(n)
+            .append("</td></tr>\n");
+      }
+      if (!Strings.isNullOrEmpty(v)) {
+        html.append("<tr><th>Version</th><td>")
+            .append(v)
+            .append("</td></tr>\n");
+      }
+      if (!Strings.isNullOrEmpty(u)) {
+        html.append("<tr><th>URL</th><td>")
+            .append(String.format("<a href=\"%s\">%s</a>", u, u))
+            .append("</td></tr>\n");
+      }
+      if (!Strings.isNullOrEmpty(a)) {
+        html.append("<tr><th>API Version</th><td>")
+            .append(a)
+            .append("</td></tr>\n");
+      }
+      html.append("</table>\n");
+    }
+  }
+
+  private static String extractTitleFromMarkdown(JarFile jar, JarEntry entry)
+        throws IOException {
+    String charEnc = null;
+    Attributes atts = entry.getAttributes();
+    if (atts != null) {
+      charEnc = Strings.emptyToNull(atts.getValue("Character-Encoding"));
+    }
+    if (charEnc == null) {
+      charEnc = "UTF-8";
+    }
+    return new MarkdownFormatter().extractTitleFromMarkdown(
+          readWholeEntry(jar, entry),
+          charEnc);
+  }
+
+  private static JarEntry findSource(JarFile jar, String file) {
+    if (file.endsWith(".html")) {
+      int d = file.lastIndexOf('.');
+      return jar.getJarEntry(file.substring(0, d) + ".md");
+    }
+    return null;
+  }
+
+  private static boolean exists(JarEntry entry) {
+    return entry != null && entry.getSize() > 0;
+  }
+
+  private void sendMarkdownAsHtml(JarFile jar, JarEntry entry,
+      String pluginName, ResourceKey key, HttpServletResponse res)
+      throws IOException {
+    byte[] rawmd = readWholeEntry(jar, entry);
+    String encoding = null;
+    Attributes atts = entry.getAttributes();
+    if (atts != null) {
+      encoding = Strings.emptyToNull(atts.getValue("Character-Encoding"));
+    }
+
+    String txtmd = RawParseUtils.decode(
+        Charset.forName(encoding != null ? encoding : "UTF-8"),
+        rawmd);
+    long time = entry.getTime();
+    if (0 < time) {
+      res.setDateHeader("Last-Modified", time);
+    }
+    sendMarkdownAsHtml(txtmd, pluginName, key, res);
+  }
+
+  private void sendResource(JarFile jar, JarEntry entry,
+      ResourceKey key, HttpServletResponse res)
       throws IOException {
     byte[] data = null;
-    if (entry.getSize() <= 128 * 1024) {
-      data = new byte[(int) entry.getSize()];
-      InputStream in = jar.getInputStream(entry);
-      try {
-        IO.readFully(in, data, 0, data.length);
-      } finally {
-        in.close();
-      }
+    if (entry.getSize() <= SMALL_RESOURCE) {
+      data = readWholeEntry(jar, entry);
     }
 
     String contentType = null;
+    String charEnc = null;
     Attributes atts = entry.getAttributes();
     if (atts != null) {
       contentType = Strings.emptyToNull(atts.getValue("Content-Type"));
+      charEnc = Strings.emptyToNull(atts.getValue("Character-Encoding"));
     }
     if (contentType == null) {
-      MimeType type = mimeUtil.getMimeType(entry.getName(), data);
-      contentType = type.toString();
+      contentType = mimeUtil.getMimeType(entry.getName(), data).toString();
     }
 
     long time = entry.getTime();
     if (0 < time) {
       res.setDateHeader("Last-Modified", time);
     }
-    res.setContentType(contentType);
     res.setHeader("Content-Length", Long.toString(entry.getSize()));
+    res.setContentType(contentType);
+    if (charEnc != null) {
+      res.setCharacterEncoding(charEnc);
+    }
     if (data != null) {
+      resourceCache.put(key, new SmallResource(data)
+          .setContentType(contentType)
+          .setCharacterEncoding(charEnc)
+          .setLastModified(time));
       res.getOutputStream().write(data);
     } else {
       InputStream in = jar.getInputStream(entry);
@@ -233,6 +530,18 @@
     }
   }
 
+  private static byte[] readWholeEntry(JarFile jar, JarEntry entry)
+      throws IOException {
+    byte[] data = new byte[(int) entry.getSize()];
+    InputStream in = jar.getInputStream(entry);
+    try {
+      IO.readFully(in, data, 0, data.length);
+    } finally {
+      in.close();
+    }
+    return data;
+  }
+
   private static String extractName(HttpServletRequest req) {
     String path = req.getPathInfo();
     if (Strings.isNullOrEmpty(path) || "/".equals(path)) {
@@ -242,7 +551,7 @@
     return 0 <= s ? path.substring(1, s) : path.substring(1);
   }
 
-  private static void noCache(HttpServletResponse res) {
+  static void noCache(HttpServletResponse res) {
     res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
     res.setHeader("Pragma", "no-cache");
     res.setHeader("Cache-Control", "no-cache, must-revalidate");
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/plugins/Resource.java
new file mode 100644
index 0000000..05970af
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/Resource.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.plugins;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+abstract class Resource {
+  static final Resource NOT_FOUND = new Resource() {
+    @Override
+    int weigh() {
+      return 0;
+    }
+
+    @Override
+    void send(HttpServletRequest req, HttpServletResponse res)
+        throws IOException {
+      HttpPluginServlet.noCache(res);
+      res.sendError(HttpServletResponse.SC_NOT_FOUND);
+    }
+  };
+
+  abstract int weigh();
+  abstract void send(HttpServletRequest req, HttpServletResponse res)
+      throws IOException;
+}
\ No newline at end of file
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceKey.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceKey.java
new file mode 100644
index 0000000..068d6b4
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceKey.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.plugins;
+
+import com.google.gerrit.server.plugins.Plugin;
+
+final class ResourceKey {
+  private final Plugin.CacheKey plugin;
+  private final String resource;
+
+  ResourceKey(Plugin p, String r) {
+    this.plugin = p.getCacheKey();
+    this.resource = r;
+  }
+
+  int weigh() {
+    return resource.length() * 2;
+  }
+
+  @Override
+  public int hashCode() {
+    return plugin.hashCode() * 31 + resource.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other instanceof ResourceKey) {
+      ResourceKey rk = (ResourceKey) other;
+      return plugin == rk.plugin && resource.equals(rk.resource);
+    }
+    return false;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.java
similarity index 65%
copy from gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
copy to gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.java
index 204d777..2514272 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.java
@@ -12,12 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.httpd.plugins;
 
-public class IncompleteUserInfoException extends Exception {
-  private static final long serialVersionUID = 1L;
+import com.google.common.cache.Weigher;
 
-  public IncompleteUserInfoException(final String userName, final String missingInfo) {
-    super("For the user \"" + userName + "\" " + missingInfo + " is not set.");
+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/plugins/SmallResource.java
new file mode 100644
index 0000000..e408f72
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.plugins;
+
+import java.io.IOException;
+
+import javax.annotation.Nullable;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+final class SmallResource extends Resource {
+  private final byte[] data;
+  private String contentType;
+  private String characterEncoding;
+  private long lastModified;
+
+  SmallResource(byte[] data) {
+    this.data = data;
+  }
+
+  SmallResource setLastModified(long when) {
+    this.lastModified = when;
+    return this;
+  }
+
+  SmallResource setContentType(String contentType) {
+    this.contentType = contentType;
+    return this;
+  }
+
+  SmallResource setCharacterEncoding(@Nullable String enc) {
+    this.characterEncoding = enc;
+    return this;
+  }
+
+  @Override
+  int weigh() {
+    return contentType.length() * 2 + data.length;
+  }
+
+  @Override
+  void send(HttpServletRequest req, HttpServletResponse res)
+      throws IOException {
+    if (0 < lastModified) {
+      res.setDateHeader("Last-Modified", lastModified);
+    }
+    res.setContentType(contentType);
+    if (characterEncoding != null) {
+     res.setCharacterEncoding(characterEncoding);
+    }
+    res.setContentLength(data.length);
+    res.getOutputStream().write(data);
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
index 26db6f9..62506f0 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.inject.Provider;
 
 /** Support for services which require a {@link ReviewDb} instance. */
@@ -70,20 +71,14 @@
       callback.onFailure(new NoSuchEntityException());
     } catch (NoSuchGroupException e) {
       callback.onFailure(new NoSuchEntityException());
-
-    } catch (OrmException e) {
-      if (e.getCause() instanceof Failure) {
-        callback.onFailure(e.getCause().getCause());
-
-      } else if (e.getCause() instanceof CorruptEntityException) {
-        callback.onFailure(e.getCause());
-
-      } else if (e.getCause() instanceof NoSuchEntityException) {
-        callback.onFailure(e.getCause());
-
-      } else {
-        callback.onFailure(e);
+    } catch (OrmRuntimeException e) {
+      Exception ex = e;
+      if (e.getCause() instanceof OrmException) {
+        ex = (OrmException) e.getCause();
       }
+      handleOrmException(callback, ex);
+    } catch (OrmException e) {
+      handleOrmException(callback, e);
     } catch (Failure e) {
       if (e.getCause() instanceof NoSuchProjectException
           || e.getCause() instanceof NoSuchChangeException) {
@@ -95,6 +90,19 @@
     }
   }
 
+  private static <T> void handleOrmException(
+      final AsyncCallback<T> callback, Exception e) {
+    if (e.getCause() instanceof Failure) {
+      callback.onFailure(e.getCause().getCause());
+    } else if (e.getCause() instanceof CorruptEntityException) {
+      callback.onFailure(e.getCause());
+    } else if (e.getCause() instanceof NoSuchEntityException) {
+      callback.onFailure(e.getCause());
+    } else {
+      callback.onFailure(e);
+    }
+  }
+
   /** Exception whose cause is passed into onFailure. */
   public static class Failure extends Exception {
     private static final long serialVersionUID = 1L;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
index 66b2dc0..0b54db1 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
@@ -14,258 +14,32 @@
 
 package com.google.gerrit.httpd.rpc;
 
-import com.google.gerrit.common.data.AccountDashboardInfo;
-import com.google.gerrit.common.data.ChangeInfo;
 import com.google.gerrit.common.data.ChangeListService;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.SingleListChangeInfo;
 import com.google.gerrit.common.data.ToggleStarRequest;
-import com.google.gerrit.common.errors.InvalidQueryException;
-import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.StarredChange;
-import com.google.gerrit.reviewdb.server.ChangeAccess;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.AccountInfoCacheFactory;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryParseException;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeDataSource;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gerrit.server.query.change.ChangeQueryRewriter;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.VoidResult;
-import com.google.gwtorm.server.ListResultSet;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
 public class ChangeListServiceImpl extends BaseServiceImplementation implements
     ChangeListService {
-  private static final Comparator<ChangeInfo> ID_COMP =
-      new Comparator<ChangeInfo>() {
-        public int compare(final ChangeInfo o1, final ChangeInfo o2) {
-          return o1.getId().get() - o2.getId().get();
-        }
-      };
-  private static final Comparator<ChangeInfo> SORT_KEY_COMP =
-      new Comparator<ChangeInfo>() {
-        public int compare(final ChangeInfo o1, final ChangeInfo o2) {
-          return o2.getSortKey().compareTo(o1.getSortKey());
-        }
-      };
-  private static final Comparator<Change> QUERY_PREV =
-      new Comparator<Change>() {
-        public int compare(final Change a, final Change b) {
-          return a.getSortKey().compareTo(b.getSortKey());
-        }
-      };
-  private static final Comparator<Change> QUERY_NEXT =
-      new Comparator<Change>() {
-        public int compare(final Change a, final Change b) {
-          return b.getSortKey().compareTo(a.getSortKey());
-        }
-      };
-
   private final Provider<CurrentUser> currentUser;
-  private final ChangeControl.Factory changeControlFactory;
-  private final AccountInfoCacheFactory.Factory accountInfoCacheFactory;
-
-  private final ChangeQueryBuilder.Factory queryBuilder;
-  private final Provider<ChangeQueryRewriter> queryRewriter;
 
   @Inject
   ChangeListServiceImpl(final Provider<ReviewDb> schema,
-      final Provider<CurrentUser> currentUser,
-      final ChangeControl.Factory changeControlFactory,
-      final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
-      final ChangeQueryBuilder.Factory queryBuilder,
-      final Provider<ChangeQueryRewriter> queryRewriter) {
+      final Provider<CurrentUser> currentUser) {
     super(schema, currentUser);
     this.currentUser = currentUser;
-    this.changeControlFactory = changeControlFactory;
-    this.accountInfoCacheFactory = accountInfoCacheFactory;
-    this.queryBuilder = queryBuilder;
-    this.queryRewriter = queryRewriter;
-  }
-
-  private boolean canRead(final Change c, final ReviewDb db) throws OrmException {
-    try {
-      return changeControlFactory.controlFor(c).isVisible(db);
-    } catch (NoSuchChangeException e) {
-      return false;
-    }
-  }
-
-  @Override
-  public void allQueryPrev(final String query, final String pos,
-      final int pageSize, final AsyncCallback<SingleListChangeInfo> callback) {
-    try {
-      run(callback, new QueryPrev(pageSize, pos) {
-        @Override
-        ResultSet<Change> query(ReviewDb db, int lim, String key)
-            throws OrmException, InvalidQueryException {
-          return searchQuery(db, query, lim, key, QUERY_PREV);
-        }
-      });
-    } catch (InvalidQueryException e) {
-      callback.onFailure(e);
-    }
-  }
-
-  @Override
-  public void allQueryNext(final String query, final String pos,
-      final int pageSize, final AsyncCallback<SingleListChangeInfo> callback) {
-    try {
-      run(callback, new QueryNext(pageSize, pos) {
-        @Override
-        ResultSet<Change> query(ReviewDb db, int lim, String key)
-            throws OrmException, InvalidQueryException {
-          return searchQuery(db, query, lim, key, QUERY_NEXT);
-        }
-      });
-    } catch (InvalidQueryException e) {
-      callback.onFailure(e);
-    }
-  }
-
-  @SuppressWarnings("unchecked")
-  private ResultSet<Change> searchQuery(final ReviewDb db, String query,
-      final int limit, final String key, final Comparator<Change> cmp)
-      throws OrmException, InvalidQueryException {
-    try {
-      final ChangeQueryBuilder builder = queryBuilder.create(currentUser.get());
-      final Predicate<ChangeData> visibleToMe = builder.is_visible();
-      Predicate<ChangeData> q = builder.parse(query);
-      q = Predicate.and(q, //
-          cmp == QUERY_PREV //
-              ? builder.sortkey_after(key) //
-              : builder.sortkey_before(key), //
-          builder.limit(limit), //
-          visibleToMe //
-          );
-
-      ChangeQueryRewriter rewriter = queryRewriter.get();
-      Predicate<ChangeData> s = rewriter.rewrite(q);
-      if (!(s instanceof ChangeDataSource)) {
-        s = rewriter.rewrite(Predicate.and(builder.status_open(), q));
-      }
-
-      if (s instanceof ChangeDataSource) {
-        ArrayList<Change> r = new ArrayList<Change>();
-        HashSet<Change.Id> want = new HashSet<Change.Id>();
-        for (ChangeData d : ((ChangeDataSource) s).read()) {
-          if (d.hasChange()) {
-            // Checking visibleToMe here should be unnecessary, the
-            // query should have already performed it.  But we don't
-            // want to trust the query rewriter that much yet.
-            //
-            if (visibleToMe.match(d)) {
-              r.add(d.getChange());
-            }
-          } else {
-            want.add(d.getId());
-          }
-        }
-
-        // Here we have to check canRead. Its impossible to
-        // do that test without the change object, and it being
-        // missing above means we have to compute it ourselves.
-        //
-        if (!want.isEmpty()) {
-          for (Change c : db.changes().get(want)) {
-            if (canRead(c, db)) {
-              r.add(c);
-            }
-          }
-        }
-
-        Collections.sort(r, cmp);
-        return new ListResultSet<Change>(r);
-      } else {
-        throw new InvalidQueryException("Not Supported", s.toString());
-      }
-    } catch (QueryParseException e) {
-      throw new InvalidQueryException(e.getMessage(), query);
-    }
-  }
-
-  public void forAccount(final Account.Id id,
-      final AsyncCallback<AccountDashboardInfo> callback) {
-    final Account.Id me = getAccountId();
-    final Account.Id target = id != null ? id : me;
-    if (target == null) {
-      callback.onFailure(new NoSuchEntityException());
-      return;
-    }
-
-    run(callback, new Action<AccountDashboardInfo>() {
-      public AccountDashboardInfo run(final ReviewDb db) throws OrmException,
-          Failure {
-        final AccountInfoCacheFactory ac = accountInfoCacheFactory.create();
-        final Account user = ac.get(target);
-        if (user == null) {
-          throw new Failure(new NoSuchEntityException());
-        }
-
-        final Set<Change.Id> stars = currentUser.get().getStarredChanges();
-        final ChangeAccess changes = db.changes();
-        final AccountDashboardInfo d;
-
-        final Set<Change.Id> openReviews = new HashSet<Change.Id>();
-        final Set<Change.Id> closedReviews = new HashSet<Change.Id>();
-        for (final PatchSetApproval ca : db.patchSetApprovals().openByUser(id)) {
-          openReviews.add(ca.getPatchSetId().getParentKey());
-        }
-        for (final PatchSetApproval ca : db.patchSetApprovals()
-            .closedByUser(id)) {
-          closedReviews.add(ca.getPatchSetId().getParentKey());
-        }
-
-        d = new AccountDashboardInfo(target);
-        d.setByOwner(filter(changes.byOwnerOpen(target), stars, ac, db));
-        d.setClosed(filter(changes.byOwnerClosed(target), stars, ac, db));
-
-        for (final ChangeInfo c : d.getByOwner()) {
-          openReviews.remove(c.getId());
-        }
-        d.setForReview(filter(changes.get(openReviews), stars, ac, db));
-        Collections.sort(d.getForReview(), ID_COMP);
-
-        for (final ChangeInfo c : d.getClosed()) {
-          closedReviews.remove(c.getId());
-        }
-        if (!closedReviews.isEmpty()) {
-          d.getClosed().addAll(filter(changes.get(closedReviews), stars, ac, db));
-          Collections.sort(d.getClosed(), SORT_KEY_COMP);
-        }
-
-        // User dashboards are visible to other users, if the current user
-        // can see any of the changes in the dashboard.
-        if (!target.equals(me)
-            && d.getByOwner().isEmpty()
-            && d.getClosed().isEmpty()
-            && d.getForReview().isEmpty()) {
-          throw new Failure(new NoSuchEntityException());
-        }
-
-        d.setAccounts(ac.create());
-        return d;
-      }
-    });
   }
 
   public void toggleStars(final ToggleStarRequest req,
@@ -297,97 +71,4 @@
       }
     });
   }
-
-  public void myStarredChangeIds(final AsyncCallback<Set<Change.Id>> callback) {
-    callback.onSuccess(currentUser.get().getStarredChanges());
-  }
-
-  private int safePageSize(final int pageSize) throws InvalidQueryException {
-    int maxLimit = currentUser.get().getCapabilities()
-      .getRange(GlobalCapability.QUERY_LIMIT)
-      .getMax();
-    if (maxLimit <= 0) {
-      throw new InvalidQueryException("Search Disabled");
-    }
-    return 0 < pageSize && pageSize <= maxLimit ? pageSize : maxLimit;
-  }
-
-  private List<ChangeInfo> filter(final ResultSet<Change> rs,
-      final Set<Change.Id> starred, final AccountInfoCacheFactory accts,
-      final ReviewDb db) throws OrmException {
-    final ArrayList<ChangeInfo> r = new ArrayList<ChangeInfo>();
-    for (final Change c : rs) {
-      if (canRead(c, db)) {
-        final ChangeInfo ci = new ChangeInfo(c);
-        accts.want(ci.getOwner());
-        ci.setStarred(starred.contains(ci.getId()));
-        r.add(ci);
-      }
-    }
-    return r;
-  }
-
-  private abstract class QueryNext implements Action<SingleListChangeInfo> {
-    protected final String pos;
-    protected final int limit;
-    protected final int slim;
-
-    QueryNext(final int pageSize, final String pos) throws InvalidQueryException {
-      this.pos = pos;
-      this.limit = safePageSize(pageSize);
-      this.slim = limit + 1;
-    }
-
-    public SingleListChangeInfo run(final ReviewDb db) throws OrmException,
-        InvalidQueryException {
-      final AccountInfoCacheFactory ac = accountInfoCacheFactory.create();
-      final SingleListChangeInfo d = new SingleListChangeInfo();
-      final Set<Change.Id> starred = currentUser.get().getStarredChanges();
-
-      final ArrayList<ChangeInfo> list = new ArrayList<ChangeInfo>();
-      final ResultSet<Change> rs = query(db, slim, pos);
-      for (final Change c : rs) {
-        if (!canRead(c, db)) {
-          continue;
-        }
-        final ChangeInfo ci = new ChangeInfo(c);
-        ac.want(ci.getOwner());
-        ci.setStarred(starred.contains(ci.getId()));
-        list.add(ci);
-        if (list.size() == slim) {
-          rs.close();
-          break;
-        }
-      }
-
-      final boolean atEnd = finish(list);
-      d.setChanges(list, atEnd);
-      d.setAccounts(ac.create());
-      return d;
-    }
-
-    boolean finish(final ArrayList<ChangeInfo> list) {
-      final boolean atEnd = list.size() <= limit;
-      if (list.size() == slim) {
-        list.remove(limit);
-      }
-      return atEnd;
-    }
-
-    abstract ResultSet<Change> query(final ReviewDb db, final int slim,
-        String sortKey) throws OrmException, InvalidQueryException;
-  }
-
-  private abstract class QueryPrev extends QueryNext {
-    QueryPrev(int pageSize, String pos) throws InvalidQueryException {
-      super(pageSize, pos);
-    }
-
-    @Override
-    boolean finish(final ArrayList<ChangeInfo> list) {
-      final boolean atEnd = super.finish(list);
-      Collections.reverse(list);
-      return atEnd;
-    }
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
index d687463..1baa49b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.httpd.rpc;
 
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.ReviewerInfo;
@@ -21,8 +23,6 @@
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupName;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -31,14 +31,14 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountVisibility;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembers;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.patch.AddReviewer;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectControl;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -53,41 +53,43 @@
 import java.util.Map;
 import java.util.Set;
 
+import javax.annotation.Nullable;
+
 class SuggestServiceImpl extends BaseServiceImplementation implements
     SuggestService {
   private static final String MAX_SUFFIX = "\u9fa5";
 
   private final Provider<ReviewDb> reviewDbProvider;
   private final AccountCache accountCache;
-  private final GroupControl.Factory groupControlFactory;
   private final GroupMembers.Factory groupMembersFactory;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
   private final AccountControl.Factory accountControlFactory;
   private final ChangeControl.Factory changeControlFactory;
+  private final ProjectControl.Factory projectControlFactory;
   private final Config cfg;
-  private final GroupCache groupCache;
+  private final GroupBackend groupBackend;
   private final boolean suggestAccounts;
 
   @Inject
   SuggestServiceImpl(final Provider<ReviewDb> schema,
       final AccountCache accountCache,
-      final GroupControl.Factory groupControlFactory,
       final GroupMembers.Factory groupMembersFactory,
       final Provider<CurrentUser> currentUser,
       final IdentifiedUser.GenericFactory identifiedUserFactory,
       final AccountControl.Factory accountControlFactory,
       final ChangeControl.Factory changeControlFactory,
-      @GerritServerConfig final Config cfg, final GroupCache groupCache) {
+      final ProjectControl.Factory projectControlFactory,
+      @GerritServerConfig final Config cfg, final GroupBackend groupBackend) {
     super(schema, currentUser);
     this.reviewDbProvider = schema;
     this.accountCache = accountCache;
-    this.groupControlFactory = groupControlFactory;
     this.groupMembersFactory = groupMembersFactory;
     this.identifiedUserFactory = identifiedUserFactory;
     this.accountControlFactory = accountControlFactory;
     this.changeControlFactory = changeControlFactory;
+    this.projectControlFactory = projectControlFactory;
     this.cfg = cfg;
-    this.groupCache = groupCache;
+    this.groupBackend = groupBackend;
 
     if ("OFF".equals(cfg.getString("suggest", null, "accounts"))) {
       this.suggestAccounts = false;
@@ -176,33 +178,31 @@
 
   public void suggestAccountGroup(final String query, final int limit,
       final AsyncCallback<List<GroupReference>> callback) {
+    suggestAccountGroupForProject(null, query, limit, callback);
+  }
+
+  public void suggestAccountGroupForProject(final Project.NameKey project,
+      final String query, final int limit,
+      final AsyncCallback<List<GroupReference>> callback) {
     run(callback, new Action<List<GroupReference>>() {
-      public List<GroupReference> run(final ReviewDb db) throws OrmException {
-        return suggestAccountGroup(db, query, limit);
+      public List<GroupReference> run(final ReviewDb db) {
+        ProjectControl projectControl = null;
+        if (project != null) {
+          try {
+            projectControl = projectControlFactory.controlFor(project);
+          } catch (NoSuchProjectException e) {
+            return Collections.emptyList();
+          }
+        }
+        return suggestAccountGroup(projectControl, query, limit);
       }
     });
   }
 
-  private List<GroupReference> suggestAccountGroup(final ReviewDb db,
-      final String query, final int limit) throws OrmException {
-    final String a = query;
-    final String b = a + MAX_SUFFIX;
-    final int max = 10;
-    final int n = limit <= 0 ? max : Math.min(limit, max);
-    List<GroupReference> r = new ArrayList<GroupReference>(n);
-    for (AccountGroupName group : db.accountGroupNames().suggestByName(a, b, n)) {
-      try {
-        if (groupControlFactory.controlFor(group.getId()).isVisible()) {
-          AccountGroup g = groupCache.get(group.getId());
-          if (g != null && g.getGroupUUID() != null) {
-            r.add(GroupReference.forGroup(g));
-          }
-        }
-      } catch (NoSuchGroupException e) {
-        continue;
-      }
-    }
-    return r;
+  private List<GroupReference> suggestAccountGroup(
+      @Nullable final ProjectControl projectControl, final String query, final int limit) {
+    final int n = limit <= 0 ? 10 : Math.min(limit, 10);
+    return Lists.newArrayList(Iterables.limit(groupBackend.suggest(query), n));
   }
 
   @Override
@@ -229,7 +229,9 @@
           public boolean isVisible(Account account) throws OrmException {
             IdentifiedUser who =
                 identifiedUserFactory.create(reviewDbProvider, account.getId());
-            return changeControl.forUser(who).isVisible(reviewDbProvider.get());
+            // we can't use changeControl directly as it won't suggest reviewers
+            // to drafts
+            return changeControl.forUser(who).isRefVisible();
           }
         };
 
@@ -241,7 +243,7 @@
           reviewer.add(new ReviewerInfo(a));
         }
         final List<GroupReference> suggestedAccountGroups =
-            suggestAccountGroup(db, query, limit);
+            suggestAccountGroup(changeControl.getProjectControl(), query, limit);
         for (final GroupReference g : suggestedAccountGroups) {
           if (suggestGroupAsReviewer(changeControl.getProject().getNameKey(), g)) {
             reviewer.add(new ReviewerInfo(g));
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java
index 0d0ffe7..a33c209 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java
@@ -58,8 +58,9 @@
   private final Provider<Impl> factory;
 
   @Inject
-  AccountCapabilitiesServlet(
+  AccountCapabilitiesServlet(final Provider<CurrentUser> currentUser,
       ParameterParser paramParser, Provider<Impl> factory) {
+    super(currentUser);
     this.paramParser = paramParser;
     this.factory = factory;
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
index e90c2bf..c7b4c79 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.common.data.GroupDetail;
 import com.google.gerrit.common.data.GroupList;
 import com.google.gerrit.common.data.GroupOptions;
+import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.InactiveAccountException;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.common.errors.NoSuchAccountException;
@@ -34,29 +35,27 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.account.GroupIncludeCache;
-import com.google.gerrit.server.account.Realm;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.VoidResult;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Set;
 
 class GroupAdminServiceImpl extends BaseServiceImplementation implements
     GroupAdminService {
   private final AccountCache accountCache;
   private final AccountResolver accountResolver;
-  private final Realm accountRealm;
   private final GroupCache groupCache;
+  private final GroupBackend groupBackend;
   private final GroupIncludeCache groupIncludeCache;
   private final GroupControl.Factory groupControlFactory;
 
@@ -70,8 +69,9 @@
       final Provider<IdentifiedUser> currentUser,
       final AccountCache accountCache,
       final GroupIncludeCache groupIncludeCache,
-      final AccountResolver accountResolver, final Realm accountRealm,
+      final AccountResolver accountResolver,
       final GroupCache groupCache,
+      final GroupBackend groupBackend,
       final GroupControl.Factory groupControlFactory,
       final CreateGroup.Factory createGroupFactory,
       final RenameGroup.Factory renameGroupFactory,
@@ -81,8 +81,8 @@
     this.accountCache = accountCache;
     this.groupIncludeCache = groupIncludeCache;
     this.accountResolver = accountResolver;
-    this.accountRealm = accountRealm;
     this.groupCache = groupCache;
+    this.groupBackend = groupBackend;
     this.groupControlFactory = groupControlFactory;
     this.createGroupFactory = createGroupFactory;
     this.renameGroupFactory = renameGroupFactory;
@@ -145,13 +145,13 @@
         final AccountGroup group = db.accountGroups().get(groupId);
         assertAmGroupOwner(db, group);
 
-        final AccountGroup owner =
-            groupCache.get(new AccountGroup.NameKey(newOwnerName));
+        GroupReference owner =
+            GroupBackends.findExactSuggestion(groupBackend, newOwnerName);
         if (owner == null) {
           throw new Failure(new NoSuchEntityException());
         }
 
-        group.setOwnerGroupId(owner.getId());
+        group.setOwnerGroupUUID(owner.getUUID());
         db.accountGroups().update(Collections.singleton(group));
         groupCache.evict(group);
         return VoidResult.INSTANCE;
@@ -178,43 +178,13 @@
     });
   }
 
-  public void changeExternalGroup(final AccountGroup.Id groupId,
-      final AccountGroup.ExternalNameKey bindTo,
-      final AsyncCallback<VoidResult> callback) {
-    run(callback, new Action<VoidResult>() {
-      public VoidResult run(final ReviewDb db) throws OrmException, Failure {
-        final AccountGroup group = db.accountGroups().get(groupId);
-        assertAmGroupOwner(db, group);
-        group.setExternalNameKey(bindTo);
-        db.accountGroups().update(Collections.singleton(group));
-        groupCache.evict(group);
-        return VoidResult.INSTANCE;
-      }
-    });
-  }
-
-  public void searchExternalGroups(final String searchFilter,
-      final AsyncCallback<List<AccountGroup.ExternalNameKey>> callback) {
-    final ArrayList<AccountGroup.ExternalNameKey> matches =
-        new ArrayList<AccountGroup.ExternalNameKey>(
-            accountRealm.lookupGroups(searchFilter));
-    Collections.sort(matches, new Comparator<AccountGroup.ExternalNameKey>() {
-      @Override
-      public int compare(AccountGroup.ExternalNameKey a,
-          AccountGroup.ExternalNameKey b) {
-        return a.get().compareTo(b.get());
-      }
-    });
-    callback.onSuccess(matches);
-  }
-
   public void addGroupMember(final AccountGroup.Id groupId,
       final String nameOrEmail, final AsyncCallback<GroupDetail> callback) {
     run(callback, new Action<GroupDetail>() {
       public GroupDetail run(ReviewDb db) throws OrmException, Failure,
           NoSuchGroupException {
         final GroupControl control = groupControlFactory.validateFor(groupId);
-        if (control.getAccountGroup().getType() != AccountGroup.Type.INTERNAL) {
+        if (groupCache.get(groupId).getType() != AccountGroup.Type.INTERNAL) {
           throw new Failure(new NameAlreadyUsedException());
         }
 
@@ -249,7 +219,7 @@
       public GroupDetail run(ReviewDb db) throws OrmException, Failure,
           NoSuchGroupException {
         final GroupControl control = groupControlFactory.validateFor(groupId);
-        if (control.getAccountGroup().getType() != AccountGroup.Type.INTERNAL) {
+        if (groupCache.get(groupId).getType() != AccountGroup.Type.INTERNAL) {
           throw new Failure(new NameAlreadyUsedException());
         }
 
@@ -282,7 +252,7 @@
       public VoidResult run(final ReviewDb db) throws OrmException,
           NoSuchGroupException, Failure {
         final GroupControl control = groupControlFactory.validateFor(groupId);
-        if (control.getAccountGroup().getType() != AccountGroup.Type.INTERNAL) {
+        if (groupCache.get(groupId).getType() != AccountGroup.Type.INTERNAL) {
           throw new Failure(new NameAlreadyUsedException());
         }
 
@@ -336,7 +306,7 @@
       public VoidResult run(final ReviewDb db) throws OrmException,
           NoSuchGroupException, Failure {
         final GroupControl control = groupControlFactory.validateFor(groupId);
-        if (control.getAccountGroup().getType() != AccountGroup.Type.INTERNAL) {
+        if (groupCache.get(groupId).getType() != AccountGroup.Type.INTERNAL) {
           throw new Failure(new NameAlreadyUsedException());
         }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ListChangesServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ListChangesServlet.java
index 91fc5b0..b501d43 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ListChangesServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ListChangesServlet.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.rpc.change;
 
 import com.google.gerrit.httpd.RestApiServlet;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ListChanges;
@@ -43,7 +44,9 @@
   private final Provider<ListChanges> factory;
 
   @Inject
-  ListChangesServlet(ParameterParser paramParser, Provider<ListChanges> ls) {
+  ListChangesServlet(final Provider<CurrentUser> currentUser,
+      ParameterParser paramParser, Provider<ListChanges> ls) {
+    super(currentUser);
     this.paramParser = paramParser;
     this.factory = ls;
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java
index 1d4d3e2..1713422 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.server.changedetail.AbandonChange;
 import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
-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;
@@ -61,9 +60,8 @@
 
   @Override
   public ChangeDetail call() throws NoSuchChangeException, OrmException,
-      EmailException, NoSuchEntityException, InvalidChangeOperationException,
-      PatchSetInfoNotAvailableException, RepositoryNotFoundException,
-      IOException {
+      EmailException, NoSuchEntityException, PatchSetInfoNotAvailableException,
+      RepositoryNotFoundException, IOException {
     final ReviewResult result =
         abandonChangeFactory.create(patchSetId.getParentKey(), message).call();
     if (result.getErrors().size() > 0) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
index d765f39..6b5299a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
@@ -143,20 +143,20 @@
 
     detail.setCanEdit(control.getRefControl().canWrite());
 
-    if (detail.getChange().getStatus().isOpen()) {
-      List<SubmitRecord> submitRecords = control.canSubmit(db, patch);
-      for (SubmitRecord rec : submitRecords) {
-        if (rec.labels != null) {
-          for (SubmitRecord.Label lbl : rec.labels) {
-            aic.want(lbl.appliedBy);
-          }
-        }
-        if (rec.status == SubmitRecord.Status.OK && control.getRefControl().canSubmit()) {
-          detail.setCanSubmit(true);
+    List<SubmitRecord> submitRecords = control.getSubmitRecords(db, patch);
+    for (SubmitRecord rec : submitRecords) {
+      if (rec.labels != null) {
+        for (SubmitRecord.Label lbl : rec.labels) {
+          aic.want(lbl.appliedBy);
         }
       }
-      detail.setSubmitRecords(submitRecords);
+      if (detail.getChange().getStatus().isOpen()
+          && rec.status == SubmitRecord.Status.OK
+          && control.getRefControl().canSubmit()) {
+        detail.setCanSubmit(true);
+      }
     }
+    detail.setSubmitRecords(submitRecords);
 
     patchsetsById = new HashMap<PatchSet.Id, PatchSet>();
     loadPatchSets();
@@ -274,13 +274,16 @@
       }
     }
 
-    final RevId cprev = loader.patchSet.getRevision();
     final Set<Change.Id> descendants = new HashSet<Change.Id>();
-    if (cprev != null) {
-      for (PatchSetAncestor a : db.patchSetAncestors().descendantsOf(cprev)) {
-        final Change.Id ck = a.getPatchSet().getParentKey();
-        if (descendants.add(ck)) {
-          changesToGet.add(a.getPatchSet().getParentKey());
+    RevId cprev;
+    for (PatchSet p : detail.getPatchSets()) {
+      cprev = p.getRevision();
+      if (cprev != null) {
+        for (PatchSetAncestor a : db.patchSetAncestors().descendantsOf(cprev)) {
+          final Change.Id ck = a.getPatchSet().getParentKey();
+          if (descendants.add(ck)) {
+            changesToGet.add(ck);
+          }
         }
       }
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
index 66c11c0..ecd8b54 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
@@ -19,8 +19,8 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtjsonrpc.common.VoidResult;
@@ -38,7 +38,7 @@
   private final ChangeControl.Factory changeControlFactory;
   private final ReviewDb db;
   private final GitRepositoryManager gitManager;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated replication;
 
   private final PatchSet.Id patchSetId;
 
@@ -46,7 +46,7 @@
   DeleteDraftChange(final ReviewDb db,
       final ChangeControl.Factory changeControlFactory,
       final GitRepositoryManager gitManager,
-      final ReplicationQueue replication,
+      final GitReferenceUpdated replication,
       @Assisted final PatchSet.Id patchSetId) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
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 de3ff2f..95a8e26 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
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
@@ -108,18 +109,19 @@
 
     final PatchList list;
 
-    if (psIdBase != null) {
-      oldId = toObjectId(psIdBase);
-      newId = toObjectId(psIdNew);
+    try {
+      if (psIdBase != null) {
+        oldId = toObjectId(psIdBase);
+        newId = toObjectId(psIdNew);
 
-      projectKey = control.getProject().getNameKey();
+        projectKey = control.getProject().getNameKey();
 
-      list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
-    } else { // OK, means use base to compare
-      list = patchListCache.get(control.getChange(), patchSet);
-      if (list == null) {
-        throw new NoSuchEntityException();
+        list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
+      } else { // OK, means use base to compare
+        list = patchListCache.get(control.getChange(), patchSet);
       }
+    } catch (PatchListNotAvailableException e) {
+      throw new NoSuchEntityException();
     }
 
     final List<Patch> patches = list.toPatchList(patchSet.getId());
@@ -185,7 +187,8 @@
     return new PatchListKey(projectKey, oldId, newId, whitespace);
   }
 
-  private PatchList listFor(final PatchListKey key) {
+  private PatchList listFor(PatchListKey key)
+      throws PatchListNotAvailableException {
     return patchListCache.get(key);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
index 183b5f6..37a1c92 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
@@ -145,6 +145,7 @@
 
             switch (lbl.status) {
               case OK:
+              case MAY:
                 ok++;
                 break;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java
index f1ce5af..e71e302 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java
@@ -24,8 +24,8 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.mail.RebasedPatchSetSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -54,7 +54,7 @@
   private final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory;
 
   private final ChangeDetailFactory.Factory changeDetailFactory;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated replication;
 
   private final PatchSet.Id patchSetId;
 
@@ -75,7 +75,7 @@
       @Assisted final PatchSet.Id patchSetId, final ChangeHookRunner hooks,
       final GitRepositoryManager gitManager,
       final PatchSetInfoFactory patchSetInfoFactory,
-      final ReplicationQueue replication,
+      final GitReferenceUpdated replication,
       @GerritPersonIdent final PersonIdent myIdent,
       final ApprovalsUtil approvalsUtil) {
     this.changeControlFactory = changeControlFactory;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java
index 5d7fe32..12d66fd 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.server.changedetail.RestoreChange;
 import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
-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;
@@ -60,9 +59,8 @@
 
   @Override
   public ChangeDetail call() throws NoSuchChangeException, OrmException,
-      EmailException, NoSuchEntityException, InvalidChangeOperationException,
-      PatchSetInfoNotAvailableException, RepositoryNotFoundException,
-      IOException {
+      EmailException, NoSuchEntityException, PatchSetInfoNotAvailableException,
+      RepositoryNotFoundException, IOException {
     final ReviewResult result =
         restoreChangeFactory.create(patchSetId.getParentKey(), message).call();
     if (result.getErrors().size() > 0) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java
index 9fb9ae1..60a03bd 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java
@@ -24,8 +24,8 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.mail.RevertedSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -54,7 +54,7 @@
   private final IdentifiedUser currentUser;
   private final RevertedSender.Factory revertedSenderFactory;
   private final ChangeDetailFactory.Factory changeDetailFactory;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated replication;
 
   private final PatchSet.Id patchSetId;
   @Nullable
@@ -76,7 +76,7 @@
       @Assisted @Nullable final String message, final ChangeHooks hooks,
       final GitRepositoryManager gitManager,
       final PatchSetInfoFactory patchSetInfoFactory,
-      final ReplicationQueue replication,
+      final GitReferenceUpdated replication,
       @GerritPersonIdent final PersonIdent myIdent) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
index 66b3278..8ad9a10 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
 import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
@@ -141,6 +142,9 @@
     } catch (RepositoryNotFoundException e) {
       log.error("Repository " + projectKey + " not found", e);
       throw new NoSuchChangeException(changeId, e);
+    } catch (IOException e) {
+      log.error("Cannot open repository " + projectKey, e);
+      throw new NoSuchChangeException(changeId, e);
     }
     try {
       final PatchList list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
@@ -151,12 +155,12 @@
           content.getOldName(), //
           content.getNewName());
 
-      try {
         return b.toPatchScript(content, comments, history);
-      } catch (IOException e) {
-        log.error("File content unavailable", e);
-        throw new NoSuchChangeException(changeId, e);
-      }
+    } catch (PatchListNotAvailableException e) {
+      throw new NoSuchChangeException(changeId, e);
+    } catch (IOException e) {
+      log.error("File content unavailable", e);
+      throw new NoSuchChangeException(changeId, e);
     } finally {
       git.close();
     }
@@ -166,7 +170,8 @@
     return new PatchListKey(projectKey, aId, bId, whitespace);
   }
 
-  private PatchList listFor(final PatchListKey key) {
+  private PatchList listFor(final PatchListKey key)
+      throws PatchListNotAvailableException {
     return patchListCache.get(key);
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/plugin/ListPluginsServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/plugin/ListPluginsServlet.java
new file mode 100644
index 0000000..5e8145c
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/plugin/ListPluginsServlet.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.rpc.plugin;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.httpd.RestApiServlet;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.plugins.ListPlugins;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+public class ListPluginsServlet extends RestApiServlet {
+  private static final long serialVersionUID = 1L;
+  private final ParameterParser paramParser;
+  private final Provider<ListPlugins> factory;
+
+  @Inject
+  ListPluginsServlet(final Provider<CurrentUser> currentUser,
+      ParameterParser paramParser, Provider<ListPlugins> ls) {
+    super(currentUser);
+    this.paramParser = paramParser;
+    this.factory = ls;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse res)
+      throws IOException {
+    ListPlugins impl = factory.get();
+    if (acceptsJson(req)) {
+      impl.setFormat(OutputFormat.JSON_COMPACT);
+    }
+    if (paramParser.parse(impl, req, res)) {
+      ByteArrayOutputStream buf = new ByteArrayOutputStream();
+      if (impl.getFormat().isJson()) {
+        res.setContentType(JSON_TYPE);
+        buf.write(JSON_MAGIC);
+      } else {
+        res.setContentType("text/plain");
+      }
+      impl.display(buf);
+      res.setCharacterEncoding("UTF-8");
+      send(req, res, buf.toByteArray());
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
index 44c3141..97e9bb4 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
@@ -22,8 +22,8 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.RefControl;
@@ -59,7 +59,7 @@
   private final ListBranches.Factory listBranchesFactory;
   private final IdentifiedUser identifiedUser;
   private final GitRepositoryManager repoManager;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated referenceUpdated;
   private final ChangeHooks hooks;
 
   private final Project.NameKey projectName;
@@ -71,7 +71,7 @@
       final ListBranches.Factory listBranchesFactory,
       final IdentifiedUser identifiedUser,
       final GitRepositoryManager repoManager,
-      final ReplicationQueue replication,
+      GitReferenceUpdated referenceUpdated,
       final ChangeHooks hooks,
 
       @Assisted Project.NameKey projectName,
@@ -81,7 +81,7 @@
     this.listBranchesFactory = listBranchesFactory;
     this.identifiedUser = identifiedUser;
     this.repoManager = repoManager;
-    this.replication = replication;
+    this.referenceUpdated = referenceUpdated;
     this.hooks = hooks;
 
     this.projectName = projectName;
@@ -144,7 +144,7 @@
           case FAST_FORWARD:
           case NEW:
           case NO_CHANGE:
-            replication.scheduleUpdate(name.getParentKey(), refname);
+            referenceUpdated.fire(name.getParentKey(), refname);
             hooks.doRefUpdatedHook(name, u, identifiedUser.getAccount());
             break;
           default: {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
index 777329b..c991c47 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
@@ -22,9 +22,9 @@
 import com.google.gerrit.common.errors.InvalidNameException;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.NoSuchProjectException;
@@ -60,7 +60,7 @@
   private final ProjectAccessFactory.Factory projectAccessFactory;
   private final ProjectControl.Factory projectControlFactory;
   private final ProjectCache projectCache;
-  private final GroupCache groupCache;
+  private final GroupBackend groupBackend;
   private final MetaDataUpdate.User metaDataUpdateFactory;
 
   private final Project.NameKey projectName;
@@ -71,7 +71,7 @@
   @Inject
   ChangeProjectAccess(final ProjectAccessFactory.Factory projectAccessFactory,
       final ProjectControl.Factory projectControlFactory,
-      final ProjectCache projectCache, final GroupCache groupCache,
+      final ProjectCache projectCache, final GroupBackend groupBackend,
       final MetaDataUpdate.User metaDataUpdateFactory,
 
       @Assisted final Project.NameKey projectName,
@@ -81,7 +81,7 @@
     this.projectAccessFactory = projectAccessFactory;
     this.projectControlFactory = projectControlFactory;
     this.projectCache = projectCache;
-    this.groupCache = groupCache;
+    this.groupBackend = groupBackend;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
 
     this.projectName = projectName;
@@ -198,12 +198,12 @@
   private void lookupGroup(PermissionRule rule) throws NoSuchGroupException {
     GroupReference ref = rule.getGroup();
     if (ref.getUUID() == null) {
-      AccountGroup.NameKey name = new AccountGroup.NameKey(ref.getName());
-      AccountGroup group = groupCache.get(name);
+      final GroupReference group =
+          GroupBackends.findBestSuggestion(groupBackend, ref.getName());
       if (group == null) {
-        throw new NoSuchGroupException(name);
+        throw new NoSuchGroupException(ref.getName());
       }
-      ref.setUUID(group.getGroupUUID());
+      ref.setUUID(group.getUUID());
     }
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
index bd6e460..a2b62cc 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
@@ -65,7 +65,8 @@
   }
 
   @Override
-  public ProjectDetail call() throws NoSuchProjectException, OrmException {
+  public ProjectDetail call() throws NoSuchProjectException, OrmException,
+      IOException {
     final Project.NameKey projectName = update.getNameKey();
     projectControlFactory.ownerFor(projectName);
 
@@ -74,6 +75,8 @@
       md = metaDataUpdateFactory.create(projectName);
     } catch (RepositoryNotFoundException notFound) {
       throw new NoSuchProjectException(projectName);
+    } catch (IOException e) {
+      throw new OrmException(e);
     }
     try {
       // TODO We really should take advantage of the Git commit DAG and
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java
index 039a301..141f332 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java
@@ -26,7 +26,7 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import org.eclipse.jgit.lib.Constants;
+import java.util.Collections;
 
 public class CreateProjectHandler extends Handler<VoidResult> {
 
@@ -74,7 +74,7 @@
     }
     args.projectDescription = "";
     args.submitType = SubmitType.MERGE_IF_NECESSARY;
-    args.branch = Constants.MASTER;
+    args.branch = Collections.emptyList();
     args.createEmptyCommit = emptyCommit;
     args.permissionsOnly = permissionsOnly;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
index a3a1c25..f2b3ca3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
@@ -20,8 +20,8 @@
 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;
-import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gwtorm.server.OrmException;
@@ -50,7 +50,7 @@
 
   private final ProjectControl.Factory projectControlFactory;
   private final GitRepositoryManager repoManager;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated replication;
   private final IdentifiedUser identifiedUser;
   private final ChangeHooks hooks;
   private final ReviewDb db;
@@ -61,7 +61,7 @@
   @Inject
   DeleteBranches(final ProjectControl.Factory projectControlFactory,
       final GitRepositoryManager repoManager,
-      final ReplicationQueue replication,
+      final GitReferenceUpdated replication,
       final IdentifiedUser identifiedUser,
       final ChangeHooks hooks,
       final ReviewDb db,
@@ -80,7 +80,7 @@
 
   @Override
   public Set<Branch.NameKey> call() throws NoSuchProjectException,
-      RepositoryNotFoundException, OrmException {
+      RepositoryNotFoundException, OrmException, IOException {
     final ProjectControl projectControl =
         projectControlFactory.controlFor(projectName);
 
@@ -121,7 +121,7 @@
           case FAST_FORWARD:
           case FORCED:
             deleted.add(branchKey);
-            replication.scheduleUpdate(projectName, refname);
+            replication.fire(projectName, refname);
             hooks.doRefUpdatedHook(branchKey, u, identifiedUser.getAccount());
             break;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
index 68fde45..2366423 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
@@ -62,7 +62,7 @@
   }
 
   @Override
-  public ListBranchesResult call() throws NoSuchProjectException {
+  public ListBranchesResult call() throws NoSuchProjectException, IOException {
     final ProjectControl pctl = projectControlFactory.validateFor( //
         projectName, //
         ProjectControl.OWNER | ProjectControl.VISIBLE);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java
index 2757640..d327d35 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.httpd.RestApiServlet;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.project.ListProjects;
 import com.google.inject.Inject;
@@ -36,7 +37,9 @@
   private final Provider<ListProjects> factory;
 
   @Inject
-  ListProjectsServlet(ParameterParser paramParser, Provider<ListProjects> ls) {
+  ListProjectsServlet(final Provider<CurrentUser> currentUser,
+      ParameterParser paramParser, Provider<ListProjects> ls) {
+    super(currentUser);
     this.paramParser = paramParser;
     this.factory = ls;
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
index 7ac4ec3..f934d11 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
@@ -23,7 +23,7 @@
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -51,7 +51,7 @@
     ProjectAccessFactory create(@Assisted Project.NameKey name);
   }
 
-  private final GroupCache groupCache;
+  private final GroupBackend groupBackend;
   private final ProjectCache projectCache;
   private final ProjectControl.Factory projectControlFactory;
   private final GroupControl.Factory groupControlFactory;
@@ -62,7 +62,7 @@
   private ProjectControl pc;
 
   @Inject
-  ProjectAccessFactory(final GroupCache groupCache,
+  ProjectAccessFactory(final GroupBackend groupBackend,
       final ProjectCache projectCache,
       final ProjectControl.Factory projectControlFactory,
       final GroupControl.Factory groupControlFactory,
@@ -70,7 +70,7 @@
       final AllProjectsName allProjectsName,
 
       @Assisted final Project.NameKey name) {
-    this.groupCache = groupCache;
+    this.groupBackend = groupBackend;
     this.projectCache = projectCache;
     this.projectControlFactory = projectControlFactory;
     this.groupControlFactory = groupControlFactory;
@@ -94,7 +94,7 @@
     try {
       config = ProjectConfig.read(md);
 
-      if (config.updateGroupNames(groupCache)) {
+      if (config.updateGroupNames(groupBackend)) {
         md.setMessage("Update group names\n");
         if (config.commit(md)) {
           projectCache.evict(config.getProject());
@@ -110,6 +110,7 @@
       md.close();
     }
 
+    final RefControl metaConfigControl = pc.controlForRef(GitRepositoryManager.REF_CONFIG);
     List<AccessSection> local = new ArrayList<AccessSection>();
     Set<String> ownerOf = new HashSet<String>();
     Map<AccountGroup.UUID, Boolean> visibleGroups =
@@ -125,7 +126,7 @@
 
       } else if (RefConfigSection.isValid(name)) {
         RefControl rc = pc.controlForRef(name);
-        if (rc.isOwner()) {
+        if (rc.isOwner() || metaConfigControl.isVisible()) {
           local.add(section);
           ownerOf.add(name);
 
@@ -195,8 +196,7 @@
 
     detail.setLocal(local);
     detail.setOwnerOf(ownerOf);
-    detail.setConfigVisible(pc.isOwner()
-        || pc.controlForRef(GitRepositoryManager.REF_CONFIG).isVisible());
+    detail.setConfigVisible(pc.isOwner() || metaConfigControl.isVisible());
     return detail;
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
index a6bb74f..ca7f448 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.common.data.ProjectAccess;
 import com.google.gerrit.common.data.ProjectAdminService;
 import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.common.data.ProjectList;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwtjsonrpc.common.AsyncCallback;
@@ -37,12 +36,10 @@
   private final ChangeProjectSettings.Factory changeProjectSettingsFactory;
   private final DeleteBranches.Factory deleteBranchesFactory;
   private final ListBranches.Factory listBranchesFactory;
-  private final VisibleProjects.Factory visibleProjectsFactory;
   private final VisibleProjectDetails.Factory visibleProjectDetailsFactory;
   private final ProjectAccessFactory.Factory projectAccessFactory;
   private final CreateProjectHandler.Factory createProjectHandlerFactory;
   private final ProjectDetailFactory.Factory projectDetailFactory;
-  private final SuggestParentCandidatesHandler.Factory suggestParentCandidatesHandlerFactory;
 
   @Inject
   ProjectAdminServiceImpl(final AddBranch.Factory addBranchFactory,
@@ -50,28 +47,19 @@
       final ChangeProjectSettings.Factory changeProjectSettingsFactory,
       final DeleteBranches.Factory deleteBranchesFactory,
       final ListBranches.Factory listBranchesFactory,
-      final VisibleProjects.Factory visibleProjectsFactory,
       final VisibleProjectDetails.Factory visibleProjectDetailsFactory,
       final ProjectAccessFactory.Factory projectAccessFactory,
       final ProjectDetailFactory.Factory projectDetailFactory,
-      final SuggestParentCandidatesHandler.Factory parentCandidatesFactory,
       final CreateProjectHandler.Factory createNewProjectFactory) {
     this.addBranchFactory = addBranchFactory;
     this.changeProjectAccessFactory = changeProjectAccessFactory;
     this.changeProjectSettingsFactory = changeProjectSettingsFactory;
     this.deleteBranchesFactory = deleteBranchesFactory;
     this.listBranchesFactory = listBranchesFactory;
-    this.visibleProjectsFactory = visibleProjectsFactory;
     this.visibleProjectDetailsFactory = visibleProjectDetailsFactory;
     this.projectAccessFactory = projectAccessFactory;
     this.projectDetailFactory = projectDetailFactory;
     this.createProjectHandlerFactory = createNewProjectFactory;
-    this.suggestParentCandidatesHandlerFactory = parentCandidatesFactory;
-  }
-
-  @Override
-  public void visibleProjects(final AsyncCallback<ProjectList> callback) {
-    visibleProjectsFactory.create().to(callback);
   }
 
   @Override
@@ -80,11 +68,6 @@
   }
 
   @Override
-  public void suggestParentCandidates(AsyncCallback<List<Project>> callback) {
-    suggestParentCandidatesHandlerFactory.create().to(callback);
-  }
-
-  @Override
   public void projectDetail(final Project.NameKey projectName,
       final AsyncCallback<ProjectDetail> callback) {
     projectDetailFactory.create(projectName).to(callback);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
index d782da4..2741640 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
@@ -51,7 +51,7 @@
   }
 
   @Override
-  public ProjectDetail call() throws NoSuchProjectException {
+  public ProjectDetail call() throws NoSuchProjectException, IOException {
     final ProjectControl pc =
         projectControlFactory.validateFor(projectName, ProjectControl.OWNER
             | ProjectControl.VISIBLE);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
index 2eb55b3..efcc22f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
@@ -34,11 +34,9 @@
         factory(ChangeProjectSettings.Factory.class);
         factory(DeleteBranches.Factory.class);
         factory(ListBranches.Factory.class);
-        factory(VisibleProjects.Factory.class);
         factory(VisibleProjectDetails.Factory.class);
         factory(ProjectAccessFactory.Factory.class);
         factory(ProjectDetailFactory.Factory.class);
-        factory(SuggestParentCandidatesHandler.Factory.class);
       }
     });
     rpc(ProjectAdminServiceImpl.class);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/SuggestParentCandidatesHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/SuggestParentCandidatesHandler.java
deleted file mode 100644
index ba0e4cd..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/SuggestParentCandidatesHandler.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License
-
-package com.google.gerrit.httpd.rpc.project;
-
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.SuggestParentCandidates;
-import com.google.inject.Inject;
-
-import java.util.List;
-
-public class SuggestParentCandidatesHandler extends Handler<List<Project>> {
-  interface Factory {
-    SuggestParentCandidatesHandler create();
-  }
-
-  private final SuggestParentCandidates suggestParentCandidates;
-
-  @Inject
-  SuggestParentCandidatesHandler(final SuggestParentCandidates suggestParentCandidates) {
-    this.suggestParentCandidates = suggestParentCandidates;
-  }
-
-  @Override
-  public List<Project> call() throws Exception {
-    return suggestParentCandidates.getProjects();
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java
index e12cfb1..1c22d83 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
-
 import com.google.gerrit.common.data.ProjectDetail;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.Project;
@@ -22,6 +21,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -50,6 +50,7 @@
       try {
         result.add(projectDetailFactory.create(projectName).call());
       } catch (NoSuchProjectException e) {
+      } catch (IOException e) {
       }
     }
     Collections.sort(result, new Comparator<ProjectDetail>() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjects.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjects.java
deleted file mode 100644
index ba65617..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjects.java
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.project;
-
-import com.google.gerrit.common.data.ProjectList;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.inject.Inject;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-class VisibleProjects extends Handler<ProjectList> {
-  interface Factory {
-    VisibleProjects create();
-  }
-
-  private final ProjectControl.Factory projectControlFactory;
-  private final ProjectCache projectCache;
-  private final CurrentUser user;
-
-  @Inject
-  VisibleProjects(final ProjectControl.Factory projectControlFactory,
-      final ProjectCache projectCache, final CurrentUser user) {
-    this.projectControlFactory = projectControlFactory;
-    this.projectCache = projectCache;
-    this.user = user;
-  }
-
-  @Override
-  public ProjectList call() {
-    ProjectList result = new ProjectList();
-    result.setProjects(getProjects());
-    result.setCanCreateProject(user.getCapabilities().canCreateProject());
-    return result;
-  }
-
-  private List<Project> getProjects() {
-    List<Project> result = new ArrayList<Project>();
-    for (Project.NameKey p : projectCache.all()) {
-      try {
-        ProjectControl c = projectControlFactory.controlFor(p);
-        if (c.isVisible() || c.isOwner()) {
-          result.add(c.getProject());
-        }
-      } catch (NoSuchProjectException e) {
-        continue;
-      }
-    }
-    Collections.sort(result, new Comparator<Project>() {
-      public int compare(final Project a, final Project b) {
-        return a.getName().compareTo(b.getName());
-      }
-    });
-    return result;
-  }
-}
diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
index 61bb52f..9cca559 100644
--- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -31,9 +31,10 @@
 import java.net.URLClassLoader;
 import java.security.CodeSource;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.Enumeration;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
 import java.util.jar.Attributes;
 import java.util.jar.JarFile;
 import java.util.jar.Manifest;
@@ -196,7 +197,7 @@
       throw e;
     }
 
-    final ArrayList<URL> jars = new ArrayList<URL>();
+    final SortedMap<String, URL> jars = new TreeMap<String, URL>();
     try {
       final ZipFile zf = new ZipFile(path);
       try {
@@ -208,6 +209,7 @@
           }
 
           if (ze.getName().startsWith("WEB-INF/lib/")) {
+            String name = ze.getName().substring("WEB-INF/lib/".length());
             final File tmp = createTempFile(safeName(ze), ".jar");
             final FileOutputStream out = new FileOutputStream(tmp);
             try {
@@ -224,7 +226,7 @@
             } finally {
               out.close();
             }
-            jars.add(tmp.toURI().toURL());
+            jars.put(name, tmp.toURI().toURL());
           }
         }
       } finally {
@@ -237,13 +239,38 @@
     if (jars.isEmpty()) {
       return GerritLauncher.class.getClassLoader();
     }
-    Collections.sort(jars, new Comparator<URL>() {
-      public int compare(URL o1, URL o2) {
-        return o1.toString().compareTo(o2.toString());
-      }
-    });
 
-    return new URLClassLoader(jars.toArray(new URL[jars.size()]));
+    // The extension API needs to be its own ClassLoader, along
+    // with a few of its dependencies. Try to construct this first.
+    List<URL> extapi = new ArrayList<URL>();
+    move(jars, "gerrit-extension-api-", extapi);
+    move(jars, "guice-", extapi);
+    move(jars, "javax.inject-1.jar", extapi);
+    move(jars, "aopalliance-1.0.jar", extapi);
+    move(jars, "guice-servlet-", extapi);
+    move(jars, "servlet-api-", extapi);
+
+    ClassLoader parent = ClassLoader.getSystemClassLoader();
+    if (!extapi.isEmpty()) {
+      parent = new URLClassLoader(
+          extapi.toArray(new URL[extapi.size()]),
+          parent);
+    }
+    return new URLClassLoader(
+        jars.values().toArray(new URL[jars.size()]),
+        parent);
+  }
+
+  private static void move(SortedMap<String, URL> jars,
+      String prefix,
+      List<URL> extapi) {
+    SortedMap<String, URL> matches = jars.tailMap(prefix);
+    if (!matches.isEmpty()) {
+      String first = matches.firstKey();
+      if (first.startsWith(prefix)) {
+        extapi.add(jars.remove(first));
+      }
+    }
   }
 
   private static String safeName(final ZipEntry ze) {
diff --git a/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java b/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
index 7d7bc49..1f08a81 100644
--- a/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
+++ b/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
@@ -18,7 +18,11 @@
 
 import org.apache.commons.codec.binary.Base64;
 
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
 import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
 import java.io.UnsupportedEncodingException;
 import java.net.SocketException;
 import java.security.InvalidKeyException;
@@ -50,7 +54,22 @@
     }
 
     _socket_ = sslFactory(verify).createSocket(_socket_, hostname, port, true);
-    _connectAction_();
+
+    // XXX: Can't call _connectAction_() because SMTP server doesn't
+    // give banner information again after STARTTLS, thus SMTP._connectAction_()
+    // will wait on __getReply() forever, see source code of commons-net-2.2.
+    //
+    // The lines below are copied from SocketClient._connectAction_() and
+    // SMTP._connectAction_() in commons-net-2.2.
+    _socket_.setSoTimeout(_timeout_);
+    _input_ = _socket_.getInputStream();
+    _output_ = _socket_.getOutputStream();
+    _reader =
+        new BufferedReader(new InputStreamReader(_input_,
+                      UTF_8));
+    _writer =
+        new BufferedWriter(new OutputStreamWriter(_output_,
+                      UTF_8));
     return true;
   }
 
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 bbff5cb..282bbc9 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,10 @@
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 
 import com.google.gerrit.common.ChangeHookRunner;
-import com.google.gerrit.ehcache.EhcachePoolImpl;
 import com.google.gerrit.httpd.CacheBasedWebSession;
 import com.google.gerrit.httpd.GitOverHttpModule;
 import com.google.gerrit.httpd.HttpCanonicalWebUrlProvider;
+import com.google.gerrit.httpd.RequestContextFilter;
 import com.google.gerrit.httpd.WebModule;
 import com.google.gerrit.httpd.WebSshGlueModule;
 import com.google.gerrit.httpd.auth.openid.OpenIdModule;
@@ -35,29 +35,40 @@
 import com.google.gerrit.pgm.util.RuntimeShutdown;
 import com.google.gerrit.pgm.util.SiteProgram;
 import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.AuthConfigModule;
 import com.google.gerrit.server.config.CanonicalWebUrlModule;
 import com.google.gerrit.server.config.CanonicalWebUrlProvider;
 import com.google.gerrit.server.config.GerritGlobalModule;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.MasterNodeStartup;
 import com.google.gerrit.server.contact.HttpContactStoreConnection;
-import com.google.gerrit.server.git.PushReplication;
 import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
 import com.google.gerrit.server.mail.SmtpEmailSender;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
 import com.google.gerrit.server.plugins.PluginModule;
+import com.google.gerrit.server.schema.SchemaUpdater;
 import com.google.gerrit.server.schema.SchemaVersionCheck;
+import com.google.gerrit.server.schema.UpdateUI;
 import com.google.gerrit.server.ssh.NoSshModule;
 import com.google.gerrit.sshd.SshModule;
 import com.google.gerrit.sshd.commands.MasterCommandModule;
 import com.google.gerrit.sshd.commands.SlaveCommandModule;
+import com.google.gwtorm.jdbc.JdbcExecutor;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.gwtorm.server.StatementExecutor;
+import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Module;
 import com.google.inject.Provider;
 
+import org.eclipse.jgit.lib.Config;
 import org.kohsuke.args4j.Option;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -145,6 +156,7 @@
       sysInjector = createSysInjector();
       sysInjector.getInstance(PluginGuiceEnvironment.class)
         .setCfgInjector(cfgInjector);
+      sysInjector.getInstance(SchemaUpgrade.class).upgradeSchema();
       manager.add(dbInjector, cfgInjector, sysInjector);
 
       if (sshd) {
@@ -157,6 +169,7 @@
 
       manager.start();
       RuntimeShutdown.add(new Runnable() {
+        @Override
         public void run() {
           log.info("caught shutdown, cleaning up");
           if (runId != null) {
@@ -191,6 +204,74 @@
     }
   }
 
+  static class SchemaUpgrade {
+
+    private final Config config;
+    private final SchemaUpdater updater;
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    SchemaUpgrade(@GerritServerConfig Config config, SchemaUpdater updater,
+        SchemaFactory<ReviewDb> schema) {
+      this.config = config;
+      this.updater = updater;
+      this.schema = schema;
+    }
+
+    void upgradeSchema() throws OrmException {
+      SchemaUpgradePolicy policy =
+          config.getEnum("site", null, "upgradeSchemaOnStartup",
+              SchemaUpgradePolicy.OFF);
+      if (policy == SchemaUpgradePolicy.AUTO
+          || policy == SchemaUpgradePolicy.AUTO_NO_PRUNE) {
+        final List<String> pruneList = new ArrayList<String>();
+        updater.update(new UpdateUI() {
+          @Override
+          public void message(String msg) {
+            log.info(msg);
+          }
+
+          @Override
+          public boolean yesno(boolean def, String msg) {
+            return true;
+          }
+
+          @Override
+          public boolean isBatch() {
+            return true;
+          }
+
+          @Override
+          public void pruneSchema(StatementExecutor e, List<String> prune) {
+            for (String p : prune) {
+              if (!pruneList.contains(p)) {
+                pruneList.add(p);
+              }
+            }
+          }
+        });
+
+        if (!pruneList.isEmpty() && policy == SchemaUpgradePolicy.AUTO) {
+          log.info("Pruning: " + pruneList.toString());
+          final JdbcSchema db = (JdbcSchema) schema.open();
+          try {
+            final JdbcExecutor e = new JdbcExecutor(db);
+            try {
+              for (String sql : pruneList) {
+                e.execute(sql);
+              }
+            } finally {
+              e.close();
+            }
+          } finally {
+            db.close();
+          }
+        }
+      }
+    }
+  }
+
+
   private String myVersion() {
     return com.google.gerrit.common.Version.getVersion();
   }
@@ -209,10 +290,9 @@
     modules.add(new ChangeHookRunner.Module());
     modules.add(new ReceiveCommitsExecutorModule());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
-    modules.add(new EhcachePoolImpl.Module());
+    modules.add(new DefaultCacheFactory.Module());
     modules.add(new SmtpEmailSender.Module());
     modules.add(new SignedTokenEmailTokenVerifier.Module());
-    modules.add(new PushReplication.Module());
     modules.add(new PluginModule());
     if (httpd) {
       modules.add(new CanonicalWebUrlModule() {
@@ -245,7 +325,7 @@
   private Injector createSshInjector() {
     final List<Module> modules = new ArrayList<Module>();
     if (sshd) {
-      modules.add(new SshModule());
+      modules.add(sysInjector.getInstance(SshModule.class));
       if (slave) {
         modules.add(new SlaveCommandModule());
       } else {
@@ -273,6 +353,7 @@
 
   private Injector createWebInjector() {
     final List<Module> modules = new ArrayList<Module>();
+    modules.add(RequestContextFilter.module());
     modules.add(CacheBasedWebSession.module());
     modules.add(HttpContactStoreConnection.module());
     modules.add(sysInjector.getInstance(GitOverHttpModule.class));
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
index 5d7983b..f100372 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
@@ -17,7 +17,6 @@
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 
 import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.ehcache.EhcachePoolImpl;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.pgm.util.SiteProgram;
@@ -27,7 +26,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.GroupCacheImpl;
-import com.google.gerrit.server.cache.CachePool;
+import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
 import com.google.gerrit.server.config.ApprovalTypesProvider;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.CanonicalWebUrlProvider;
@@ -97,11 +96,10 @@
             Scopes.SINGLETON);
         bind(String.class).annotatedWith(CanonicalWebUrl.class)
             .toProvider(CanonicalWebUrlProvider.class).in(Scopes.SINGLETON);
-        bind(CachePool.class);
 
         install(AccountCacheImpl.module());
         install(GroupCacheImpl.module());
-        install(new EhcachePoolImpl.Module());
+        install(new DefaultCacheFactory.Module());
         install(new FactoryModule() {
           @Override
           protected void configure() {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java
index d967969..a5ce908 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java
@@ -47,6 +47,7 @@
     manager.add(dbInjector);
     manager.start();
     RuntimeShutdown.add(new Runnable() {
+      @Override
       public void run() {
         try {
           System.in.close();
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 f06946f..2d56453 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
@@ -191,6 +191,11 @@
         }
 
         @Override
+        public boolean isBatch() {
+          return ui.isBatch();
+        }
+
+        @Override
         public void pruneSchema(StatementExecutor e, List<String> prune) {
           for (String p : prune) {
             if (!pruneList.contains(p)) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
index 451ed30..cabdc64 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
@@ -43,7 +43,7 @@
   @Option(name = "--all", usage = "recompile all rules")
   private boolean all;
 
-  @Option(name = "--quiet", usage = "supress some messsages")
+  @Option(name = "--quiet", usage = "suppress some messages")
   private boolean quiet;
 
   @Argument(index = 0, multiValued = true, metaVar = "PROJECT", usage = "project to compile rules for")
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
index c09329a..795ba5b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
@@ -33,7 +33,6 @@
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.TextProgressMonitor;
@@ -109,7 +108,7 @@
     final Repository git;
     try {
       git = gitManager.openRepository(project);
-    } catch (RepositoryNotFoundException e) {
+    } catch (IOException e) {
       return;
     }
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SchemaUpgradePolicy.java
similarity index 65%
copy from gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
copy to gerrit-pgm/src/main/java/com/google/gerrit/pgm/SchemaUpgradePolicy.java
index 204d777..67f5c91 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SchemaUpgradePolicy.java
@@ -12,12 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.pgm;
 
-public class IncompleteUserInfoException extends Exception {
-  private static final long serialVersionUID = 1L;
+/** Policy for auto upgrading schema on server startup */
+public enum SchemaUpgradePolicy {
 
-  public IncompleteUserInfoException(final String userName, final String missingInfo) {
-    super("For the user \"" + userName + "\" " + missingInfo + " is not set.");
-  }
+  /** Perform schema migration if necessary and prune unused objects */
+  AUTO,
+
+  /** Like AUTO but don't prune unused objects */
+  AUTO_NO_PRUNE,
+
+  /** No automatic schema upgrade */
+  OFF
 }
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 a57de3c..d85ff20 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
@@ -17,8 +17,8 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.launcher.GerritLauncher;
-import com.google.gerrit.lifecycle.LifecycleListener;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
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 7a3556e..c5732e9 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
@@ -28,12 +28,14 @@
 class InitSendEmail implements InitStep {
   private final ConsoleUI ui;
   private final Section sendemail;
+  private final SitePaths site;
 
   @Inject
   InitSendEmail(final ConsoleUI ui, final SitePaths site,
       final Section.Factory sections) {
     this.ui = ui;
     this.sendemail = sections.get("sendemail");
+    this.site = site;
   }
 
   public void run() {
@@ -49,7 +51,9 @@
             true);
 
     String username = null;
-    if ((enc != null && enc != Encryption.NONE) || !isLocal(hostname)) {
+    if (site.gerrit_config.exists()) {
+      username = sendemail.get("smtpUser");
+    } else if ((enc != null && enc != Encryption.NONE) || !isLocal(hostname)) {
       username = username();
     }
     sendemail.string("SMTP username", "smtpUser", username);
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 d26d46e..ee3030d 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
@@ -72,6 +72,7 @@
     mkdir(site.mail_dir);
     mkdir(site.static_dir);
     mkdir(site.plugins_dir);
+    mkdir(site.data_dir);
 
     for (InitStep step : steps) {
       step.run();
@@ -80,10 +81,6 @@
     savePublic(flags.cfg);
     saveSecure(flags.sec);
 
-    if (!site.replication_config.exists()) {
-      site.replication_config.createNewFile();
-    }
-
     extract(site.gerrit_sh, Init.class, "gerrit.sh");
     chmod(0755, site.gerrit_sh);
     chmod(0700, site.tmp_dir);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
index 14c8d9f..68762bb 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.pgm.util;
 
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.config.SitePaths;
 
 import org.apache.log4j.Appender;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
index 23f36a1..57cc7c4 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
@@ -16,7 +16,7 @@
 
 import static java.util.concurrent.TimeUnit.HOURS;
 
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.WorkQueue;
diff --git a/gerrit-plugin-api/.gitignore b/gerrit-plugin-api/.gitignore
index 574d1fc..e87ebb8 100644
--- a/gerrit-plugin-api/.gitignore
+++ b/gerrit-plugin-api/.gitignore
@@ -5,4 +5,4 @@
 /.settings/org.eclipse.m2e.core.prefs
 /.settings/org.eclipse.core.resources.prefs
 /.settings/org.eclipse.jdt.core.prefs
-/gerrit-pluginapi-ssh.iml
+/gerrit-plugin-api.iml
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
index 5c4ca3449..84f6f7b 100644
--- a/gerrit-plugin-api/pom.xml
+++ b/gerrit-plugin-api/pom.xml
@@ -62,7 +62,6 @@
             <excludes>
               <exclude>gwtexpui:gwtexpui</exclude>
               <exclude>gwtjsonrpc:gwtjsonrpc</exclude>
-              <exclude>com.google.gerrit:gerrit-ehcache</exclude>
               <exclude>com.google.gerrit:gerrit-prettify</exclude>
               <exclude>com.google.gerrit:gerrit-patch-commonsnet</exclude>
               <exclude>com.google.gerrit:gerrit-patch-jgit</exclude>
@@ -82,7 +81,6 @@
 
               <exclude>asm:asm</exclude>
               <exclude>eu.medsea.mimeutil:mime-util</exclude>
-              <exclude>net.sf.ehcache:ehcache-core</exclude>
               <exclude>org.antlr:antlr</exclude>
               <exclude>org.antlr:antlr-runtime</exclude>
               <exclude>org.apache.mina:mina-core</exclude>
diff --git a/gerrit-ehcache/.gitignore b/gerrit-plugin-archetype/.gitignore
similarity index 84%
rename from gerrit-ehcache/.gitignore
rename to gerrit-plugin-archetype/.gitignore
index fe190c9..80d6257 100644
--- a/gerrit-ehcache/.gitignore
+++ b/gerrit-plugin-archetype/.gitignore
@@ -1,6 +1,5 @@
 /target
 /.classpath
 /.project
-/.settings/org.eclipse.m2e.core.prefs
 /.settings/org.maven.ide.eclipse.prefs
-/gerrit-ehcache.iml
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
diff --git a/gerrit-plugin-archetype/pom.xml b/gerrit-plugin-archetype/pom.xml
new file mode 100644
index 0000000..dd1794b
--- /dev/null
+++ b/gerrit-plugin-archetype/pom.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2012 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>com.google.gerrit</groupId>
+    <artifactId>gerrit-parent</artifactId>
+    <version>2.5-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>gerrit-plugin-archetype</artifactId>
+  <name>Gerrit Code Review - Plugin Archetype</name>
+
+  <properties>
+    <defaultGerritApiVersion>${project.version}</defaultGerritApiVersion>
+  </properties>
+
+  <build>
+    <resources>
+      <resource>
+        <directory>src/main/resources</directory>
+        <filtering>true</filtering>
+        <includes>
+          <include>META-INF/maven/archetype-metadata.xml</include>
+        </includes>
+      </resource>
+      <resource>
+        <directory>src/main/resources</directory>
+        <filtering>false</filtering>
+        <excludes>
+          <exclude>META-INF/maven/archetype-metadata.xml</exclude>
+        </excludes>
+      </resource>
+    </resources>
+  </build>
+
+</project>
diff --git a/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
new file mode 100644
index 0000000..88328be
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2012 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<archetype-descriptor name="com.sap.ldi.qi.itests">
+  <requiredProperties>
+    <requiredProperty key="pluginName"/>
+
+    <requiredProperty key="Gerrit-Module">
+      <defaultValue>Y</defaultValue>
+    </requiredProperty>
+    <requiredProperty key="Gerrit-SshModule">
+      <defaultValue>Y</defaultValue>
+    </requiredProperty>
+    <requiredProperty key="Gerrit-HttpModule">
+      <defaultValue>Y</defaultValue>
+    </requiredProperty>
+
+    <requiredProperty key="Implementation-Vendor"/>
+    <requiredProperty key="Implementation-Url"/>
+
+    <requiredProperty key="gerritApiType">
+      <defaultValue>plugin</defaultValue>
+    </requiredProperty>
+    <requiredProperty key="gerritApiVersion">
+      <defaultValue>${defaultGerritApiVersion}</defaultValue>
+    </requiredProperty>
+  </requiredProperties>
+
+  <fileSets>
+    <fileSet filtered="true" packaged="true">
+      <directory>src/main/java</directory>
+      <includes>
+        <include>**/*.java</include>
+      </includes>
+    </fileSet>
+
+    <fileSet filtered="true">
+      <directory>src/main/resources/Documentation</directory>
+      <includes>
+        <include>**/*.md</include>
+      </includes>
+    </fileSet>
+
+    <fileSet>
+      <directory></directory>
+      <includes>
+        <include>.gitignore</include>
+        <include>LICENSE</include>
+      </includes>
+    </fileSet>
+  </fileSets>
+</archetype-descriptor>
diff --git a/gerrit-ehcache/.gitignore b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.gitignore
similarity index 84%
copy from gerrit-ehcache/.gitignore
copy to gerrit-plugin-archetype/src/main/resources/archetype-resources/.gitignore
index fe190c9..80d6257 100644
--- a/gerrit-ehcache/.gitignore
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.gitignore
@@ -1,6 +1,5 @@
 /target
 /.classpath
 /.project
-/.settings/org.eclipse.m2e.core.prefs
 /.settings/org.maven.ide.eclipse.prefs
-/gerrit-ehcache.iml
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/LICENSE b/gerrit-plugin-archetype/src/main/resources/archetype-resources/LICENSE
new file mode 100644
index 0000000..11069ed
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/LICENSE
@@ -0,0 +1,201 @@
+                              Apache License
+                        Version 2.0, January 2004
+                     http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+   "License" shall mean the terms and conditions for use, reproduction,
+   and distribution as defined by Sections 1 through 9 of this document.
+
+   "Licensor" shall mean the copyright owner or entity authorized by
+   the copyright owner that is granting the License.
+
+   "Legal Entity" shall mean the union of the acting entity and all
+   other entities that control, are controlled by, or are under common
+   control with that entity. For the purposes of this definition,
+   "control" means (i) the power, direct or indirect, to cause the
+   direction or management of such entity, whether by contract or
+   otherwise, or (ii) ownership of fifty percent (50%) or more of the
+   outstanding shares, or (iii) beneficial ownership of such entity.
+
+   "You" (or "Your") shall mean an individual or Legal Entity
+   exercising permissions granted by this License.
+
+   "Source" form shall mean the preferred form for making modifications,
+   including but not limited to software source code, documentation
+   source, and configuration files.
+
+   "Object" form shall mean any form resulting from mechanical
+   transformation or translation of a Source form, including but
+   not limited to compiled object code, generated documentation,
+   and conversions to other media types.
+
+   "Work" shall mean the work of authorship, whether in Source or
+   Object form, made available under the License, as indicated by a
+   copyright notice that is included in or attached to the work
+   (an example is provided in the Appendix below).
+
+   "Derivative Works" shall mean any work, whether in Source or Object
+   form, that is based on (or derived from) the Work and for which the
+   editorial revisions, annotations, elaborations, or other modifications
+   represent, as a whole, an original work of authorship. For the purposes
+   of this License, Derivative Works shall not include works that remain
+   separable from, or merely link (or bind by name) to the interfaces of,
+   the Work and Derivative Works thereof.
+
+   "Contribution" shall mean any work of authorship, including
+   the original version of the Work and any modifications or additions
+   to that Work or Derivative Works thereof, that is intentionally
+   submitted to Licensor for inclusion in the Work by the copyright owner
+   or by an individual or Legal Entity authorized to submit on behalf of
+   the copyright owner. For the purposes of this definition, "submitted"
+   means any form of electronic, verbal, or written communication sent
+   to the Licensor or its representatives, including but not limited to
+   communication on electronic mailing lists, source code control systems,
+   and issue tracking systems that are managed by, or on behalf of, the
+   Licensor for the purpose of discussing and improving the Work, but
+   excluding communication that is conspicuously marked or otherwise
+   designated in writing by the copyright owner as "Not a Contribution."
+
+   "Contributor" shall mean Licensor and any individual or Legal Entity
+   on behalf of whom a Contribution has been received by Licensor and
+   subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   copyright license to reproduce, prepare Derivative Works of,
+   publicly display, publicly perform, sublicense, and distribute the
+   Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   (except as stated in this section) patent license to make, have made,
+   use, offer to sell, sell, import, and otherwise transfer the Work,
+   where such license applies only to those patent claims licensable
+   by such Contributor that are necessarily infringed by their
+   Contribution(s) alone or by combination of their Contribution(s)
+   with the Work to which such Contribution(s) was submitted. If You
+   institute patent litigation against any entity (including a
+   cross-claim or counterclaim in a lawsuit) alleging that the Work
+   or a Contribution incorporated within the Work constitutes direct
+   or contributory patent infringement, then any patent licenses
+   granted to You under this License for that Work shall terminate
+   as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+   Work or Derivative Works thereof in any medium, with or without
+   modifications, and in Source or Object form, provided that You
+   meet the following conditions:
+
+   (a) You must give any other recipients of the Work or
+       Derivative Works a copy of this License; and
+
+   (b) You must cause any modified files to carry prominent notices
+       stating that You changed the files; and
+
+   (c) You must retain, in the Source form of any Derivative Works
+       that You distribute, all copyright, patent, trademark, and
+       attribution notices from the Source form of the Work,
+       excluding those notices that do not pertain to any part of
+       the Derivative Works; and
+
+   (d) If the Work includes a "NOTICE" text file as part of its
+       distribution, then any Derivative Works that You distribute must
+       include a readable copy of the attribution notices contained
+       within such NOTICE file, excluding those notices that do not
+       pertain to any part of the Derivative Works, in at least one
+       of the following places: within a NOTICE text file distributed
+       as part of the Derivative Works; within the Source form or
+       documentation, if provided along with the Derivative Works; or,
+       within a display generated by the Derivative Works, if and
+       wherever such third-party notices normally appear. The contents
+       of the NOTICE file are for informational purposes only and
+       do not modify the License. You may add Your own attribution
+       notices within Derivative Works that You distribute, alongside
+       or as an addendum to the NOTICE text from the Work, provided
+       that such additional attribution notices cannot be construed
+       as modifying the License.
+
+   You may add Your own copyright statement to Your modifications and
+   may provide additional or different license terms and conditions
+   for use, reproduction, or distribution of Your modifications, or
+   for any such Derivative Works as a whole, provided Your use,
+   reproduction, and distribution of the Work otherwise complies with
+   the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+   any Contribution intentionally submitted for inclusion in the Work
+   by You to the Licensor shall be under the terms and conditions of
+   this License, without any additional terms or conditions.
+   Notwithstanding the above, nothing herein shall supersede or modify
+   the terms of any separate license agreement you may have executed
+   with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+   names, trademarks, service marks, or product names of the Licensor,
+   except as required for reasonable and customary use in describing the
+   origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+   agreed to in writing, Licensor provides the Work (and each
+   Contributor provides its Contributions) on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+   implied, including, without limitation, any warranties or conditions
+   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+   PARTICULAR PURPOSE. You are solely responsible for determining the
+   appropriateness of using or redistributing the Work and assume any
+   risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+   whether in tort (including negligence), contract, or otherwise,
+   unless required by applicable law (such as deliberate and grossly
+   negligent acts) or agreed to in writing, shall any Contributor be
+   liable to You for damages, including any direct, indirect, special,
+   incidental, or consequential damages of any character arising as a
+   result of this License or out of the use or inability to use the
+   Work (including but not limited to damages for loss of goodwill,
+   work stoppage, computer failure or malfunction, or any and all
+   other commercial damages or losses), even if such Contributor
+   has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+   the Work or Derivative Works thereof, You may choose to offer,
+   and charge a fee for, acceptance of support, warranty, indemnity,
+   or other liability obligations and/or rights consistent with this
+   License. However, in accepting such obligations, You may act only
+   on Your own behalf and on Your sole responsibility, not on behalf
+   of any other Contributor, and only if You agree to indemnify,
+   defend, and hold each Contributor harmless for any liability
+   incurred by, or claims asserted against, such Contributor by reason
+   of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+   To apply the Apache License to your work, attach the following
+   boilerplate notice, with the fields enclosed by brackets "[]"
+   replaced with your own identifying information. (Don't include
+   the brackets!)  The text should be enclosed in the appropriate
+   comment syntax for the file format. We also recommend that a
+   file or class name and description of purpose be included on the
+   same "printed page" as the copyright notice for easier
+   identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
new file mode 100644
index 0000000..92099fa
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
@@ -0,0 +1,103 @@
+<!--
+Copyright (C) 2012 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>${groupId}</groupId>
+  <artifactId>${artifactId}</artifactId>
+  <packaging>jar</packaging>
+  <version>${version}</version>
+  <name>${pluginName}</name>
+
+  <properties>
+    <Gerrit-ApiType>${gerritApiType}</Gerrit-ApiType>
+    <Gerrit-ApiVersion>${gerritApiVersion}</Gerrit-ApiVersion>
+  </properties>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <version>2.4</version>
+        <configuration>
+          <archive>
+            <manifestEntries>
+#if ($Gerrit-Module.equalsIgnoreCase("Y"))
+              <Gerrit-Module>${package}.Module</Gerrit-Module>
+#end
+#if ($Gerrit-SshModule.equalsIgnoreCase("Y"))
+              <Gerrit-SshModule>${package}.SshModule</Gerrit-SshModule>
+#end
+#if ($Gerrit-HttpModule.equalsIgnoreCase("Y"))
+              <Gerrit-HttpModule>${package}.HttpModule</Gerrit-HttpModule>
+#end
+
+              <Implementation-Vendor>${Implementation-Vendor}</Implementation-Vendor>
+              <Implementation-URL>${Implementation-Url}</Implementation-URL>
+
+              <Implementation-Title>${Gerrit-ApiType} ${project.artifactId}</Implementation-Title>
+              <Implementation-Version>${project.version}</Implementation-Version>
+
+              <Gerrit-ApiType>${Gerrit-ApiType}</Gerrit-ApiType>
+              <Gerrit-ApiVersion>${Gerrit-ApiVersion}</Gerrit-ApiVersion>
+            </manifestEntries>
+          </archive>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>2.3.2</version>
+        <configuration>
+          <source>1.6</source>
+          <target>1.6</target>
+          <encoding>UTF-8</encoding>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-${Gerrit-ApiType}-api</artifactId>
+      <version>${Gerrit-ApiVersion}</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.8.1</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <repositories>
+    <repository>
+      <id>gerrit-api-repository</id>
+#if ($gerritApiVersion.endsWith("SNAPSHOT"))
+      <url>https://gerrit-api.commondatastorage.googleapis.com/snapshot/</url>
+#else
+      <url>https://gerrit-api.commondatastorage.googleapis.com/release/</url>
+#end
+    </repository>
+  </repositories>
+</project>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java
similarity index 70%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
copy to gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java
index 3370b08..2840112 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2012 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.cache;
+package ${package};
 
-public interface CachePool {
-  public <K, V> ProxyCache<K, V> register(CacheProvider<K, V> provider);
+import com.google.inject.servlet.ServletModule;
+
+class HttpModule extends ServletModule {
+  @Override
+  protected void configureServlets() {
+    // TODO
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/Module.java
similarity index 71%
rename from gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
rename to gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/Module.java
index 3370b08..0d28349 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/Module.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2012 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.cache;
+package ${package};
 
-public interface CachePool {
-  public <K, V> ProxyCache<K, V> register(CacheProvider<K, V> provider);
+import com.google.inject.AbstractModule;
+
+class Module extends AbstractModule {
+  @Override
+  protected void configure() {
+    // TODO
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/SshModule.java
similarity index 66%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
copy to gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/SshModule.java
index 3370b08..aa15ca5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/SshModule.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2012 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,8 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.cache;
+package ${package};
 
-public interface CachePool {
-  public <K, V> ProxyCache<K, V> register(CacheProvider<K, V> provider);
+import com.google.gerrit.sshd.PluginCommandModule;
+
+class SshModule extends PluginCommandModule {
+  @Override
+  protected void configureCommands() {
+    // command("my-command").to(MyCommand.class);
+  }
 }
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/cmd-start.md b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/cmd-start.md
new file mode 100644
index 0000000..beecb90
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/cmd-start.md
@@ -0,0 +1 @@
+TODO: command documentation
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/config.md b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/config.md
new file mode 100644
index 0000000..bde3084
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/config.md
@@ -0,0 +1 @@
+TODO: config documentation
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
index 8e2541a..2ea659d 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
@@ -51,7 +51,7 @@
       StringKey<com.google.gwtorm.client.Key<?>> {
     private static final long serialVersionUID = 1L;
 
-    @Column(id = 1, length = 40)
+    @Column(id = 1)
     protected String uuid;
 
     protected UUID() {
@@ -79,30 +79,10 @@
     }
   }
 
-  /** Distinguished name, within organization directory server. */
-  public static class ExternalNameKey extends
-      StringKey<com.google.gwtorm.client.Key<?>> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1)
-    protected String name;
-
-    protected ExternalNameKey() {
-    }
-
-    public ExternalNameKey(final String n) {
-      name = n;
-    }
-
-    @Override
-    public String get() {
-      return name;
-    }
-
-    @Override
-    protected void set(String newValue) {
-      name = newValue;
-    }
+  /** @return true if the UUID is for a group managed within Gerrit. */
+  public static boolean isInternalGroup(AccountGroup.UUID uuid) {
+    return uuid.get().startsWith("global:")
+        || uuid.get().matches("[0-9a-f]{40}");
   }
 
   /** Synthetic key to link to within the database */
@@ -157,20 +137,7 @@
      * who is a member of the owner group. These groups are not treated special
      * in the code.
      */
-    INTERNAL,
-
-    /**
-     * Group defined by external LDAP database.
-     * <p>
-     * A group whose membership is determined by the LDAP directory that we
-     * connect to for user and group information. In UI contexts the membership
-     * of the group is not displayed, as it may be exceedingly large, or might
-     * contain users who have never logged into this server before (and thus
-     * have no matching account record). Adding or removing users from an LDAP
-     * group requires making edits through the LDAP directory, and cannot be
-     * done through our UI.
-     */
-    LDAP;
+    INTERNAL;
   }
 
   /** Common UUID assigned to the "Project Owners" placeholder group. */
@@ -193,14 +160,6 @@
   @Column(id = 2)
   protected Id groupId;
 
-  /**
-   * Identity of the group whose members can manage this group.
-   * <p>
-   * This can be a self-reference to indicate the group's members manage itself.
-   */
-  @Column(id = 3)
-  protected Id ownerGroupId;
-
   /** A textual description of the group's purpose. */
   @Column(id = 4, length = Integer.MAX_VALUE, notNull = false)
   protected String description;
@@ -209,10 +168,6 @@
   @Column(id = 5, length = 8)
   protected String groupType;
 
-  /** Distinguished name in the directory server. */
-  @Column(id = 6, notNull = false)
-  protected ExternalNameKey externalName;
-
   @Column(id = 7)
   protected boolean visibleToAll;
 
@@ -220,6 +175,14 @@
   @Column(id = 9)
   protected UUID groupUUID;
 
+  /**
+   * Identity of the group whose members can manage this group.
+   * <p>
+   * This can be a self-reference to indicate the group's members manage itself.
+   */
+  @Column(id = 10)
+  protected UUID ownerGroupUUID;
+
   protected AccountGroup() {
   }
 
@@ -227,9 +190,9 @@
       final AccountGroup.Id newId, final AccountGroup.UUID uuid) {
     name = newName;
     groupId = newId;
-    ownerGroupId = groupId;
     visibleToAll = false;
     groupUUID = uuid;
+    ownerGroupUUID = groupUUID;
     setType(Type.INTERNAL);
   }
 
@@ -257,12 +220,12 @@
     description = d;
   }
 
-  public AccountGroup.Id getOwnerGroupId() {
-    return ownerGroupId;
+  public AccountGroup.UUID getOwnerGroupUUID() {
+    return ownerGroupUUID;
   }
 
-  public void setOwnerGroupId(final AccountGroup.Id id) {
-    ownerGroupId = id;
+  public void setOwnerGroupUUID(final AccountGroup.UUID uuid) {
+    ownerGroupUUID = uuid;
   }
 
   public Type getType() {
@@ -273,14 +236,6 @@
     groupType = t.name();
   }
 
-  public ExternalNameKey getExternalNameKey() {
-    return externalName;
-  }
-
-  public void setExternalNameKey(final ExternalNameKey k) {
-    externalName = k;
-  }
-
   public void setVisibleToAll(final boolean visibleToAll) {
     this.visibleToAll = visibleToAll;
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
index 93e6fb3..e592101 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
@@ -22,7 +22,7 @@
 public final class AccountProjectWatch {
 
   public enum NotifyType {
-    NEW_CHANGES, ALL_COMMENTS, SUBMITTED_CHANGES
+    NEW_CHANGES, ALL_COMMENTS, SUBMITTED_CHANGES, ALL
   }
 
   public static final String FILTER_ALL = "*";
@@ -159,6 +159,12 @@
       case SUBMITTED_CHANGES:
         notifySubmittedChanges = v;
         break;
+
+      case ALL:
+        notifyNewChanges = v;
+        notifyAllComments = v;
+        notifySubmittedChanges = v;
+        break;
     }
   }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java
index 9e88244..1de80f3 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java
@@ -29,10 +29,6 @@
   @Query("WHERE groupUUID = ?")
   ResultSet<AccountGroup> byUUID(AccountGroup.UUID uuid) throws OrmException;
 
-  @Query("WHERE externalName = ?")
-  ResultSet<AccountGroup> byExternalName(AccountGroup.ExternalNameKey name)
-      throws OrmException;
-
   @Query
   ResultSet<AccountGroup> all() throws OrmException;
 }
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index 70397d8..af18173 100644
--- a/gerrit-server/pom.xml
+++ b/gerrit-server/pom.xml
@@ -110,6 +110,11 @@
     </dependency>
 
     <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+
+    <dependency>
       <groupId>com.google.gerrit</groupId>
       <artifactId>gerrit-antlr</artifactId>
       <version>${project.version}</version>
@@ -170,6 +175,11 @@
       <groupId>com.googlecode.prolog-cafe</groupId>
       <artifactId>PrologCafe</artifactId>
     </dependency>
+
+    <dependency>
+      <groupId>org.pegdown</groupId>
+      <artifactId>pegdown</artifactId>
+    </dependency>
   </dependencies>
 
   <build>
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 274e73ff..832bd23 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
@@ -50,7 +50,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -203,7 +202,7 @@
     private Repository openRepository(final Project.NameKey name) {
         try {
             return repoManager.openRepository(name);
-        } catch (RepositoryNotFoundException err) {
+        } catch (IOException err) {
             log.warn("Cannot open repository " + name.get(), err);
             return null;
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleManager.java b/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleManager.java
index b720a25..1a3ad9b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleManager.java
@@ -14,75 +14,90 @@
 
 package com.google.gerrit.lifecycle;
 
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.inject.Binding;
 import com.google.inject.Injector;
+import com.google.inject.Provider;
 import com.google.inject.TypeLiteral;
+import com.google.inject.util.Providers;
 
 import org.slf4j.LoggerFactory;
 
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
 import java.util.List;
 
 /** Tracks and executes registered {@link LifecycleListener}s. */
 public class LifecycleManager {
-  private final LinkedHashMap<LifecycleListener, Boolean> listeners =
-      new LinkedHashMap<LifecycleListener, Boolean>();
+  private final List<Provider<LifecycleListener>> listeners = newList();
+  private final List<RegistrationHandle> handles = newList();
 
-  private boolean started;
+  /** Index of the last listener to start successfully; -1 when not started. */
+  private int startedIndex = -1;
+
+  /** Add a handle that must be cleared during stop. */
+  public void add(RegistrationHandle handle) {
+    handles.add(handle);
+  }
 
   /** Add a single listener. */
-  public void add(final LifecycleListener listener) {
-    listeners.put(listener, true);
+  public void add(LifecycleListener listener) {
+    listeners.add(Providers.of(listener));
+  }
+
+  /** Add a single listener. */
+  public void add(Provider<LifecycleListener> listener) {
+    listeners.add(listener);
   }
 
   /** Add all {@link LifecycleListener}s registered in the Injector. */
-  public void add(final Injector injector) {
-    if (started) {
-      throw new IllegalStateException("Already started");
-    }
-    for (final Binding<LifecycleListener> binding : get(injector)) {
-      add(binding.getProvider().get());
+  public void add(Injector injector) {
+    Preconditions.checkState(startedIndex < 0, "Already started");
+    for (Binding<LifecycleListener> binding : get(injector)) {
+      add(binding.getProvider());
     }
   }
 
   /** Add all {@link LifecycleListener}s registered in the Injectors. */
-  public void add(final Injector... injectors) {
-    for (final Injector i : injectors) {
+  public void add(Injector... injectors) {
+    for (Injector i : injectors) {
       add(i);
     }
   }
 
   /** Start all listeners, in the order they were registered. */
   public void start() {
-    if (!started) {
-      started = true;
-      for (LifecycleListener obj : listeners.keySet()) {
-        obj.start();
-      }
+    for (int i = startedIndex + 1; i < listeners.size(); i++) {
+      LifecycleListener listener = listeners.get(i).get();
+      startedIndex = i;
+      listener.start();
     }
   }
 
   /** Stop all listeners, in the reverse order they were registered. */
   public void stop() {
-    if (started) {
-      final List<LifecycleListener> t =
-          new ArrayList<LifecycleListener>(listeners.keySet());
+    for (int i = handles.size() - 1; 0 <= i; i--) {
+      handles.get(i).remove();
+    }
+    handles.clear();
 
-      for (int i = t.size() - 1; 0 <= i; i--) {
-        final LifecycleListener obj = t.get(i);
-        try {
-          obj.stop();
-        } catch (Throwable err) {
-          LoggerFactory.getLogger(obj.getClass()).warn("Failed to stop", err);
-        }
+    for (int i = startedIndex; 0 <= i; i--) {
+      LifecycleListener obj = listeners.get(i).get();
+      try {
+        obj.stop();
+      } catch (Throwable err) {
+        LoggerFactory.getLogger(obj.getClass()).warn("Failed to stop", err);
       }
-
-      started = false;
+      startedIndex = i - 1;
     }
   }
 
   private static List<Binding<LifecycleListener>> get(Injector i) {
     return i.findBindingsByType(new TypeLiteral<LifecycleListener>() {});
   }
+
+  private static <T> List<T> newList() {
+    return Lists.newArrayListWithCapacity(4);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java b/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java
index dcc8a35..04682f5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java
@@ -1,5 +1,6 @@
 package com.google.gerrit.lifecycle;
 
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.inject.AbstractModule;
 import com.google.inject.Singleton;
 import com.google.inject.binder.LinkedBindingBuilder;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java b/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
index fd72e0c..fbee145 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
@@ -189,6 +189,8 @@
       git = gitMgr.openRepository(project);
     } catch (RepositoryNotFoundException e) {
       throw new CompileException("Cannot open repository " + project, e);
+    } catch (IOException e) {
+      throw new CompileException("Cannot open repository " + project, e);
     }
     try {
       ObjectLoader ldr = git.open(rulesId, Constants.OBJ_BLOB);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
index b9f476b..1185fd3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
@@ -42,6 +43,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 
+import java.io.IOException;
 import java.util.Map;
 
 public final class StoredValues {
@@ -79,8 +81,10 @@
       ObjectId b = ObjectId.fromString(psInfo.getRevId());
       Whitespace ws = Whitespace.IGNORE_NONE;
       PatchListKey plKey = new PatchListKey(projectKey, a, b, ws);
-      PatchList patchList = plCache.get(plKey);
-      if (patchList == null) {
+      PatchList patchList;
+      try {
+        patchList = plCache.get(plKey);
+      } catch (PatchListNotAvailableException e) {
         throw new SystemException("Cannot create " + plKey);
       }
       return patchList;
@@ -100,6 +104,8 @@
         repo = gitMgr.openRepository(projectKey);
       } catch (RepositoryNotFoundException e) {
         throw new SystemException(e.getMessage());
+      } catch (IOException e) {
+        throw new SystemException(e.getMessage());
       }
       env.addToCleanup(new Runnable() {
         @Override
@@ -130,4 +136,4 @@
 
   private StoredValues() {
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java b/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java
index 4d1d631..ae76a6e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java
@@ -26,8 +26,5 @@
   SSH_COMMAND,
 
   /** Access from a Git client using any Git protocol. */
-  GIT,
-
-  /** Access through replication */
-  REPLICATION;
+  GIT;
 }
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 556ae82..a295c49 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
@@ -124,14 +124,14 @@
     Account.Id authorId = info.getAuthor() != null
         ? info.getAuthor().getAccount()
         : null;
-    if (authorId != null) {
+    if (authorId != null && !ps.isDraft()) {
       need.add(authorId);
     }
 
     Account.Id committerId = info.getCommitter() != null
         ? info.getCommitter().getAccount()
         : null;
-    if (committerId != null) {
+    if (committerId != null && !ps.isDraft()) {
       need.add(committerId);
     }
     need.remove(change.getOwner());
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 ec28378..8d31593 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
@@ -29,9 +29,9 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.TrackingFooter;
 import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeOp;
-import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.mail.RebasedPatchSetSender;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
@@ -240,7 +240,7 @@
       RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory,
       final ChangeHookRunner hooks, GitRepositoryManager gitManager,
       final PatchSetInfoFactory patchSetInfoFactory,
-      final ReplicationQueue replication, PersonIdent myIdent,
+      final GitReferenceUpdated replication, PersonIdent myIdent,
       final ChangeControl.Factory changeControlFactory,
       final ApprovalsUtil approvalsUtil) throws NoSuchChangeException,
       EmailException, OrmException, MissingObjectException,
@@ -381,7 +381,7 @@
               + ": " + ru.getResult());
         }
 
-        replication.scheduleUpdate(change.getProject(), ru.getName());
+        replication.fire(change.getProject(), ru.getName());
 
         List<PatchSetApproval> patchSetApprovals = approvalsUtil.copyVetosToLatestPatchSet(change);
 
@@ -424,7 +424,7 @@
       final RevertedSender.Factory revertedSenderFactory,
       final ChangeHooks hooks, GitRepositoryManager gitManager,
       final PatchSetInfoFactory patchSetInfoFactory,
-      final ReplicationQueue replication, PersonIdent myIdent)
+      final GitReferenceUpdated replication, PersonIdent myIdent)
       throws NoSuchChangeException, EmailException, OrmException,
       MissingObjectException, IncorrectObjectTypeException, IOException,
       PatchSetInfoNotAvailableException {
@@ -495,7 +495,7 @@
         throw new IOException("Failed to create ref " + ps.getRefName()
             + " in " + git.getDirectory() + ": " + ru.getResult());
       }
-      replication.scheduleUpdate(db.changes().get(changeId).getProject(),
+      replication.fire(db.changes().get(changeId).getProject(),
           ru.getName());
 
       final ChangeMessage cmsg =
@@ -525,7 +525,7 @@
 
   public static void deleteDraftChange(final PatchSet.Id patchSetId,
       GitRepositoryManager gitManager,
-      final ReplicationQueue replication, final ReviewDb db)
+      final GitReferenceUpdated replication, final ReviewDb db)
       throws NoSuchChangeException, OrmException, IOException {
     final Change.Id changeId = patchSetId.getParentKey();
     final Change change = db.changes().get(changeId);
@@ -546,7 +546,7 @@
 
   public static void deleteOnlyDraftPatchSet(final PatchSet patch,
       final Change change, GitRepositoryManager gitManager,
-      final ReplicationQueue replication, final ReviewDb db)
+      final GitReferenceUpdated replication, final ReviewDb db)
       throws NoSuchChangeException, OrmException, IOException {
     final PatchSet.Id patchSetId = patch.getId();
     if (patch == null || !patch.isDraft()) {
@@ -569,7 +569,7 @@
           throw new IOException("Failed to delete ref " + patch.getRefName() +
               " in " + repo.getDirectory() + ": " + update.getResult());
       }
-      replication.scheduleUpdate(change.getProject(), update.getName());
+      replication.fire(change.getProject(), update.getName());
     } finally {
       repo.close();
     }
@@ -585,12 +585,8 @@
 
   public static <T extends ReplyToChangeSender> void updatedChange(
       final ReviewDb db, final IdentifiedUser user, final Change change,
-      final ChangeMessage cmsg, ReplyToChangeSender.Factory<T> senderFactory,
-      final String err) throws NoSuchChangeException,
-      InvalidChangeOperationException, EmailException, OrmException {
-    if (change == null) {
-      throw new InvalidChangeOperationException(err);
-    }
+      final ChangeMessage cmsg, ReplyToChangeSender.Factory<T> senderFactory)
+      throws NoSuchChangeException, EmailException, OrmException {
     db.changeMessages().insert(Collections.singleton(cmsg));
 
     new ApprovalsUtil(db, null).syncChangeStatus(change);
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
new file mode 100644
index 0000000..e64533c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import com.google.gerrit.reviewdb.client.Account;
+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.server.config.FactoryModule;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.args4j.AccountGroupIdHandler;
+import com.google.gerrit.server.args4j.AccountGroupUUIDHandler;
+import com.google.gerrit.server.args4j.AccountIdHandler;
+import com.google.gerrit.server.args4j.ChangeIdHandler;
+import com.google.gerrit.server.args4j.ObjectIdHandler;
+import com.google.gerrit.server.args4j.PatchSetIdHandler;
+import com.google.gerrit.server.args4j.ProjectControlHandler;
+import com.google.gerrit.server.args4j.SocketAddressHandler;
+import com.google.gerrit.util.cli.CmdLineParser;
+import com.google.gerrit.util.cli.OptionHandlerUtil;
+
+import org.eclipse.jgit.lib.ObjectId;
+
+import org.kohsuke.args4j.spi.OptionHandler;
+
+import java.net.SocketAddress;
+
+public class CmdLineParserModule extends FactoryModule {
+  public CmdLineParserModule() {
+  }
+
+  @Override
+  protected void configure() {
+    factory(CmdLineParser.Factory.class);
+
+    registerOptionHandler(Account.Id.class, AccountIdHandler.class);
+    registerOptionHandler(AccountGroup.Id.class, AccountGroupIdHandler.class);
+    registerOptionHandler(AccountGroup.UUID.class, AccountGroupUUIDHandler.class);
+    registerOptionHandler(Change.Id.class, ChangeIdHandler.class);
+    registerOptionHandler(ObjectId.class, ObjectIdHandler.class);
+    registerOptionHandler(PatchSet.Id.class, PatchSetIdHandler.class);
+    registerOptionHandler(ProjectControl.class, ProjectControlHandler.class);
+    registerOptionHandler(SocketAddress.class, SocketAddressHandler.class);
+  }
+
+  private <T> void registerOptionHandler(Class<T> type,
+      Class<? extends OptionHandler<T>> impl) {
+    install(OptionHandlerUtil.moduleFor(type, impl));
+  }
+}
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 6e519c4..050f7e1 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -24,8 +25,9 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.account.MaterializedGroupMembership;
+import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AuthConfig;
@@ -46,13 +48,10 @@
 import java.net.MalformedURLException;
 import java.net.SocketAddress;
 import java.net.URL;
-import java.util.AbstractSet;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
 import java.util.TimeZone;
@@ -70,7 +69,7 @@
     private final Provider<String> canonicalUrl;
     private final Realm realm;
     private final AccountCache accountCache;
-    private final MaterializedGroupMembership.Factory groupMembershipFactory;
+    private final GroupBackend groupBackend;
 
     @Inject
     GenericFactory(
@@ -79,14 +78,14 @@
         final @AnonymousCowardName String anonymousCowardName,
         final @CanonicalWebUrl Provider<String> canonicalUrl,
         final Realm realm, final AccountCache accountCache,
-        final MaterializedGroupMembership.Factory groupMembershipFactory) {
+        final GroupBackend groupBackend) {
       this.capabilityControlFactory = capabilityControlFactory;
       this.authConfig = authConfig;
       this.anonymousCowardName = anonymousCowardName;
       this.canonicalUrl = canonicalUrl;
       this.realm = realm;
       this.accountCache = accountCache;
-      this.groupMembershipFactory = groupMembershipFactory;
+      this.groupBackend = groupBackend;
     }
 
     public IdentifiedUser create(final Account.Id id) {
@@ -96,14 +95,14 @@
     public IdentifiedUser create(Provider<ReviewDb> db, Account.Id id) {
       return new IdentifiedUser(capabilityControlFactory, AccessPath.UNKNOWN,
           authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
-          groupMembershipFactory, null, db, id);
+          groupBackend, null, db, id);
     }
 
     public IdentifiedUser create(AccessPath accessPath,
         Provider<SocketAddress> remotePeerProvider, Account.Id id) {
       return new IdentifiedUser(capabilityControlFactory, accessPath,
           authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
-          groupMembershipFactory, remotePeerProvider, null, id);
+          groupBackend, remotePeerProvider, null, id);
     }
   }
 
@@ -121,7 +120,7 @@
     private final Provider<String> canonicalUrl;
     private final Realm realm;
     private final AccountCache accountCache;
-    private final MaterializedGroupMembership.Factory groupMembershipFactory;
+    private final GroupBackend groupBackend;
 
     private final Provider<SocketAddress> remotePeerProvider;
     private final Provider<ReviewDb> dbProvider;
@@ -133,7 +132,7 @@
         final @AnonymousCowardName String anonymousCowardName,
         final @CanonicalWebUrl Provider<String> canonicalUrl,
         final Realm realm, final AccountCache accountCache,
-        final MaterializedGroupMembership.Factory groupMembershipFactory,
+        final GroupBackend groupBackend,
 
         final @RemotePeer Provider<SocketAddress> remotePeerProvider,
         final Provider<ReviewDb> dbProvider) {
@@ -143,7 +142,7 @@
       this.canonicalUrl = canonicalUrl;
       this.realm = realm;
       this.accountCache = accountCache;
-      this.groupMembershipFactory = groupMembershipFactory;
+      this.groupBackend = groupBackend;
 
       this.remotePeerProvider = remotePeerProvider;
       this.dbProvider = dbProvider;
@@ -153,40 +152,22 @@
         final Account.Id id) {
       return new IdentifiedUser(capabilityControlFactory, accessPath,
           authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
-          groupMembershipFactory, remotePeerProvider, dbProvider, id);
+          groupBackend, remotePeerProvider, dbProvider, id);
     }
   }
 
   private static final Logger log =
       LoggerFactory.getLogger(IdentifiedUser.class);
 
-  private static final Set<AccountGroup.UUID> registeredGroups =
-      new AbstractSet<AccountGroup.UUID>() {
-        private final List<AccountGroup.UUID> groups =
-            Collections.unmodifiableList(Arrays.asList(new AccountGroup.UUID[] {
-                AccountGroup.ANONYMOUS_USERS, AccountGroup.REGISTERED_USERS}));
-
-        @Override
-        public boolean contains(Object o) {
-          return groups.contains(o);
-        }
-
-        @Override
-        public Iterator<AccountGroup.UUID> iterator() {
-          return groups.iterator();
-        }
-
-        @Override
-        public int size() {
-          return groups.size();
-        }
-      };
+  private static final GroupMembership registeredGroups =
+      new ListGroupMembership(ImmutableSet.of(
+          AccountGroup.ANONYMOUS_USERS,
+          AccountGroup.REGISTERED_USERS));
 
   private final Provider<String> canonicalUrl;
-  private final Realm realm;
   private final AccountCache accountCache;
-  private final MaterializedGroupMembership.Factory groupMembershipFactory;
   private final AuthConfig authConfig;
+  private final GroupBackend groupBackend;
   private final String anonymousCowardName;
 
   @Nullable
@@ -210,14 +191,13 @@
       final String anonymousCowardName,
       final Provider<String> canonicalUrl,
       final Realm realm, final AccountCache accountCache,
-      final MaterializedGroupMembership.Factory groupMembershipFactory,
+      final GroupBackend groupBackend,
       @Nullable final Provider<SocketAddress> remotePeerProvider,
       @Nullable final Provider<ReviewDb> dbProvider, final Account.Id id) {
     super(capabilityControlFactory, accessPath);
     this.canonicalUrl = canonicalUrl;
-    this.realm = realm;
     this.accountCache = accountCache;
-    this.groupMembershipFactory = groupMembershipFactory;
+    this.groupBackend = groupBackend;
     this.authConfig = authConfig;
     this.anonymousCowardName = anonymousCowardName;
     this.remotePeerProvider = remotePeerProvider;
@@ -225,7 +205,8 @@
     this.accountId = id;
   }
 
-  private AccountState state() {
+  // TODO(cranger): maybe get the state through the accountCache instead.
+  public AccountState state() {
     if (state == null) {
       state = accountCache.get(getAccountId());
     }
@@ -272,12 +253,11 @@
   public GroupMembership getEffectiveGroups() {
     if (effectiveGroups == null) {
       if (authConfig.isIdentityTrustable(state().getExternalIds())) {
-        effectiveGroups = realm.groups(state());
+        effectiveGroups = groupBackend.membershipsOf(this);
       } else {
-        effectiveGroups = groupMembershipFactory.create(registeredGroups);
+        effectiveGroups = registeredGroups;
       }
     }
-
     return effectiveGroups;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
new file mode 100644
index 0000000..eba59c8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.inject.Inject;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * User identity for plugin code that needs an identity.
+ * <p>
+ * An InternalUser has no real identity, it acts as the server and can access
+ * anything it wants, anytime it wants, given the JVM's own direct access to
+ * data. Plugins may use this when they need to have a CurrentUser with read
+ * permission on anything.
+ */
+public class InternalUser extends CurrentUser {
+  public interface Factory {
+    InternalUser create();
+  }
+
+  @Inject
+  protected InternalUser(CapabilityControl.Factory capabilityControlFactory) {
+    super(capabilityControlFactory, AccessPath.UNKNOWN);
+  }
+
+  @Override
+  public GroupMembership getEffectiveGroups() {
+    return GroupMembership.EMPTY;
+  }
+
+  @Override
+  public Set<Change.Id> getStarredChanges() {
+    return Collections.emptySet();
+  }
+
+  @Override
+  public Collection<AccountProjectWatch> getNotificationFilters() {
+    return Collections.emptySet();
+  }
+
+  @Override
+  public String toString() {
+    return "InternalUser";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java
index 8847f96..ea1f1d1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java
@@ -29,7 +29,8 @@
    *
    * @param repoManager Git repository manager to open the git repository
    * @param branch the branch for which it should be checked if it exists
-   * @return <code>true</code> if the specified branch exists, otherwise
+   * @return <code>true</code> if the specified branch exists or if
+   *         <code>HEAD</code> points to this branch, otherwise
    *         <code>false</code>
    * @throws RepositoryNotFoundException the repository of the branch's project
    *         does not exist.
@@ -40,7 +41,11 @@
       IOException {
     final Repository repo = repoManager.openRepository(branch.getParentKey());
     try {
-      return repo.getRef(branch.get()) != null;
+      boolean exists = repo.getRef(branch.get()) != null;
+      if (!exists) {
+        exists = repo.getFullBranch().equals(branch.get());
+      }
+      return exists;
     } finally {
       repo.close();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java
deleted file mode 100644
index fc6ac9c..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.account.CapabilityControl;
-import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.account.ListGroupMembership;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Set;
-
-public class ReplicationUser extends CurrentUser {
-  /** Magic set of groups enabling read of any project and reference. */
-  public static final GroupMembership EVERYTHING_VISIBLE =
-      new ListGroupMembership(Collections.<AccountGroup.UUID>emptySet());
-
-  public interface Factory {
-    ReplicationUser create(@Assisted GroupMembership authGroups);
-  }
-
-  private final GroupMembership effectiveGroups;
-
-  @Inject
-  protected ReplicationUser(CapabilityControl.Factory capabilityControlFactory,
-      @Assisted GroupMembership authGroups) {
-    super(capabilityControlFactory, AccessPath.REPLICATION);
-    effectiveGroups = authGroups;
-  }
-
-  @Override
-  public GroupMembership getEffectiveGroups() {
-    return effectiveGroups;
-  }
-
-  @Override
-  public Set<Change.Id> getStarredChanges() {
-    return Collections.emptySet();
-  }
-
-  @Override
-  public Collection<AccountProjectWatch> getNotificationFilters() {
-    return Collections.emptySet();
-  }
-
-  public boolean isEverythingVisible() {
-    return getEffectiveGroups() == EVERYTHING_VISIBLE;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java
new file mode 100644
index 0000000..fe1072d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+public class StringUtil {
+  /**
+   * An array of the string representations that should be used in place
+   * of the non-printable characters in the beginning of the ASCII table
+   * when escaping a string. The index of each element in the array
+   * corresponds to its ASCII value, i.e. the string representation of
+   * ASCII 0 is found in the first element of this array.
+   */
+  static String[] NON_PRINTABLE_CHARS =
+    { "\\x00", "\\x01", "\\x02", "\\x03", "\\x04", "\\x05", "\\x06", "\\a",
+      "\\b",   "\\t",   "\\n",   "\\v",   "\\f",   "\\r",   "\\x0e", "\\x0f",
+      "\\x10", "\\x11", "\\x12", "\\x13", "\\x14", "\\x15", "\\x16", "\\x17",
+      "\\x18", "\\x19", "\\x1a", "\\x1b", "\\x1c", "\\x1d", "\\x1e", "\\x1f" };
+
+  /**
+   * Escapes the input string so that all non-printable characters
+   * (0x00-0x1f) are represented as a hex escape (\x00, \x01, ...)
+   * or as a C-style escape sequence (\a, \b, \t, \n, \v, \f, or \r).
+   * Backslashes in the input string are doubled (\\).
+   */
+  public static String escapeString(final String str) {
+    // Allocate a buffer big enough to cover the case with a string needed
+    // very excessive escaping without having to reallocate the buffer.
+    final StringBuilder result = new StringBuilder(3 * str.length());
+
+    for (int i = 0; i < str.length(); i++) {
+      char c = str.charAt(i);
+      if (c < NON_PRINTABLE_CHARS.length) {
+        result.append(NON_PRINTABLE_CHARS[c]);
+      } else if (c == '\\') {
+        result.append("\\\\");
+      } else {
+        result.append(c);
+      }
+    }
+    return result.toString();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
index 72fb2e8..4827ed5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
@@ -14,12 +14,14 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -27,45 +29,58 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
 
 /** Translates an email address to a set of matching accounts. */
 @Singleton
 public class AccountByEmailCacheImpl implements AccountByEmailCache {
+  private static final Logger log = LoggerFactory
+      .getLogger(AccountByEmailCacheImpl.class);
   private static final String CACHE_NAME = "accounts_byemail";
 
   public static Module module() {
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<String, Set<Account.Id>>> type =
-            new TypeLiteral<Cache<String, Set<Account.Id>>>() {};
-        core(type, CACHE_NAME).populateWith(Loader.class);
+        cache(CACHE_NAME,
+            String.class,
+            new TypeLiteral<Set<Account.Id>>() {})
+          .loader(Loader.class);
         bind(AccountByEmailCacheImpl.class);
         bind(AccountByEmailCache.class).to(AccountByEmailCacheImpl.class);
       }
     };
   }
 
-  private final Cache<String, Set<Account.Id>> cache;
+  private final LoadingCache<String, Set<Account.Id>> cache;
 
   @Inject
   AccountByEmailCacheImpl(
-      @Named(CACHE_NAME) final Cache<String, Set<Account.Id>> cache) {
+      @Named(CACHE_NAME) LoadingCache<String, Set<Account.Id>> cache) {
     this.cache = cache;
   }
 
   public Set<Account.Id> get(final String email) {
-    return cache.get(email);
+    try {
+      return cache.get(email);
+    } catch (ExecutionException e) {
+      log.warn("Cannot resolve accounts by email", e);
+      return Collections.emptySet();
+    }
   }
 
   public void evict(final String email) {
-    cache.remove(email);
+    if (email != null) {
+      cache.invalidate(email);
+    }
   }
 
-  static class Loader extends EntryCreator<String, Set<Account.Id>> {
+  static class Loader extends CacheLoader<String, Set<Account.Id>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
@@ -74,10 +89,10 @@
     }
 
     @Override
-    public Set<Account.Id> createEntry(final String email) throws Exception {
+    public Set<Account.Id> load(String email) throws Exception {
       final ReviewDb db = schema.open();
       try {
-        final HashSet<Account.Id> r = new HashSet<Account.Id>();
+        Set<Account.Id> r = Sets.newHashSet();
         for (Account a : db.accounts().byPreferredEmail(email)) {
           r.add(a.getId());
         }
@@ -85,30 +100,10 @@
             .byEmailAddress(email)) {
           r.add(a.getAccountId());
         }
-        return pack(r);
+        return ImmutableSet.copyOf(r);
       } finally {
         db.close();
       }
     }
-
-    @Override
-    public Set<Account.Id> missing(final String key) {
-      return Collections.emptySet();
-    }
-
-    private static Set<Account.Id> pack(final Set<Account.Id> c) {
-      switch (c.size()) {
-        case 0:
-          return Collections.emptySet();
-        case 1:
-          return one(c);
-        default:
-          return Collections.unmodifiableSet(new HashSet<Account.Id>(c));
-      }
-    }
-
-    private static <T> Set<T> one(final Set<T> c) {
-      return Collections.singleton(c.iterator().next());
-    }
   }
 }
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 819ec31..4217f9f 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
@@ -14,14 +14,16 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.base.Optional;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableSet;
 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.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -30,14 +32,21 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
 
 /** Caches important (but small) account state to avoid database hits. */
 @Singleton
 public class AccountCacheImpl implements AccountCache {
+  private static final Logger log = LoggerFactory
+      .getLogger(AccountCacheImpl.class);
+
   private static final String BYID_NAME = "accounts";
   private static final String BYUSER_NAME = "accounts_byname";
 
@@ -45,13 +54,13 @@
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<Account.Id, AccountState>> byIdType =
-            new TypeLiteral<Cache<Account.Id, AccountState>>() {};
-        core(byIdType, BYID_NAME).populateWith(ByIdLoader.class);
+        cache(BYID_NAME, Account.Id.class, AccountState.class)
+          .loader(ByIdLoader.class);
 
-        final TypeLiteral<Cache<String, Account.Id>> byUsernameType =
-            new TypeLiteral<Cache<String, Account.Id>>() {};
-        core(byUsernameType, BYUSER_NAME).populateWith(ByNameLoader.class);
+        cache(BYUSER_NAME,
+            String.class,
+            new TypeLiteral<Optional<Account.Id>>() {})
+          .loader(ByNameLoader.class);
 
         bind(AccountCacheImpl.class);
         bind(AccountCache.class).to(AccountCacheImpl.class);
@@ -59,54 +68,76 @@
     };
   }
 
-  private final Cache<Account.Id, AccountState> byId;
-  private final Cache<String, Account.Id> byName;
+  private final LoadingCache<Account.Id, AccountState> byId;
+  private final LoadingCache<String, Optional<Account.Id>> byName;
 
   @Inject
-  AccountCacheImpl(@Named(BYID_NAME) Cache<Account.Id, AccountState> byId,
-      @Named(BYUSER_NAME) Cache<String, Account.Id> byUsername) {
+  AccountCacheImpl(@Named(BYID_NAME) LoadingCache<Account.Id, AccountState> byId,
+      @Named(BYUSER_NAME) LoadingCache<String, Optional<Account.Id>> byUsername) {
     this.byId = byId;
     this.byName = byUsername;
   }
 
-  public AccountState get(final Account.Id accountId) {
-    return byId.get(accountId);
+  public AccountState get(Account.Id accountId) {
+    try {
+      return byId.get(accountId);
+    } catch (ExecutionException e) {
+      log.warn("Cannot load AccountState for " + accountId, e);
+      return missing(accountId);
+    }
   }
 
   @Override
   public AccountState getByUsername(String username) {
-    Account.Id id = byName.get(username);
-    return id != null ? byId.get(id) : null;
+    try {
+      Optional<Account.Id> id = byName.get(username);
+      return id != null && id.isPresent() ? byId.get(id.get()) : null;
+    } catch (ExecutionException e) {
+      log.warn("Cannot load AccountState for " + username, e);
+      return null;
+    }
   }
 
-  public void evict(final Account.Id accountId) {
-    byId.remove(accountId);
+  public void evict(Account.Id accountId) {
+    if (accountId != null) {
+      byId.invalidate(accountId);
+    }
   }
 
   public void evictByUsername(String username) {
-    byName.remove(username);
+    if (username != null) {
+      byName.invalidate(username);
+    }
   }
 
-  static class ByIdLoader extends EntryCreator<Account.Id, AccountState> {
+  private static AccountState missing(Account.Id accountId) {
+    Account account = new Account(accountId);
+    Collection<AccountExternalId> ids = Collections.emptySet();
+    Set<AccountGroup.UUID> anon = ImmutableSet.of(AccountGroup.ANONYMOUS_USERS);
+    return new AccountState(account, anon, ids);
+  }
+
+  static class ByIdLoader extends CacheLoader<Account.Id, AccountState> {
     private final SchemaFactory<ReviewDb> schema;
     private final GroupCache groupCache;
-    private final Cache<String, Account.Id> byName;
+    private final LoadingCache<String, Optional<Account.Id>> byName;
 
     @Inject
     ByIdLoader(SchemaFactory<ReviewDb> sf, GroupCache groupCache,
-        @Named(BYUSER_NAME) Cache<String, Account.Id> byUsername) {
+        @Named(BYUSER_NAME) LoadingCache<String, Optional<Account.Id>> byUsername) {
       this.schema = sf;
       this.groupCache = groupCache;
       this.byName = byUsername;
     }
 
     @Override
-    public AccountState createEntry(final Account.Id key) throws Exception {
+    public AccountState load(Account.Id key) throws Exception {
       final ReviewDb db = schema.open();
       try {
         final AccountState state = load(db, key);
-        if (state.getUserName() != null) {
-          byName.put(state.getUserName(), state.getAccount().getId());
+        String user = state.getUserName();
+        if (user != null) {
+          byName.put(user, Optional.of(state.getAccount().getId()));
         }
         return state;
       } finally {
@@ -142,18 +173,9 @@
 
       return new AccountState(account, internalGroups, externalIds);
     }
-
-    @Override
-    public AccountState missing(final Account.Id accountId) {
-      final Account account = new Account(accountId);
-      final Collection<AccountExternalId> ids = Collections.emptySet();
-      final Set<AccountGroup.UUID> anonymous =
-          Collections.singleton(AccountGroup.ANONYMOUS_USERS);
-      return new AccountState(account, anonymous, ids);
-    }
   }
 
-  static class ByNameLoader extends EntryCreator<String, Account.Id> {
+  static class ByNameLoader extends CacheLoader<String, Optional<Account.Id>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
@@ -162,14 +184,17 @@
     }
 
     @Override
-    public Account.Id createEntry(final String username) throws Exception {
+    public Optional<Account.Id> load(String username) throws Exception {
       final ReviewDb db = schema.open();
       try {
         final AccountExternalId.Key key = new AccountExternalId.Key( //
             AccountExternalId.SCHEME_USERNAME, //
             username);
         final AccountExternalId id = db.accountExternalIds().get(key);
-        return id != null ? id.getAccountId() : null;
+        if (id != null) {
+          return Optional.of(id.getAccountId());
+        }
+        return Optional.absent();
       } finally {
         db.close();
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
index e297ad7..32b4e2c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
@@ -83,10 +83,21 @@
    * effective groups.
    */
   public boolean canSee(final Account otherUser) {
+    return canSee(otherUser.getId());
+  }
+
+  /**
+   * Returns true if the otherUser is allowed to see the current user, based
+   * on the account visibility policy. Depending on the group membership
+   * realms supported, this may not be able to determine SAME_GROUP or
+   * VISIBLE_GROUP correctly (defaulting to not being visible). This is because
+   * {@link GroupMembership#getKnownGroups()} may only return a subset of the
+   * effective groups.
+   */
+  public boolean canSee(final Account.Id otherUser) {
     // Special case: I can always see myself.
     if (currentUser instanceof IdentifiedUser
-        && ((IdentifiedUser) currentUser).getAccountId()
-            .equals(otherUser.getId())) {
+        && ((IdentifiedUser) currentUser).getAccountId().equals(otherUser)) {
       return true;
     }
 
@@ -131,7 +142,7 @@
     return false;
   }
 
-  private Set<AccountGroup.UUID> groupsOf(Account account) {
-    return userFactory.create(account.getId()).getEffectiveGroups().getKnownGroups();
+  private Set<AccountGroup.UUID> groupsOf(Account.Id account) {
+    return userFactory.create(account).getEffectiveGroups().getKnownGroups();
   }
 }
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 eb42921..1524185 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
@@ -83,7 +83,7 @@
       || canAdministrateServer();
   }
 
-  /** @return true if the user can create a group. */
+  /** @return true if the user can create a project. */
   public boolean canCreateProject() {
     return canPerform(GlobalCapability.CREATE_PROJECT)
       || canAdministrateServer();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
index 844e604..c90f3e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -15,25 +15,20 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.inject.Inject;
 
-import java.util.Collections;
 import java.util.Set;
 
 public class DefaultRealm implements Realm {
   private final EmailExpander emailExpander;
   private final AccountByEmailCache byEmail;
-  private final MaterializedGroupMembership.Factory groupMembershipFactory;
 
   @Inject
   DefaultRealm(final EmailExpander emailExpander,
-      final AccountByEmailCache byEmail,
-      final MaterializedGroupMembership.Factory groupMembershipFactory) {
+      final AccountByEmailCache byEmail) {
     this.emailExpander = emailExpander;
     this.byEmail = byEmail;
-    this.groupMembershipFactory = groupMembershipFactory;
   }
 
   @Override
@@ -65,11 +60,6 @@
   }
 
   @Override
-  public GroupMembership groups(final AccountState who) {
-    return groupMembershipFactory.create(who.getInternalGroups());
-  }
-
-  @Override
   public Account.Id lookup(final String accountName) {
     if (emailExpander.canExpand(accountName)) {
       final Set<Account.Id> c = byEmail.get(emailExpander.expand(accountName));
@@ -79,9 +69,4 @@
     }
     return null;
   }
-
-  @Override
-  public Set<AccountGroup.ExternalNameKey> lookupGroups(String name) {
-    return Collections.emptySet();
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java
new file mode 100644
index 0000000..b4e770f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.IdentifiedUser;
+
+import java.util.Collection;
+
+import javax.annotation.Nullable;
+
+/**
+ * Implementations of GroupBackend provide lookup and membership accessors
+ * to a group system.
+ */
+@ExtensionPoint
+public interface GroupBackend {
+  /** @return {@code true} if the backend can operate on the UUID. */
+  boolean handles(AccountGroup.UUID uuid);
+
+  /**
+   * Looks up a group in the backend. If the group does not exist, null is
+   * returned.
+   *
+   * @param uuid the group identifier
+   * @return the group
+   */
+  @Nullable
+  GroupDescription.Basic get(AccountGroup.UUID uuid);
+
+  /** @return suggestions for the group name sorted by name. */
+  Collection<GroupReference> suggest(String name);
+
+  /** @return the group membership checker for the backend. */
+  GroupMembership membershipsOf(IdentifiedUser user);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java
new file mode 100644
index 0000000..cdbb0e4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.data.GroupReference;
+
+import java.util.Collection;
+import java.util.Comparator;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utility class for dealing with a GroupBackend.
+ */
+public class GroupBackends {
+
+  public static final Comparator<GroupReference> GROUP_REF_NAME_COMPARATOR =
+      new Comparator<GroupReference>() {
+    @Override
+    public int compare(GroupReference a, GroupReference b) {
+      return a.getName().compareTo(b.getName());
+    }
+  };
+
+  /**
+   * Runs {@link GroupBackend#suggest(String)} and filters the result to return
+   * the best suggestion, or null if one does not exist.
+   *
+   * @param groupBackend the group backend
+   * @param name the name for which to suggest groups
+   * @return the best single GroupReference suggestion
+   */
+  @Nullable
+  public static GroupReference findBestSuggestion(
+      GroupBackend groupBackend, String name) {
+    Collection<GroupReference> refs = groupBackend.suggest(name);
+    if (refs.size() == 1) {
+      return Iterables.getOnlyElement(refs);
+    }
+
+    for (GroupReference ref : refs) {
+      if (isExactSuggestion(ref, name)) {
+        return ref;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Runs {@link GroupBackend#suggest(String)} and filters the result to return
+   * the exact suggestion, or null if one does not exist.
+   *
+   * @param groupBackend the group backend
+   * @param name the name for which to suggest groups
+   * @return the exact single GroupReference suggestion
+   */
+  @Nullable
+  public static GroupReference findExactSuggestion(
+      GroupBackend groupBackend, String name) {
+    Collection<GroupReference> refs = groupBackend.suggest(name);
+    for (GroupReference ref : refs) {
+      if (isExactSuggestion(ref, name)) {
+        return ref;
+      }
+    }
+    return null;
+  }
+
+  /** Returns whether the GroupReference is an exact suggestion for the name. */
+  public static boolean isExactSuggestion(GroupReference ref, String name) {
+    return ref.getName().equalsIgnoreCase(name) || ref.getUUID().get().equals(name);
+  }
+
+  private GroupBackends() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
index b092ac4..3b9e85f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
@@ -16,8 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
 
-import java.util.Collection;
-
 import javax.annotation.Nullable;
 
 /** Tracks group objects in memory for efficient access. */
@@ -34,8 +32,6 @@
   @Nullable
   public AccountGroup get(AccountGroup.UUID uuid);
 
-  public Collection<AccountGroup> get(AccountGroup.ExternalNameKey externalName);
-
   /** @return sorted iteration of groups. */
   public abstract Iterable<AccountGroup> all();
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
index d29a5e5..b301839 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -14,12 +14,15 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.base.Optional;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupName;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -27,48 +30,41 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
-import java.util.ArrayList;
-import java.util.Collection;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.util.Collections;
 import java.util.List;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
+import java.util.concurrent.ExecutionException;
 
 /** Tracks group objects in memory for efficient access. */
 @Singleton
 public class GroupCacheImpl implements GroupCache {
+  private static final Logger log = LoggerFactory
+      .getLogger(GroupCacheImpl.class);
+
   private static final String BYID_NAME = "groups";
   private static final String BYNAME_NAME = "groups_byname";
   private static final String BYUUID_NAME = "groups_byuuid";
-  private static final String BYEXT_NAME = "groups_byext";
-  private static final String BYNAME_LIST = "groups_byname_list";
 
   public static Module module() {
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<AccountGroup.Id, AccountGroup>> byId =
-            new TypeLiteral<Cache<AccountGroup.Id, AccountGroup>>() {};
-        core(byId, BYID_NAME).populateWith(ByIdLoader.class);
+        cache(BYID_NAME,
+            AccountGroup.Id.class,
+            new TypeLiteral<Optional<AccountGroup>>() {})
+          .loader(ByIdLoader.class);
 
-        final TypeLiteral<Cache<AccountGroup.NameKey, AccountGroup>> byName =
-            new TypeLiteral<Cache<AccountGroup.NameKey, AccountGroup>>() {};
-        core(byName, BYNAME_NAME).populateWith(ByNameLoader.class);
+        cache(BYNAME_NAME,
+            String.class,
+            new TypeLiteral<Optional<AccountGroup>>() {})
+          .loader(ByNameLoader.class);
 
-        final TypeLiteral<Cache<AccountGroup.UUID, AccountGroup>> byUUID =
-            new TypeLiteral<Cache<AccountGroup.UUID, AccountGroup>>() {};
-        core(byUUID, BYUUID_NAME).populateWith(ByUUIDLoader.class);
-
-        final TypeLiteral<Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>>> byExternalName =
-            new TypeLiteral<Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>>>() {};
-        core(byExternalName, BYEXT_NAME) //
-            .populateWith(ByExternalNameLoader.class);
-
-        final TypeLiteral<Cache<ListKey, SortedSet<AccountGroup.NameKey>>> listType =
-          new TypeLiteral<Cache<ListKey, SortedSet<AccountGroup.NameKey>>>() {};
-        core(listType, BYNAME_LIST).populateWith(Lister.class);
+        cache(BYUUID_NAME,
+            String.class,
+            new TypeLiteral<Optional<AccountGroup>>() {})
+          .loader(ByUUIDLoader.class);
 
         bind(GroupCacheImpl.class);
         bind(GroupCache.class).to(GroupCacheImpl.class);
@@ -76,94 +72,113 @@
     };
   }
 
-  private final Cache<AccountGroup.Id, AccountGroup> byId;
-  private final Cache<AccountGroup.NameKey, AccountGroup> byName;
-  private final Cache<AccountGroup.UUID, AccountGroup> byUUID;
-  private final Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>> byExternalName;
-  private final Cache<ListKey,SortedSet<AccountGroup.NameKey>> list;
-  private final Lock listLock;
+  private final LoadingCache<AccountGroup.Id, Optional<AccountGroup>> byId;
+  private final LoadingCache<String, Optional<AccountGroup>> byName;
+  private final LoadingCache<String, Optional<AccountGroup>> byUUID;
+  private final SchemaFactory<ReviewDb> schema;
 
   @Inject
   GroupCacheImpl(
-      @Named(BYID_NAME) Cache<AccountGroup.Id, AccountGroup> byId,
-      @Named(BYNAME_NAME) Cache<AccountGroup.NameKey, AccountGroup> byName,
-      @Named(BYUUID_NAME) Cache<AccountGroup.UUID, AccountGroup> byUUID,
-      @Named(BYEXT_NAME) Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>> byExternalName,
-      @Named(BYNAME_LIST) final Cache<ListKey, SortedSet<AccountGroup.NameKey>> list) {
+      @Named(BYID_NAME) LoadingCache<AccountGroup.Id, Optional<AccountGroup>> byId,
+      @Named(BYNAME_NAME) LoadingCache<String, Optional<AccountGroup>> byName,
+      @Named(BYUUID_NAME) LoadingCache<String, Optional<AccountGroup>> byUUID,
+      SchemaFactory<ReviewDb> schema) {
     this.byId = byId;
     this.byName = byName;
     this.byUUID = byUUID;
-    this.byExternalName = byExternalName;
-    this.list = list;
-    this.listLock = new ReentrantLock(true /* fair */);
+    this.schema = schema;
   }
 
+  @Override
   public AccountGroup get(final AccountGroup.Id groupId) {
-    return byId.get(groupId);
+    try {
+      Optional<AccountGroup> g = byId.get(groupId);
+      return g.isPresent() ? g.get() : missing(groupId);
+    } catch (ExecutionException e) {
+      log.warn("Cannot load group "+groupId, e);
+      return missing(groupId);
+    }
   }
 
+  @Override
   public void evict(final AccountGroup group) {
-    byId.remove(group.getId());
-    byName.remove(group.getNameKey());
-    byUUID.remove(group.getGroupUUID());
-    byExternalName.remove(group.getExternalNameKey());
+    if (group.getId() != null) {
+      byId.invalidate(group.getId());
+    }
+    if (group.getNameKey() != null) {
+      byName.invalidate(group.getNameKey().get());
+    }
+    if (group.getGroupUUID() != null) {
+      byUUID.invalidate(group.getGroupUUID().get());
+    }
   }
 
+  @Override
   public void evictAfterRename(final AccountGroup.NameKey oldName,
       final AccountGroup.NameKey newName) {
-    byName.remove(oldName);
-    updateGroupList(oldName, newName);
+    if (oldName != null) {
+      byName.invalidate(oldName.get());
+    }
+    if (newName != null) {
+      byName.invalidate(newName.get());
+    }
   }
 
-  public AccountGroup get(final AccountGroup.NameKey name) {
-    return byName.get(name);
+  @Override
+  public AccountGroup get(AccountGroup.NameKey name) {
+    if (name == null) {
+      return null;
+    }
+    try {
+      return byName.get(name.get()).orNull();
+    } catch (ExecutionException e) {
+      log.warn(String.format("Cannot lookup group %s by name", name.get()), e);
+      return null;
+    }
   }
 
-  public AccountGroup get(final AccountGroup.UUID uuid) {
-    return byUUID.get(uuid);
-  }
-
-  public Collection<AccountGroup> get(
-      final AccountGroup.ExternalNameKey externalName) {
-    return byExternalName.get(externalName);
+  @Override
+  public AccountGroup get(AccountGroup.UUID uuid) {
+    if (uuid == null) {
+      return null;
+    }
+    try {
+      return byUUID.get(uuid.get()).orNull();
+    } catch (ExecutionException e) {
+      log.warn(String.format("Cannot lookup group %s by name", uuid.get()), e);
+      return null;
+    }
   }
 
   @Override
   public Iterable<AccountGroup> all() {
-    final List<AccountGroup> groups = new ArrayList<AccountGroup>();
-    for (final AccountGroup.NameKey groupName : list.get(ListKey.ALL)) {
-      final AccountGroup group = get(groupName);
-      if (group != null) {
-        groups.add(group);
+    try {
+      ReviewDb db = schema.open();
+      try {
+        return Collections.unmodifiableList(db.accountGroups().all().toList());
+      } finally {
+        db.close();
       }
+    } catch (OrmException e) {
+      log.warn("Cannot list internal groups", e);
+      return Collections.emptyList();
     }
-    return Collections.unmodifiableList(groups);
   }
 
   @Override
-  public void onCreateGroup(final AccountGroup.NameKey newGroupName) {
-    updateGroupList(null, newGroupName);
+  public void onCreateGroup(AccountGroup.NameKey newGroupName) {
+    byName.invalidate(newGroupName.get());
   }
 
-  private void updateGroupList(final AccountGroup.NameKey nameToRemove,
-      final AccountGroup.NameKey nameToAdd) {
-    listLock.lock();
-    try {
-      SortedSet<AccountGroup.NameKey> n = list.get(ListKey.ALL);
-      n = new TreeSet<AccountGroup.NameKey>(n);
-      if (nameToRemove != null) {
-        n.remove(nameToRemove);
-      }
-      if (nameToAdd != null) {
-        n.add(nameToAdd);
-      }
-      list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n));
-    } finally {
-      listLock.unlock();
-    }
+  private static AccountGroup missing(AccountGroup.Id key) {
+    AccountGroup.NameKey name = new AccountGroup.NameKey("Deleted Group" + key);
+    AccountGroup g = new AccountGroup(name, key, null);
+    g.setType(AccountGroup.Type.SYSTEM);
+    return g;
   }
 
-  static class ByIdLoader extends EntryCreator<AccountGroup.Id, AccountGroup> {
+  static class ByIdLoader extends
+      CacheLoader<AccountGroup.Id, Optional<AccountGroup>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
@@ -172,32 +187,18 @@
     }
 
     @Override
-    public AccountGroup createEntry(final AccountGroup.Id key) throws Exception {
+    public Optional<AccountGroup> load(final AccountGroup.Id key)
+        throws Exception {
       final ReviewDb db = schema.open();
       try {
-        final AccountGroup group = db.accountGroups().get(key);
-        if (group != null) {
-          return group;
-        } else {
-          return missing(key);
-        }
+        return Optional.fromNullable(db.accountGroups().get(key));
       } finally {
         db.close();
       }
     }
-
-    @Override
-    public AccountGroup missing(final AccountGroup.Id key) {
-      final AccountGroup.NameKey name =
-          new AccountGroup.NameKey("Deleted Group" + key.toString());
-      final AccountGroup g = new AccountGroup(name, key, null);
-      g.setType(AccountGroup.Type.SYSTEM);
-      return g;
-    }
   }
 
-  static class ByNameLoader extends
-      EntryCreator<AccountGroup.NameKey, AccountGroup> {
+  static class ByNameLoader extends CacheLoader<String, Optional<AccountGroup>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
@@ -206,25 +207,23 @@
     }
 
     @Override
-    public AccountGroup createEntry(final AccountGroup.NameKey key)
+    public Optional<AccountGroup> load(String name)
         throws Exception {
-      final AccountGroupName r;
       final ReviewDb db = schema.open();
       try {
-        r = db.accountGroupNames().get(key);
+        AccountGroup.NameKey key = new AccountGroup.NameKey(name);
+        AccountGroupName r = db.accountGroupNames().get(key);
         if (r != null) {
-          return db.accountGroups().get(r.getId());
-        } else {
-          return null;
+          return Optional.fromNullable(db.accountGroups().get(r.getId()));
         }
+        return Optional.absent();
       } finally {
         db.close();
       }
     }
   }
 
-  static class ByUUIDLoader extends
-      EntryCreator<AccountGroup.UUID, AccountGroup> {
+  static class ByUUIDLoader extends CacheLoader<String, Optional<AccountGroup>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
@@ -233,74 +232,23 @@
     }
 
     @Override
-    public AccountGroup createEntry(final AccountGroup.UUID uuid)
+    public Optional<AccountGroup> load(String uuid)
         throws Exception {
       final ReviewDb db = schema.open();
       try {
-        List<AccountGroup> r = db.accountGroups().byUUID(uuid).toList();
+        List<AccountGroup> r;
+
+        r = db.accountGroups().byUUID(new AccountGroup.UUID(uuid)).toList();
         if (r.size() == 1) {
-          return r.get(0);
+          return Optional.of(r.get(0));
+        } else if (r.size() == 0) {
+          return Optional.absent();
         } else {
-          return null;
+          throw new OrmDuplicateKeyException("Duplicate group UUID " + uuid);
         }
       } finally {
         db.close();
       }
     }
   }
-
-  static class ByExternalNameLoader extends
-      EntryCreator<AccountGroup.ExternalNameKey, Collection<AccountGroup>> {
-    private final SchemaFactory<ReviewDb> schema;
-
-    @Inject
-    ByExternalNameLoader(final SchemaFactory<ReviewDb> sf) {
-      schema = sf;
-    }
-
-    @Override
-    public Collection<AccountGroup> createEntry(
-        final AccountGroup.ExternalNameKey key) throws Exception {
-      final ReviewDb db = schema.open();
-      try {
-        return db.accountGroups().byExternalName(key).toList();
-      } finally {
-        db.close();
-      }
-    }
-  }
-
-  static class ListKey {
-    static final ListKey ALL = new ListKey();
-
-    private ListKey() {
-    }
-  }
-
-  static class Lister extends EntryCreator<ListKey, SortedSet<AccountGroup.NameKey>> {
-    private final SchemaFactory<ReviewDb> schema;
-
-    @Inject
-    Lister(final SchemaFactory<ReviewDb> sf) {
-      schema = sf;
-    }
-
-    @Override
-    public SortedSet<AccountGroup.NameKey> createEntry(ListKey key)
-        throws Exception {
-      final ReviewDb db = schema.open();
-      try {
-        final List<AccountGroupName> groupNames =
-            db.accountGroupNames().all().toList();
-        final SortedSet<AccountGroup.NameKey> groups =
-            new TreeSet<AccountGroup.NameKey>();
-        for (final AccountGroupName groupName : groupNames) {
-          groups.add(groupName.getNameKey());
-        }
-        return Collections.unmodifiableSortedSet(groups);
-      } finally {
-        db.close();
-      }
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
index f7451a8..d9b12ac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -27,11 +29,14 @@
   public static class Factory {
     private final GroupCache groupCache;
     private final Provider<CurrentUser> user;
+    private final GroupBackend groupBackend;
 
     @Inject
-    Factory(final GroupCache gc, final Provider<CurrentUser> cu) {
+    Factory(final GroupCache gc, final Provider<CurrentUser> cu,
+        final GroupBackend gb) {
       groupCache = gc;
       user = cu;
+      groupBackend = gb;
     }
 
     public GroupControl controlFor(final AccountGroup.Id groupId)
@@ -40,20 +45,20 @@
       if (group == null) {
         throw new NoSuchGroupException(groupId);
       }
-      return new GroupControl(groupCache, user.get(), group);
+      return new GroupControl(user.get(), group);
     }
 
     public GroupControl controlFor(final AccountGroup.UUID groupId)
         throws NoSuchGroupException {
-      final AccountGroup group = groupCache.get(groupId);
+      final GroupDescription.Basic group = groupBackend.get(groupId);
       if (group == null) {
         throw new NoSuchGroupException(groupId);
       }
-      return new GroupControl(groupCache, user.get(), group);
+      return new GroupControl(user.get(), group);
     }
 
     public GroupControl controlFor(final AccountGroup group) {
-      return new GroupControl(groupCache, user.get(), group);
+      return new GroupControl(user.get(), group);
     }
 
     public GroupControl validateFor(final AccountGroup.Id groupId)
@@ -66,25 +71,23 @@
     }
   }
 
-  private final GroupCache groupCache;
   private final CurrentUser user;
-  private final AccountGroup group;
+  private final GroupDescription.Basic group;
   private Boolean isOwner;
 
-  GroupControl(GroupCache g, CurrentUser who, AccountGroup gc) {
-    groupCache = g;
+  GroupControl(CurrentUser who, GroupDescription.Basic gd) {
     user = who;
-    group = gc;
+    group =  gd;
+  }
+
+  GroupControl(CurrentUser who, AccountGroup ag) {
+    this(who, GroupDescriptions.forAccountGroup(ag));
   }
 
   public CurrentUser getCurrentUser() {
     return user;
   }
 
-  public AccountGroup getAccountGroup() {
-    return group;
-  }
-
   /** Can this user see this group exists? */
   public boolean isVisible() {
     return group.isVisibleToAll()
@@ -93,9 +96,11 @@
   }
 
   public boolean isOwner() {
-    if (isOwner == null) {
-      AccountGroup g = groupCache.get(group.getOwnerGroupId());
-      AccountGroup.UUID ownerUUID = g != null ? g.getGroupUUID() : null;
+    AccountGroup accountGroup = GroupDescriptions.toAccountGroup(group);
+    if (accountGroup == null) {
+      isOwner = false;
+    } else if (isOwner == null) {
+      AccountGroup.UUID ownerUUID = accountGroup.getOwnerGroupUUID();
       isOwner = getCurrentUser().getEffectiveGroups().contains(ownerUUID)
              || getCurrentUser().getCapabilities().canAdministrateServer();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
index 7f88134..2e500bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -39,6 +41,7 @@
   private final ReviewDb db;
   private final GroupControl.Factory groupControl;
   private final GroupCache groupCache;
+  private final GroupBackend groupBackend;
   private final AccountInfoCacheFactory aic;
   private final GroupInfoCacheFactory gic;
 
@@ -48,12 +51,14 @@
   @Inject
   GroupDetailFactory(final ReviewDb db,
       final GroupControl.Factory groupControl, final GroupCache groupCache,
+      final GroupBackend groupBackend,
       final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
       final GroupInfoCacheFactory.Factory groupInfoCacheFactory,
       @Assisted final AccountGroup.Id groupId) {
     this.db = db;
     this.groupControl = groupControl;
     this.groupCache = groupCache;
+    this.groupBackend = groupBackend;
     this.aic = accountInfoCacheFactory.create();
     this.gic = groupInfoCacheFactory.create();
 
@@ -63,10 +68,13 @@
   @Override
   public GroupDetail call() throws OrmException, NoSuchGroupException {
     control = groupControl.validateFor(groupId);
-    final AccountGroup group = control.getAccountGroup();
+    final AccountGroup group = groupCache.get(groupId);
     final GroupDetail detail = new GroupDetail();
     detail.setGroup(group);
-    detail.setOwnerGroup(groupCache.get(group.getOwnerGroupId()));
+    GroupDescription.Basic ownerGroup = groupBackend.get(group.getOwnerGroupUUID());
+    if (ownerGroup != null) {
+      detail.setOwnerGroup(GroupReference.forGroup(ownerGroup));
+    }
     switch (group.getType()) {
       case INTERNAL:
         detail.setMembers(loadMembers());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index 791d0f5..7fbba45 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -14,12 +14,14 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupInclude;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -27,24 +29,30 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
 
 /** Tracks group inclusions in memory for efficient access. */
 @Singleton
 public class GroupIncludeCacheImpl implements GroupIncludeCache {
+  private static final Logger log = LoggerFactory
+      .getLogger(GroupIncludeCacheImpl.class);
   private static final String BYINCLUDE_NAME = "groups_byinclude";
 
   public static Module module() {
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>>> byInclude =
-            new TypeLiteral<Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>>>() {};
-        core(byInclude, BYINCLUDE_NAME).populateWith(ByIncludeLoader.class);
+        cache(BYINCLUDE_NAME,
+            AccountGroup.UUID.class,
+            new TypeLiteral<Set<AccountGroup.UUID>>() {})
+          .loader(ByIncludeLoader.class);
 
         bind(GroupIncludeCacheImpl.class);
         bind(GroupIncludeCache.class).to(GroupIncludeCacheImpl.class);
@@ -52,24 +60,31 @@
     };
   }
 
-  private final Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>> byInclude;
+  private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> byInclude;
 
   @Inject
   GroupIncludeCacheImpl(
-      @Named(BYINCLUDE_NAME) Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>> byInclude) {
+      @Named(BYINCLUDE_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> byInclude) {
     this.byInclude = byInclude;
   }
 
   public Collection<AccountGroup.UUID> getByInclude(AccountGroup.UUID groupId) {
-    return byInclude.get(groupId);
+    try {
+      return byInclude.get(groupId);
+    } catch (ExecutionException e) {
+      log.warn("Cannot load included groups", e);
+      return Collections.emptySet();
+    }
   }
 
   public void evictInclude(AccountGroup.UUID groupId) {
-    byInclude.remove(groupId);
+    if (groupId != null) {
+      byInclude.invalidate(groupId);
+    }
   }
 
   static class ByIncludeLoader extends
-      EntryCreator<AccountGroup.UUID, Collection<AccountGroup.UUID>> {
+      CacheLoader<AccountGroup.UUID, Set<AccountGroup.UUID>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
@@ -78,32 +93,28 @@
     }
 
     @Override
-    public Collection<AccountGroup.UUID> createEntry(final AccountGroup.UUID key) throws Exception {
+    public Set<AccountGroup.UUID> load(AccountGroup.UUID key) throws Exception {
       final ReviewDb db = schema.open();
       try {
         List<AccountGroup> group = db.accountGroups().byUUID(key).toList();
         if (group.size() != 1) {
-          return Collections.emptyList();
+          return Collections.emptySet();
         }
 
-        Set<AccountGroup.Id> ids = new HashSet<AccountGroup.Id>();
-        for (AccountGroupInclude agi : db.accountGroupIncludes().byInclude(group.get(0).getId())) {
+        Set<AccountGroup.Id> ids = Sets.newHashSet();
+        for (AccountGroupInclude agi : db.accountGroupIncludes()
+            .byInclude(group.get(0).getId())) {
           ids.add(agi.getGroupId());
         }
 
-        Set<AccountGroup.UUID> groupArray = new HashSet<AccountGroup.UUID> ();
+        Set<AccountGroup.UUID> groupArray = Sets.newHashSet();
         for (AccountGroup g : db.accountGroups().get(ids)) {
           groupArray.add(g.getGroupUUID());
         }
-        return Collections.unmodifiableCollection(groupArray);
+        return ImmutableSet.copyOf(groupArray);
       } finally {
         db.close();
       }
     }
-
-    @Override
-    public Collection<AccountGroup.UUID> missing(final AccountGroup.UUID key) {
-      return Collections.emptyList();
-    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
index 9bb571e..d536c09 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
@@ -24,7 +24,6 @@
  * the presence of a user in a particular group.
  */
 public interface GroupMembership {
-
   public static final GroupMembership EMPTY =
       new ListGroupMembership(Collections.<AccountGroup.UUID>emptySet());
 
@@ -45,7 +44,7 @@
    * This may not return all groups the {@link #contains(AccountGroup.UUID)}
    * would return {@code true} for, but will at least contain all top level
    * groups. This restriction stems from the API of some group systems, which
-   * make it expensive to enumate the members of a group.
+   * make it expensive to enumerate the members of a group.
    */
   Set<AccountGroup.UUID> getKnownGroups();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/MaterializedGroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
similarity index 89%
rename from gerrit-server/src/main/java/com/google/gerrit/server/account/MaterializedGroupMembership.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
index 81ff656..d448fff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/MaterializedGroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
@@ -25,11 +25,12 @@
 import java.util.Set;
 
 /**
- * Creates a GroupMembership object from materialized collection of groups.
+ * Creates a GroupMembership checker for the internal group system, which
+ * starts with the seed groups and includes all child groups.
  */
-public class MaterializedGroupMembership implements GroupMembership {
+public class IncludingGroupMembership implements GroupMembership {
   public interface Factory {
-    MaterializedGroupMembership create(Iterable<AccountGroup.UUID> groupIds);
+    IncludingGroupMembership create(Iterable<AccountGroup.UUID> groupIds);
   }
 
   private final GroupIncludeCache groupIncludeCache;
@@ -37,7 +38,7 @@
   private final Queue<AccountGroup.UUID> groupQueue;
 
   @Inject
-  MaterializedGroupMembership(
+  IncludingGroupMembership(
       GroupIncludeCache groupIncludeCache,
       @Assisted Iterable<AccountGroup.UUID> seedGroups) {
     this.groupIncludeCache = groupIncludeCache;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
new file mode 100644
index 0000000..ad65499
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.Collection;
+
+/**
+ * Implementation of GroupBackend for the internal group system.
+ */
+@Singleton
+public class InternalGroupBackend implements GroupBackend {
+  private static final Function<AccountGroup, GroupReference> ACT_GROUP_TO_GROUP_REF =
+      new Function<AccountGroup, GroupReference>() {
+        @Override
+        public GroupReference apply(AccountGroup group) {
+          return GroupReference.forGroup(group);
+        }
+      };
+
+  private final GroupControl.Factory groupControlFactory;
+  private final GroupCache groupCache;
+  private final IncludingGroupMembership.Factory groupMembershipFactory;
+
+
+  @Inject
+  InternalGroupBackend(GroupControl.Factory groupControlFactory,
+      GroupCache groupCache,
+      IncludingGroupMembership.Factory groupMembershipFactory) {
+    this.groupControlFactory = groupControlFactory;
+    this.groupCache = groupCache;
+    this.groupMembershipFactory = groupMembershipFactory;
+  }
+
+  @Override
+  public boolean handles(AccountGroup.UUID uuid) {
+    return AccountGroup.isInternalGroup(uuid);
+  }
+
+  @Override
+  public GroupDescription.Internal get(AccountGroup.UUID uuid) {
+    if (!handles(uuid)) {
+      return null;
+    }
+
+    AccountGroup g = groupCache.get(uuid);
+    if (g == null) {
+      return null;
+    }
+    return GroupDescriptions.forAccountGroup(g);
+  }
+
+  @Override
+  public Collection<GroupReference> suggest(final String name) {
+    Iterable<AccountGroup> filtered = Iterables.filter(groupCache.all(),
+        new Predicate<AccountGroup>() {
+          @Override
+          public boolean apply(AccountGroup group) {
+            // startsWithIgnoreCase && isVisible
+            return group.getName().regionMatches(true, 0, name, 0, name.length())
+                && groupControlFactory.controlFor(group).isVisible();
+          }
+        });
+    return Lists.newArrayList(Iterables.transform(filtered, ACT_GROUP_TO_GROUP_REF));
+  }
+
+  @Override
+  public GroupMembership membershipsOf(IdentifiedUser user) {
+    return groupMembershipFactory.create(user.state().getInternalGroups());
+  }
+}
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 5f94df2..1ff1a3e 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
@@ -107,7 +107,10 @@
     final AccountGroup group = new AccountGroup(nameKey, groupId, uuid);
     group.setVisibleToAll(visibleToAll);
     if (ownerGroupId != null) {
-      group.setOwnerGroupId(ownerGroupId);
+      AccountGroup ownerGroup = groupCache.get(ownerGroupId);
+      if (ownerGroup != null) {
+        group.setOwnerGroupUUID(ownerGroup.getGroupUUID());
+      }
     }
     if (groupDescription != null) {
       group.setDescription(groupDescription);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
index 2ebd0e5..e44d46e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
@@ -15,11 +15,8 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 
-import java.util.Set;
-
 public interface Realm {
   /** Can the end-user modify this field of their own account? */
   public boolean allowsEdit(Account.FieldName field);
@@ -34,8 +31,6 @@
 
   public void onCreateAccount(AuthRequest who, Account account);
 
-  public GroupMembership groups(AccountState who);
-
   /**
    * Locate an account whose local username is the given account name.
    * <p>
@@ -45,9 +40,4 @@
    * user by that email address.
    */
   public Account.Id lookup(String accountName);
-
-  /**
-   * Search for matching external groups.
-   */
-  public Set<AccountGroup.ExternalNameKey> lookupGroups(String name);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
new file mode 100644
index 0000000..1974961
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -0,0 +1,161 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import static com.google.gerrit.server.account.GroupBackends.GROUP_REF_NAME_COMPARATOR;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Universal implementation of the GroupBackend that works with the injected
+ * set of GroupBackends.
+ */
+@Singleton
+public class UniversalGroupBackend implements GroupBackend {
+  private static final Logger log =
+      LoggerFactory.getLogger(UniversalGroupBackend.class);
+
+  private final DynamicSet<GroupBackend> backends;
+
+  @Inject
+  UniversalGroupBackend(DynamicSet<GroupBackend> backends) {
+    this.backends = backends;
+  }
+
+  @Nullable
+  private GroupBackend backend(AccountGroup.UUID uuid) {
+    if (uuid != null) {
+      for (GroupBackend g : backends) {
+        if (g.handles(uuid)) {
+          return g;
+        }
+      }
+    }
+    return null;
+  }
+
+  @Override
+  public boolean handles(AccountGroup.UUID uuid) {
+    return backend(uuid) != null;
+  }
+
+  @Override
+  public GroupDescription.Basic get(AccountGroup.UUID uuid) {
+    GroupBackend b = backend(uuid);
+    if (b == null) {
+      log.warn("Unknown GroupBackend for UUID: " + uuid);
+      return null;
+    }
+    return b.get(uuid);
+  }
+
+  @Override
+  public Collection<GroupReference> suggest(String name) {
+    Set<GroupReference> groups = Sets.newTreeSet(GROUP_REF_NAME_COMPARATOR);
+    for (GroupBackend g : backends) {
+      groups.addAll(g.suggest(name));
+    }
+    return groups;
+  }
+
+  @Override
+  public GroupMembership membershipsOf(IdentifiedUser user) {
+    return new UniversalGroupMembership(user);
+  }
+
+  private class UniversalGroupMembership implements GroupMembership {
+   private final Map<GroupBackend, GroupMembership> memberships;
+
+   private UniversalGroupMembership(IdentifiedUser user) {
+     ImmutableMap.Builder<GroupBackend, GroupMembership> builder =
+         ImmutableMap.builder();
+     for (GroupBackend g : backends) {
+       builder.put(g, g.membershipsOf(user));
+     }
+     this.memberships = builder.build();
+   }
+
+   @Nullable
+   private GroupMembership membership(AccountGroup.UUID uuid) {
+     if (uuid != null) {
+       for (Map.Entry<GroupBackend, GroupMembership> m : memberships.entrySet()) {
+         if (m.getKey().handles(uuid)) {
+           return m.getValue();
+         }
+       }
+     }
+     return null;
+   }
+
+   @Override
+   public boolean contains(AccountGroup.UUID uuid) {
+     GroupMembership m = membership(uuid);
+     if (m == null) {
+       log.warn("Unknown GroupMembership for UUID: " + uuid);
+       return false;
+     }
+     return m.contains(uuid);
+   }
+
+   @Override
+   public boolean containsAnyOf(Iterable<AccountGroup.UUID> uuids) {
+     Multimap<GroupMembership, AccountGroup.UUID> lookups =
+         ArrayListMultimap.create();
+     for (AccountGroup.UUID uuid : uuids) {
+       GroupMembership m = membership(uuid);
+       if (m == null) {
+         log.warn("Unknown GroupMembership for UUID: " + uuid);
+         continue;
+       }
+       lookups.put(m, uuid);
+     }
+     for (Map.Entry<GroupMembership, Collection<AccountGroup.UUID>> entry :
+          lookups.asMap().entrySet()) {
+       if (entry.getKey().containsAnyOf(entry.getValue())) {
+         return true;
+       }
+     }
+     return false;
+   }
+
+   @Override
+   public Set<AccountGroup.UUID> getKnownGroups() {
+     Set<AccountGroup.UUID> groups = Sets.newHashSet();
+     for (GroupMembership m : memberships.values()) {
+       groups.addAll(m.getKnownGroups());
+     }
+     return groups;
+   }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupIdHandler.java
similarity index 97%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupIdHandler.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupIdHandler.java
index 307a10a..bf74a4a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupIdHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupIdHandler.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.sshd.args4j;
+package com.google.gerrit.server.args4j;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupCache;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupUUIDHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java
similarity index 77%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupUUIDHandler.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java
index 49bf695..406ca58 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupUUIDHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java
@@ -12,10 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.sshd.args4j;
+package com.google.gerrit.server.args4j;
 
+import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupBackends;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -27,25 +29,25 @@
 import org.kohsuke.args4j.spi.Setter;
 
 public class AccountGroupUUIDHandler extends OptionHandler<AccountGroup.UUID> {
-  private final GroupCache groupCache;
+  private final GroupBackend groupBackend;
 
   @Inject
-  public AccountGroupUUIDHandler(final GroupCache groupCache,
+  public AccountGroupUUIDHandler(final GroupBackend groupBackend,
       @Assisted final CmdLineParser parser, @Assisted final OptionDef option,
       @Assisted final Setter<AccountGroup.UUID> setter) {
     super(parser, option, setter);
-    this.groupCache = groupCache;
+    this.groupBackend = groupBackend;
   }
 
   @Override
   public final int parseArguments(final Parameters params)
       throws CmdLineException {
     final String n = params.getParameter(0);
-    final AccountGroup group = groupCache.get(new AccountGroup.NameKey(n));
+    final GroupReference group = GroupBackends.findBestSuggestion(groupBackend, n);
     if (group == null) {
       throw new CmdLineException(owner, "Group \"" + n + "\" does not exist");
     }
-    setter.addValue(group.getGroupUUID());
+    setter.addValue(group.getUUID());
     return 1;
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
similarity index 98%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountIdHandler.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
index d54ae34..8e71b88 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountIdHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.sshd.args4j;
+package com.google.gerrit.server.args4j;
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AuthType;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ChangeIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
similarity index 94%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ChangeIdHandler.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
index 0194b91..9c3d052 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ChangeIdHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.sshd.args4j;
+package com.google.gerrit.server.args4j;
 
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -56,7 +56,8 @@
     try {
       final Change.Key key = Change.Key.parse(tokens[2]);
       final Project.NameKey project = new Project.NameKey(tokens[0]);
-      final Branch.NameKey branch = new Branch.NameKey(project, tokens[1]);
+      final Branch.NameKey branch =
+          new Branch.NameKey(project, "refs/heads/" + tokens[1]);
       for (final Change change : db.changes().byBranchKey(branch, key)) {
         setter.addValue(change.getId());
         return 1;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ObjectIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ObjectIdHandler.java
similarity index 96%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ObjectIdHandler.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/args4j/ObjectIdHandler.java
index adb5ad6..b7f2fb9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ObjectIdHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ObjectIdHandler.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.sshd.args4j;
+package com.google.gerrit.server.args4j;
 
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/PatchSetIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/PatchSetIdHandler.java
similarity index 97%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/PatchSetIdHandler.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/args4j/PatchSetIdHandler.java
index 2d6a4df..a48568f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/PatchSetIdHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/PatchSetIdHandler.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.sshd.args4j;
+package com.google.gerrit.server.args4j;
 
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.inject.Inject;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ProjectControlHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java
similarity index 98%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ProjectControlHandler.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java
index e0f7c4c..c1c9c50 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ProjectControlHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.sshd.args4j;
+package com.google.gerrit.server.args4j;
 
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.project.NoSuchProjectException;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SocketAddressHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/SocketAddressHandler.java
similarity index 97%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SocketAddressHandler.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/args4j/SocketAddressHandler.java
index 454a084..0c20b2d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SocketAddressHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/SocketAddressHandler.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.sshd.args4j;
+package com.google.gerrit.server.args4j;
 
 import com.google.gerrit.server.util.SocketUtil;
 import com.google.inject.Inject;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SubcommandHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/SubcommandHandler.java
similarity index 96%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SubcommandHandler.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/args4j/SubcommandHandler.java
index 3df73a8..619ec1f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SubcommandHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/SubcommandHandler.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.sshd.args4j;
+package com.google.gerrit.server.args4j;
 
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
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 e81bfc2..2265bc2 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,7 +17,6 @@
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountException;
-import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.util.ssl.BlindSSLSocketFactory;
@@ -47,7 +46,8 @@
 import javax.net.ssl.SSLSocketFactory;
 
 @Singleton class Helper {
-  private final GroupCache groupCache;
+  static final String LDAP_UUID = "ldap:";
+
   private final Config config;
   private final String server;
   private final String username;
@@ -58,8 +58,7 @@
   private final String readTimeOutMillis;
 
   @Inject
-  Helper(@GerritServerConfig final Config config, final GroupCache groupCache) {
-    this.groupCache = groupCache;
+  Helper(@GerritServerConfig final Config config) {
     this.config = config;
     this.server = LdapRealm.required(config, "server");
     this.username = LdapRealm.optional(config, "username");
@@ -195,12 +194,7 @@
 
     final Set<AccountGroup.UUID> actual = new HashSet<AccountGroup.UUID>();
     for (String dn : groupDNs) {
-      for (AccountGroup group : groupCache
-          .get(new AccountGroup.ExternalNameKey(dn))) {
-        if (group.getType() == AccountGroup.Type.LDAP) {
-          actual.add(group.getGroupUUID());
-        }
-      }
+      actual.add(new AccountGroup.UUID(LDAP_UUID + dn));
     }
 
     if (actual.isEmpty()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
new file mode 100644
index 0000000..5c30e5c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
@@ -0,0 +1,227 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.auth.ldap;
+
+import static com.google.gerrit.server.account.GroupBackends.GROUP_REF_NAME_COMPARATOR;
+import static com.google.gerrit.server.auth.ldap.Helper.LDAP_UUID;
+import static com.google.gerrit.server.auth.ldap.LdapModule.GROUP_CACHE;
+import static com.google.gerrit.server.auth.ldap.LdapModule.GROUP_EXIST_CACHE;
+
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.ParameterizedString;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.account.ListGroupMembership;
+import com.google.gerrit.server.auth.ldap.Helper.LdapSchema;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.name.Named;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+import javax.naming.InvalidNameException;
+import javax.naming.NamingException;
+import javax.naming.directory.DirContext;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+
+/**
+ * Implementation of GroupBackend for the LDAP group system.
+ */
+public class LdapGroupBackend implements GroupBackend {
+  private static final Logger log = LoggerFactory.getLogger(LdapGroupBackend.class);
+
+  private static final String LDAP_NAME = "ldap/";
+  private static final String GROUPNAME = "groupname";
+
+  private final Helper helper;
+  private final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache;
+  private final LoadingCache<String, Boolean> existsCache;
+  private final Provider<CurrentUser> userProvider;
+
+  @Inject
+  LdapGroupBackend(
+      Helper helper,
+      @Named(GROUP_CACHE) LoadingCache<String, Set<AccountGroup.UUID>> membershipCache,
+      @Named(GROUP_EXIST_CACHE) LoadingCache<String, Boolean> existsCache,
+      Provider<CurrentUser> userProvider) {
+    this.helper = helper;
+    this.membershipCache = membershipCache;
+    this.existsCache = existsCache;
+    this.userProvider = userProvider;
+  }
+
+  private static boolean isLdapUUID(AccountGroup.UUID uuid) {
+    return uuid.get().startsWith(LDAP_UUID);
+  }
+
+  private static GroupReference groupReference(LdapQuery.Result res)
+      throws NamingException {
+    return new GroupReference(
+        new AccountGroup.UUID(LDAP_UUID + res.getDN()),
+        LDAP_NAME + cnFor(res.getDN()));
+  }
+
+  private static String cnFor(String dn) {
+    try {
+      LdapName name = new LdapName(dn);
+      if (!name.isEmpty()) {
+        String cn = name.get(name.size() - 1);
+        int index = cn.indexOf('=');
+        if (index >= 0) {
+          cn = cn.substring(index + 1);
+        }
+        return cn;
+      }
+    } catch (InvalidNameException e) {
+      log.warn("Cannot parse LDAP dn for cn", e);
+    }
+    return dn;
+  }
+
+  @Override
+  public boolean handles(AccountGroup.UUID uuid) {
+    return isLdapUUID(uuid);
+  }
+
+  @Override
+  public GroupDescription.Basic get(final AccountGroup.UUID uuid) {
+    if (!handles(uuid)) {
+      return null;
+    }
+
+    String groupDn = uuid.get().substring(LDAP_UUID.length());
+    CurrentUser user = userProvider.get();
+    if (!(user instanceof IdentifiedUser)
+        || !membershipsOf((IdentifiedUser) user).contains(uuid)) {
+      try {
+        if (!existsCache.get(groupDn)) {
+          return null;
+        }
+      } catch (ExecutionException e) {
+        log.warn(String.format("Cannot lookup group %s in LDAP", groupDn), e);
+        return null;
+      }
+    }
+
+    final String name = LDAP_NAME + cnFor(groupDn);
+    return new GroupDescription.Basic() {
+      @Override
+      public AccountGroup.UUID getGroupUUID() {
+        return uuid;
+      }
+
+      @Override
+      public String getName() {
+        return name;
+      }
+
+      @Override
+      public boolean isVisibleToAll() {
+        return false;
+      }
+    };
+  }
+
+  @Override
+  public Collection<GroupReference> suggest(String name) {
+    AccountGroup.UUID uuid = new AccountGroup.UUID(name);
+    if (isLdapUUID(uuid)) {
+      GroupDescription.Basic g = get(uuid);
+      if (g == null) {
+        return Collections.emptySet();
+      }
+      return Collections.singleton(GroupReference.forGroup(g));
+    } else if (name.startsWith(LDAP_NAME)) {
+      return suggestLdap(name.substring(LDAP_NAME.length()));
+    }
+    return Collections.emptySet();
+  }
+
+  @Override
+  public GroupMembership membershipsOf(IdentifiedUser user) {
+    String id = findId(user.state().getExternalIds());
+    if (id == null) {
+      return GroupMembership.EMPTY;
+    }
+
+    try {
+      return new ListGroupMembership(membershipCache.get(id));
+    } catch (ExecutionException e) {
+      log.warn(String.format("Cannot lookup membershipsOf %s in LDAP", id), e);
+      return GroupMembership.EMPTY;
+    }
+  }
+
+  private static String findId(final Collection<AccountExternalId> ids) {
+    for (final AccountExternalId i : ids) {
+      if (i.isScheme(AccountExternalId.SCHEME_GERRIT)) {
+        return i.getSchemeRest();
+      }
+    }
+    return null;
+  }
+
+
+  private Set<GroupReference> suggestLdap(String name) {
+    if (name.isEmpty()) {
+      return Collections.emptySet();
+    }
+
+    Set<GroupReference> out = Sets.newTreeSet(GROUP_REF_NAME_COMPARATOR);
+    try {
+      DirContext ctx = helper.open();
+      try {
+        // Do exact lookups until there are at least 3 characters.
+        name = Rdn.escapeValue(name) + ((name.length() >= 3) ? "*" : "");
+        LdapSchema schema = helper.getSchema(ctx);
+        ParameterizedString filter = ParameterizedString.asis(
+            schema.groupPattern.replace(GROUPNAME, name).toString());
+        Set<String> returnAttrs = Collections.<String>emptySet();
+        Map<String, String> params = Collections.emptyMap();
+        for (String groupBase : schema.groupBases) {
+          LdapQuery query = new LdapQuery(
+              groupBase, schema.groupScope, filter, returnAttrs);
+          for (LdapQuery.Result res : query.query(ctx, params)) {
+            out.add(groupReference(res));
+          }
+        }
+      } finally {
+        try {
+          ctx.close();
+        } catch (NamingException e) {
+          log.warn("Cannot close LDAP query handle", e);
+        }
+      }
+    } catch (NamingException e) {
+      log.warn("Cannot query LDAP for groups matching requested name", e);
+    }
+    return out;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
index 6eb2f54..29533b9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
@@ -16,10 +16,12 @@
 
 import static java.util.concurrent.TimeUnit.HOURS;
 
+import com.google.common.base.Optional;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.Realm;
-import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.inject.Scopes;
 import com.google.inject.TypeLiteral;
@@ -29,20 +31,31 @@
 public class LdapModule extends CacheModule {
   static final String USERNAME_CACHE = "ldap_usernames";
   static final String GROUP_CACHE = "ldap_groups";
+  static final String GROUP_EXIST_CACHE = "ldap_group_existence";
+
 
   @Override
   protected void configure() {
-    final TypeLiteral<Cache<String, Set<AccountGroup.UUID>>> groups =
-        new TypeLiteral<Cache<String, Set<AccountGroup.UUID>>>() {};
-    core(groups, GROUP_CACHE).maxAge(1, HOURS) //
-        .populateWith(LdapRealm.MemberLoader.class);
+    cache(GROUP_CACHE,
+        String.class,
+        new TypeLiteral<Set<AccountGroup.UUID>>() {})
+      .expireAfterWrite(1, HOURS)
+      .loader(LdapRealm.MemberLoader.class);
 
-    final TypeLiteral<Cache<String, Account.Id>> usernames =
-        new TypeLiteral<Cache<String, Account.Id>>() {};
-    core(usernames, USERNAME_CACHE) //
-        .populateWith(LdapRealm.UserLoader.class);
+    cache(USERNAME_CACHE,
+        String.class,
+        new TypeLiteral<Optional<Account.Id>>() {})
+      .loader(LdapRealm.UserLoader.class);
+
+    cache(GROUP_EXIST_CACHE,
+        String.class,
+        new TypeLiteral<Boolean>() {})
+      .expireAfterWrite(1, HOURS)
+      .loader(LdapRealm.ExistenceLoader.class);
 
     bind(Realm.class).to(LdapRealm.class).in(Scopes.SINGLETON);
     bind(Helper.class);
+
+    DynamicSet.bind(binder(), GroupBackend.class).to(LdapGroupBackend.class);
   }
 }
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 910bf06..72eb7ec 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
@@ -16,7 +16,10 @@
 
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT;
 
-import com.google.common.collect.Iterables;
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
@@ -24,20 +27,13 @@
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountException;
-import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.EmailExpander;
-import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.account.MaterializedGroupMembership;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.auth.AuthenticationUnavailableException;
-import com.google.gerrit.server.auth.ldap.Helper.LdapSchema;
-import com.google.gerrit.server.cache.Cache;
-import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -48,15 +44,16 @@
 import org.slf4j.LoggerFactory;
 
 import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
 
+import javax.naming.CompositeName;
+import javax.naming.Name;
 import javax.naming.NamingException;
 import javax.naming.directory.DirContext;
 
@@ -65,34 +62,30 @@
   static final Logger log = LoggerFactory.getLogger(LdapRealm.class);
   static final String LDAP = "com.sun.jndi.ldap.LdapCtxFactory";
   static final String USERNAME = "username";
-  private static final String GROUPNAME = "groupname";
 
   private final Helper helper;
   private final AuthConfig authConfig;
   private final EmailExpander emailExpander;
-  private final Cache<String, Account.Id> usernameCache;
+  private final LoadingCache<String, Optional<Account.Id>> usernameCache;
   private final Set<Account.FieldName> readOnlyAccountFields;
   private final Config config;
 
-  private final Cache<String, Set<AccountGroup.UUID>> membershipCache;
-  private final MaterializedGroupMembership.Factory groupMembershipFactory;
+  private final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache;
 
   @Inject
   LdapRealm(
       final Helper helper,
       final AuthConfig authConfig,
       final EmailExpander emailExpander,
-      @Named(LdapModule.GROUP_CACHE) final Cache<String, Set<AccountGroup.UUID>> membershipCache,
-      @Named(LdapModule.USERNAME_CACHE) final Cache<String, Account.Id> usernameCache,
-      @GerritServerConfig final Config config,
-      final MaterializedGroupMembership.Factory groupMembershipFactory) {
+      @Named(LdapModule.GROUP_CACHE) final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache,
+      @Named(LdapModule.USERNAME_CACHE) final LoadingCache<String, Optional<Account.Id>> usernameCache,
+      @GerritServerConfig final Config config) {
     this.helper = helper;
     this.authConfig = authConfig;
     this.emailExpander = emailExpander;
     this.usernameCache = usernameCache;
     this.membershipCache = membershipCache;
     this.config = config;
-    this.groupMembershipFactory = groupMembershipFactory;
 
     this.readOnlyAccountFields = new HashSet<Account.FieldName>();
 
@@ -189,6 +182,7 @@
     return r.isEmpty() ? null : r;
   }
 
+  @Override
   public AuthRequest authenticate(final AuthRequest who)
       throws AccountException {
     if (config.getBoolean("ldap", "localUsernameToLowerCase", false)) {
@@ -261,65 +255,24 @@
 
   @Override
   public void onCreateAccount(final AuthRequest who, final Account account) {
-    usernameCache.put(who.getLocalUser(), account.getId());
+    usernameCache.put(who.getLocalUser(), Optional.of(account.getId()));
   }
 
   @Override
-  public GroupMembership groups(final AccountState who) {
-    return groupMembershipFactory.create(Iterables.concat(
-        membershipCache.get(findId(who.getExternalIds())),
-        who.getInternalGroups()));
-  }
-
-  private static String findId(final Collection<AccountExternalId> ids) {
-    for (final AccountExternalId i : ids) {
-      if (i.isScheme(AccountExternalId.SCHEME_GERRIT)) {
-        return i.getSchemeRest();
-      }
+  public Account.Id lookup(String accountName) {
+    if (Strings.isNullOrEmpty(accountName)) {
+      return null;
     }
-    return null;
-  }
-
-  @Override
-  public Account.Id lookup(final String accountName) {
-    return usernameCache.get(accountName);
-  }
-
-  @Override
-  public Set<AccountGroup.ExternalNameKey> lookupGroups(String name) {
-    final Set<AccountGroup.ExternalNameKey> out;
-    final Map<String, String> params = Collections.<String, String> emptyMap();
-
-    out = new HashSet<AccountGroup.ExternalNameKey>();
     try {
-      final DirContext ctx = helper.open();
-      try {
-        final LdapSchema schema = helper.getSchema(ctx);
-        final ParameterizedString filter =
-            ParameterizedString.asis(schema.groupPattern
-                .replace(GROUPNAME, name).toString());
-        for (String groupBase : schema.groupBases) {
-          final LdapQuery query =
-              new LdapQuery(groupBase, schema.groupScope, filter, Collections
-                  .<String> emptySet());
-          for (LdapQuery.Result res : query.query(ctx, params)) {
-            out.add(new AccountGroup.ExternalNameKey(res.getDN()));
-          }
-        }
-      } finally {
-        try {
-          ctx.close();
-        } catch (NamingException e) {
-          log.warn("Cannot close LDAP query handle", e);
-        }
-      }
-    } catch (NamingException e) {
-      log.warn("Cannot query LDAP for groups matching requested name", e);
+      Optional<Account.Id> id = usernameCache.get(accountName);
+      return id != null ? id.orNull() : null;
+    } catch (ExecutionException e) {
+      log.warn(String.format("Cannot lookup account %s in LDAP", accountName), e);
+      return null;
     }
-    return out;
   }
 
-  static class UserLoader extends EntryCreator<String, Account.Id> {
+  static class UserLoader extends CacheLoader<String, Optional<Account.Id>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
@@ -328,25 +281,23 @@
     }
 
     @Override
-    public Account.Id createEntry(final String username) throws Exception {
+    public Optional<Account.Id> load(String username) throws Exception {
+      final ReviewDb db = schema.open();
       try {
-        final ReviewDb db = schema.open();
-        try {
-          final AccountExternalId extId =
-              db.accountExternalIds().get(
-                  new AccountExternalId.Key(SCHEME_GERRIT, username));
-          return extId != null ? extId.getAccountId() : null;
-        } finally {
-          db.close();
+        final AccountExternalId extId =
+            db.accountExternalIds().get(
+                new AccountExternalId.Key(SCHEME_GERRIT, username));
+        if (extId != null) {
+          return Optional.of(extId.getAccountId());
         }
-      } catch (OrmException e) {
-        log.warn("Cannot query for username in database", e);
-        return null;
+        return Optional.absent();
+      } finally {
+        db.close();
       }
     }
   }
 
-  static class MemberLoader extends EntryCreator<String, Set<AccountGroup.UUID>> {
+  static class MemberLoader extends CacheLoader<String, Set<AccountGroup.UUID>> {
     private final Helper helper;
 
     @Inject
@@ -355,8 +306,7 @@
     }
 
     @Override
-    public Set<AccountGroup.UUID> createEntry(final String username)
-        throws Exception {
+    public Set<AccountGroup.UUID> load(String username) throws Exception {
       final DirContext ctx = helper.open();
       try {
         return helper.queryForGroups(ctx, username, null);
@@ -368,10 +318,34 @@
         }
       }
     }
+  }
+
+  static class ExistenceLoader extends CacheLoader<String, Boolean> {
+    private final Helper helper;
+
+    @Inject
+    ExistenceLoader(final Helper helper) {
+      this.helper = helper;
+    }
 
     @Override
-    public Set<AccountGroup.UUID> missing(final String key) {
-      return Collections.emptySet();
+    public Boolean load(final String groupDn) throws Exception {
+      final DirContext ctx = helper.open();
+      try {
+        Name compositeGroupName = new CompositeName().add(groupDn);
+        try {
+          ctx.getAttributes(compositeGroupName);
+          return true;
+        } catch (NamingException e) {
+          return false;
+        }
+      } finally {
+        try {
+          ctx.close();
+        } catch (NamingException e) {
+          log.warn("Cannot close LDAP query handle", e);
+        }
+      }
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/Cache.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/Cache.java
deleted file mode 100644
index 7892ea1..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/Cache.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.cache;
-
-/**
- * A fast in-memory and/or on-disk based cache.
- *
- * @type <K> type of key used to lookup entries in the cache.
- * @type <V> type of value stored within each cache entry.
- */
-public interface Cache<K, V> {
-  /** Get the element from the cache, or null if not stored in the cache. */
-  public V get(K key);
-
-  /** Put one element into the cache, replacing any existing value. */
-  public void put(K key, V value);
-
-  /** Remove any existing value from the cache, no-op if not present. */
-  public void remove(K key);
-
-  /** Remove all cached items. */
-  public void removeAll();
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheBinding.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheBinding.java
new file mode 100644
index 0000000..625bd14
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheBinding.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.cache;
+
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.Weigher;
+import com.google.inject.TypeLiteral;
+
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.Nullable;
+
+/** Configure a cache declared within a {@link CacheModule} instance. */
+public interface CacheBinding<K, V> {
+  /** Set the total size of the cache. */
+  CacheBinding<K, V> maximumWeight(long weight);
+
+  /** Set the time an element lives before being expired. */
+  CacheBinding<K, V> expireAfterWrite(long duration, TimeUnit durationUnits);
+
+  /** Populate the cache with items from the CacheLoader. */
+  CacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> clazz);
+
+  /** Algorithm to weigh an object with a method other than the unit weight 1. */
+  CacheBinding<K, V> weigher(Class<? extends Weigher<K, V>> clazz);
+
+  String name();
+  TypeLiteral<K> keyType();
+  TypeLiteral<V> valueType();
+  long maximumWeight();
+  @Nullable Long expireAfterWrite(TimeUnit unit);
+  @Nullable Weigher<K, V> weigher();
+  @Nullable CacheLoader<K, V> loader();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
index 7fb3b3b..c1e92da 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
@@ -14,33 +14,41 @@
 
 package com.google.gerrit.server.cache;
 
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.cache.Weigher;
+import com.google.gerrit.extensions.annotations.Exports;
 import com.google.inject.AbstractModule;
 import com.google.inject.Key;
 import com.google.inject.Provider;
 import com.google.inject.Scopes;
 import com.google.inject.TypeLiteral;
-import com.google.inject.internal.UniqueAnnotations;
 import com.google.inject.name.Names;
+import com.google.inject.util.Types;
 
 import java.io.Serializable;
+import java.lang.reflect.Type;
 
 /**
  * Miniature DSL to support binding {@link Cache} instances in Guice.
  */
 public abstract class CacheModule extends AbstractModule {
+  private static final TypeLiteral<Cache<?, ?>> ANY_CACHE =
+      new TypeLiteral<Cache<?, ?>>() {};
+
   /**
-   * Declare an unnamed in-memory cache.
+   * Declare a named in-memory cache.
    *
    * @param <K> type of key used to lookup entries.
    * @param <V> type of value stored by the cache.
-   * @param type type literal for the cache, this literal will be used to match
-   *        injection sites.
-   * @return binding to describe the cache. Caller must set at least the name on
-   *         the returned binding.
+   * @return binding to describe the cache.
    */
-  protected <K, V> UnnamedCacheBinding<K, V> core(
-      final TypeLiteral<Cache<K, V>> type) {
-    return core(Key.get(type));
+  protected <K, V> CacheBinding<K, V> cache(
+      String name,
+      Class<K> keyType,
+      Class<V> valType) {
+    return cache(name, TypeLiteral.get(keyType), TypeLiteral.get(valType));
   }
 
   /**
@@ -48,74 +56,127 @@
    *
    * @param <K> type of key used to lookup entries.
    * @param <V> type of value stored by the cache.
-   * @param type type literal for the cache, this literal will be used to match
-   *        injection sites. Injection sites are matched by this type literal
-   *        and with {@code @Named} annotations.
    * @return binding to describe the cache.
    */
-  protected <K, V> NamedCacheBinding<K, V> core(
-      final TypeLiteral<Cache<K, V>> type, final String name) {
-    return core(Key.get(type, Names.named(name))).name(name);
-  }
-
-  private <K, V> UnnamedCacheBinding<K, V> core(final Key<Cache<K, V>> key) {
-    final boolean disk = false;
-    final CacheProvider<K, V> b = new CacheProvider<K, V>(disk, this);
-    bind(key).toProvider(b).in(Scopes.SINGLETON);
-    return b;
+  protected <K, V> CacheBinding<K, V> cache(
+      String name,
+      Class<K> keyType,
+      TypeLiteral<V> valType) {
+    return cache(name, TypeLiteral.get(keyType), valType);
   }
 
   /**
-   * Declare an unnamed in-memory/on-disk cache.
+   * Declare a named in-memory cache.
    *
-   * @param <K> type of key used to find entries, must be {@link Serializable}.
-   * @param <V> type of value stored by the cache, must be {@link Serializable}.
-   * @param type type literal for the cache, this literal will be used to match
-   *        injection sites. Injection sites are matched by this type literal
-   *        and with {@code @Named} annotations.
-   * @return binding to describe the cache. Caller must set at least the name on
-   *         the returned binding.
+   * @param <K> type of key used to lookup entries.
+   * @param <V> type of value stored by the cache.
+   * @return binding to describe the cache.
    */
-  protected <K extends Serializable, V extends Serializable> UnnamedCacheBinding<K, V> disk(
-      final TypeLiteral<Cache<K, V>> type) {
-    return disk(Key.get(type));
+  protected <K, V> CacheBinding<K, V> cache(
+      String name,
+      TypeLiteral<K> keyType,
+      TypeLiteral<V> valType) {
+    Type type = Types.newParameterizedType(
+        Cache.class,
+        keyType.getType(), valType.getType());
+
+    @SuppressWarnings("unchecked")
+    Key<Cache<K, V>> key = (Key<Cache<K, V>>) Key.get(type, Names.named(name));
+
+    CacheProvider<K, V> m =
+        new CacheProvider<K, V>(this, name, keyType, valType);
+    bind(key).toProvider(m).in(Scopes.SINGLETON);
+    bind(ANY_CACHE).annotatedWith(Exports.named(name)).to(key);
+    return m.maximumWeight(1024);
+  }
+
+  <K,V> Provider<CacheLoader<K,V>> bindCacheLoader(
+      CacheProvider<K, V> m,
+      Class<? extends CacheLoader<K,V>> impl) {
+    Type type = Types.newParameterizedType(
+        Cache.class,
+        m.keyType().getType(), m.valueType().getType());
+
+    Type loadingType = Types.newParameterizedType(
+        LoadingCache.class,
+        m.keyType().getType(), m.valueType().getType());
+
+    Type loaderType = Types.newParameterizedType(
+        CacheLoader.class,
+        m.keyType().getType(), m.valueType().getType());
+
+    @SuppressWarnings("unchecked")
+    Key<LoadingCache<K, V>> key =
+        (Key<LoadingCache<K, V>>) Key.get(type, Names.named(m.name));
+
+    @SuppressWarnings("unchecked")
+    Key<LoadingCache<K, V>> loadingKey =
+        (Key<LoadingCache<K, V>>) Key.get(loadingType, Names.named(m.name));
+
+    @SuppressWarnings("unchecked")
+    Key<CacheLoader<K, V>> loaderKey =
+        (Key<CacheLoader<K, V>>) Key.get(loaderType, Names.named(m.name));
+
+    bind(loaderKey).to(impl).in(Scopes.SINGLETON);
+    bind(loadingKey).to(key);
+    return getProvider(loaderKey);
+  }
+
+  <K,V> Provider<Weigher<K,V>> bindWeigher(
+      CacheProvider<K, V> m,
+      Class<? extends Weigher<K,V>> impl) {
+    Type weigherType = Types.newParameterizedType(
+        Weigher.class,
+        m.keyType().getType(), m.valueType().getType());
+
+    @SuppressWarnings("unchecked")
+    Key<Weigher<K, V>> key =
+        (Key<Weigher<K, V>>) Key.get(weigherType, Names.named(m.name));
+
+    bind(key).to(impl).in(Scopes.SINGLETON);
+    return getProvider(key);
   }
 
   /**
    * Declare a named in-memory/on-disk cache.
    *
-   * @param <K> type of key used to find entries, must be {@link Serializable}.
-   * @param <V> type of value stored by the cache, must be {@link Serializable}.
-   * @param type type literal for the cache, this literal will be used to match
-   *        injection sites. Injection sites are matched by this type literal
-   *        and with {@code @Named} annotations.
+   * @param <K> type of key used to lookup entries.
+   * @param <V> type of value stored by the cache.
    * @return binding to describe the cache.
    */
-  protected <K extends Serializable, V extends Serializable> NamedCacheBinding<K, V> disk(
-      final TypeLiteral<Cache<K, V>> type, final String name) {
-    return disk(Key.get(type, Names.named(name))).name(name);
+  protected <K extends Serializable, V extends Serializable> CacheBinding<K, V> persist(
+      String name,
+      Class<K> keyType,
+      Class<V> valType) {
+    return persist(name, TypeLiteral.get(keyType), TypeLiteral.get(valType));
   }
 
-  private <K, V> UnnamedCacheBinding<K, V> disk(final Key<Cache<K, V>> key) {
-    final boolean disk = true;
-    final CacheProvider<K, V> b = new CacheProvider<K, V>(disk, this);
-    bind(key).toProvider(b).in(Scopes.SINGLETON);
-    return b;
+  /**
+   * Declare a named in-memory/on-disk cache.
+   *
+   * @param <K> type of key used to lookup entries.
+   * @param <V> type of value stored by the cache.
+   * @return binding to describe the cache.
+   */
+  protected <K extends Serializable, V extends Serializable> CacheBinding<K, V> persist(
+      String name,
+      Class<K> keyType,
+      TypeLiteral<V> valType) {
+    return persist(name, TypeLiteral.get(keyType), valType);
   }
 
-  <K, V> Provider<EntryCreator<K, V>> getEntryCreator(CacheProvider<K, V> cp,
-      Class<? extends EntryCreator<K, V>> type) {
-    Key<EntryCreator<K, V>> key = newKey();
-    bind(key).to(type).in(Scopes.SINGLETON);
-    return getProvider(key);
-  }
-
-  @SuppressWarnings("unchecked")
-  private static <K, V> Key<EntryCreator<K, V>> newKey() {
-    return (Key<EntryCreator<K, V>>) newKeyImpl();
-  }
-
-  private static Key<?> newKeyImpl() {
-    return Key.get(EntryCreator.class, UniqueAnnotations.create());
+  /**
+   * Declare a named in-memory/on-disk cache.
+   *
+   * @param <K> type of key used to lookup entries.
+   * @param <V> type of value stored by the cache.
+   * @return binding to describe the cache.
+   */
+  protected <K extends Serializable, V extends Serializable> CacheBinding<K, V> persist(
+      String name,
+      TypeLiteral<K> keyType,
+      TypeLiteral<V> valType) {
+    return ((CacheProvider<K, V>) cache(name, keyType, valType))
+        .persist(true);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
index 1fa047b..1b8eea5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// Copyright (C) 2012 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,130 +14,156 @@
 
 package com.google.gerrit.server.cache;
 
-import static com.google.gerrit.server.cache.EvictionPolicy.LFU;
-import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.Weigher;
+import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
+import com.google.inject.TypeLiteral;
 
 import java.util.concurrent.TimeUnit;
 
-public final class CacheProvider<K, V> implements Provider<Cache<K, V>>,
-    NamedCacheBinding<K, V>, UnnamedCacheBinding<K, V> {
+import javax.annotation.Nullable;
+
+class CacheProvider<K, V>
+    implements Provider<Cache<K, V>>,
+    CacheBinding<K, V> {
   private final CacheModule module;
-  private final boolean disk;
-  private int memoryLimit;
-  private int diskLimit;
-  private long maxAge;
-  private EvictionPolicy evictionPolicy;
-  private String cacheName;
-  private ProxyCache<K, V> cache;
-  private Provider<EntryCreator<K, V>> entryCreator;
+  final String name;
+  private final TypeLiteral<K> keyType;
+  private final TypeLiteral<V> valType;
+  private boolean persist;
+  private long maximumWeight;
+  private Long expireAfterWrite;
+  private Provider<CacheLoader<K, V>> loader;
+  private Provider<Weigher<K, V>> weigher;
 
-  CacheProvider(final boolean disk, CacheModule module) {
-    this.disk = disk;
+  private String plugin;
+  private MemoryCacheFactory memoryCacheFactory;
+  private PersistentCacheFactory persistentCacheFactory;
+  private boolean frozen;
+
+  CacheProvider(CacheModule module,
+      String name,
+      TypeLiteral<K> keyType,
+      TypeLiteral<V> valType) {
     this.module = module;
+    this.name = name;
+    this.keyType = keyType;
+    this.valType = valType;
+  }
 
-    memoryLimit(1024);
-    maxAge(90, DAYS);
-    evictionPolicy(LFU);
-
-    if (disk) {
-      diskLimit(16384);
-    }
+  @Inject(optional = true)
+  void setPluginName(@PluginName String pluginName) {
+    this.plugin = pluginName;
   }
 
   @Inject
-  void setCachePool(final CachePool pool) {
-    this.cache = pool.register(this);
+  void setMemoryCacheFactory(MemoryCacheFactory factory) {
+    this.memoryCacheFactory = factory;
   }
 
-  public void bind(Cache<K, V> impl) {
-    if (cache == null) {
-      throw new ProvisionException("Cache was never registered");
-    }
-    cache.bind(impl);
+  @Inject(optional = true)
+  void setPersistentCacheFactory(@Nullable PersistentCacheFactory factory) {
+    this.persistentCacheFactory = factory;
   }
 
-  public EntryCreator<K, V> getEntryCreator() {
-    return entryCreator != null ? entryCreator.get() : null;
-  }
-
-  public String getName() {
-    if (cacheName == null) {
-      throw new ProvisionException("Cache has no name");
-    }
-    return cacheName;
-  }
-
-  public boolean disk() {
-    return disk;
-  }
-
-  public int memoryLimit() {
-    return memoryLimit;
-  }
-
-  public int diskLimit() {
-    return diskLimit;
-  }
-
-  public long maxAge() {
-    return maxAge;
-  }
-
-  public EvictionPolicy evictionPolicy() {
-    return evictionPolicy;
-  }
-
-  public NamedCacheBinding<K, V> name(final String name) {
-    if (cacheName != null) {
-      throw new IllegalStateException("Cache name already set");
-    }
-    cacheName = name;
-    return this;
-  }
-
-  public NamedCacheBinding<K, V> memoryLimit(final int objects) {
-    memoryLimit = objects;
-    return this;
-  }
-
-  public NamedCacheBinding<K, V> diskLimit(final int objects) {
-    if (!disk) {
-      // TODO This should really be a compile time type error, but I'm
-      // too lazy to create the mess of permutations required to setup
-      // type safe returns for bindings in our little DSL.
-      //
-      throw new IllegalStateException("Cache is not disk based");
-    }
-    diskLimit = objects;
-    return this;
-  }
-
-  public NamedCacheBinding<K, V> maxAge(final long duration, final TimeUnit unit) {
-    maxAge = SECONDS.convert(duration, unit);
+  CacheBinding<K, V> persist(boolean p) {
+    Preconditions.checkState(!frozen, "binding frozen, cannot be modified");
+    persist = p;
     return this;
   }
 
   @Override
-  public NamedCacheBinding<K, V> evictionPolicy(final EvictionPolicy policy) {
-    evictionPolicy = policy;
+  public CacheBinding<K, V> maximumWeight(long weight) {
+    Preconditions.checkState(!frozen, "binding frozen, cannot be modified");
+    maximumWeight = weight;
     return this;
   }
 
-  public NamedCacheBinding<K, V> populateWith(
-      Class<? extends EntryCreator<K, V>> creator) {
-    entryCreator = module.getEntryCreator(this, creator);
+  @Override
+  public CacheBinding<K, V> expireAfterWrite(long duration, TimeUnit unit) {
+    Preconditions.checkState(!frozen, "binding frozen, cannot be modified");
+    expireAfterWrite = SECONDS.convert(duration, unit);
     return this;
   }
 
-  public Cache<K, V> get() {
-    if (cache == null) {
-      throw new ProvisionException("Cache \"" + cacheName + "\" not available");
+  @Override
+  public CacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> impl) {
+    Preconditions.checkState(!frozen, "binding frozen, cannot be modified");
+    loader = module.bindCacheLoader(this, impl);
+    return this;
+  }
+
+  @Override
+  public CacheBinding<K, V> weigher(Class<? extends Weigher<K, V>> impl) {
+    Preconditions.checkState(!frozen, "binding frozen, cannot be modified");
+    weigher = module.bindWeigher(this, impl);
+    return this;
+  }
+
+  @Override
+  public String name() {
+    if (!Strings.isNullOrEmpty(plugin)) {
+      return plugin + "." + name;
     }
-    return cache;
+    return name;
+  }
+
+  @Override
+  public TypeLiteral<K> keyType() {
+    return keyType;
+  }
+
+  @Override
+  public TypeLiteral<V> valueType() {
+    return valType;
+  }
+
+  @Override
+  public long maximumWeight() {
+    return maximumWeight;
+  }
+
+  @Override
+  @Nullable
+  public Long expireAfterWrite(TimeUnit unit) {
+   return expireAfterWrite != null
+       ? unit.convert(expireAfterWrite, SECONDS)
+       : null;
+  }
+
+  @Override
+  @Nullable
+  public Weigher<K, V> weigher() {
+    return weigher != null ? weigher.get() : null;
+  }
+
+  @Override
+  @Nullable
+  public CacheLoader<K, V> loader() {
+    return loader != null ? loader.get() : null;
+  }
+
+  @Override
+  public Cache<K, V> get() {
+    frozen = true;
+
+    if (loader != null) {
+      CacheLoader<K, V> ldr = loader.get();
+      if (persist && persistentCacheFactory != null) {
+        return persistentCacheFactory.build(this, ldr);
+      }
+      return memoryCacheFactory.build(this, ldr);
+    } else if (persist && persistentCacheFactory != null) {
+      return persistentCacheFactory.build(this);
+    } else {
+      return memoryCacheFactory.build(this);
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java
deleted file mode 100644
index bafdc49..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.package com.google.gerrit.server.git;
-
-package com.google.gerrit.server.cache;
-
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * An infinitely sized cache backed by java.util.ConcurrentHashMap.
- * <p>
- * This cache type is only suitable for unit tests, as it has no upper limit on
- * number of items held in the cache. No upper limit can result in memory leaks
- * in production servers.
- */
-public class ConcurrentHashMapCache<K, V> implements Cache<K, V> {
-  private final ConcurrentHashMap<K, V> map = new ConcurrentHashMap<K, V>();
-
-  @Override
-  public V get(K key) {
-    return map.get(key);
-  }
-
-  @Override
-  public void put(K key, V value) {
-    map.put(key, value);
-  }
-
-  @Override
-  public void remove(K key) {
-    map.remove(key);
-  }
-
-  @Override
-  public void removeAll() {
-    map.clear();
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/EntryCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/EntryCreator.java
deleted file mode 100644
index af07e08..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/EntryCreator.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.cache;
-
-/**
- * Creates a cache entry on demand when its not found.
- *
- * @param <K> type of the cache's key.
- * @param <V> type of the cache's value element.
- */
-public abstract class EntryCreator<K, V> {
-  /**
-   * Invoked on a cache miss, to compute the cache entry.
-   *
-   * @param key entry whose content needs to be obtained.
-   * @return new cache content. The caller will automatically put this object
-   *         into the cache.
-   * @throws Exception the cache content cannot be computed. No entry will be
-   *         stored in the cache, and {@link #missing(Object)} will be invoked
-   *         instead. Future requests for the same key will retry this method.
-   */
-  public abstract V createEntry(K key) throws Exception;
-
-  /** Invoked when {@link #createEntry(Object)} fails, by default return null. */
-  public V missing(K key) {
-    return null;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/EvictionPolicy.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/EvictionPolicy.java
deleted file mode 100644
index cff4f11..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/EvictionPolicy.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.cache;
-
-/** How entries should be evicted from the cache. */
-public enum EvictionPolicy {
-  /** Least recently used is evicted first. */
-  LRU,
-
-  /** Least frequently used is evicted first. */
-  LFU;
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/MemoryCacheFactory.java
similarity index 62%
copy from gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/cache/MemoryCacheFactory.java
index 204d777..6b8b489 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/MemoryCacheFactory.java
@@ -12,12 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.server.cache;
 
-public class IncompleteUserInfoException extends Exception {
-  private static final long serialVersionUID = 1L;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
 
-  public IncompleteUserInfoException(final String userName, final String missingInfo) {
-    super("For the user \"" + userName + "\" " + missingInfo + " is not set.");
-  }
+public interface MemoryCacheFactory {
+  <K, V> Cache<K, V> build(CacheBinding<K, V> def);
+
+  <K, V> LoadingCache<K, V> build(
+      CacheBinding<K, V> def,
+      CacheLoader<K, V> loader);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/NamedCacheBinding.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/NamedCacheBinding.java
deleted file mode 100644
index 3394c71..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/NamedCacheBinding.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.cache;
-
-import java.util.concurrent.TimeUnit;
-
-/** Configure a cache declared within a {@link CacheModule} instance. */
-public interface NamedCacheBinding<K, V> {
-  /** Set the number of objects to cache in memory. */
-  public NamedCacheBinding<K, V> memoryLimit(int objects);
-
-  /** Set the number of objects to cache in memory. */
-  public NamedCacheBinding<K, V> diskLimit(int objects);
-
-  /** Set the time an element lives before being expired. */
-  public NamedCacheBinding<K, V> maxAge(long duration, TimeUnit durationUnits);
-
-  /** Set the eviction policy for elements when the cache is full. */
-  public NamedCacheBinding<K, V> evictionPolicy(EvictionPolicy policy);
-
-  /** Populate the cache with items from the EntryCreator. */
-  public NamedCacheBinding<K, V> populateWith(Class<? extends EntryCreator<K, V>> creator);
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
similarity index 62%
copy from gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
index 204d777..983e956 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
@@ -12,12 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.server.cache;
 
-public class IncompleteUserInfoException extends Exception {
-  private static final long serialVersionUID = 1L;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
 
-  public IncompleteUserInfoException(final String userName, final String missingInfo) {
-    super("For the user \"" + userName + "\" " + missingInfo + " is not set.");
-  }
+public interface PersistentCacheFactory {
+  <K, V> Cache<K, V> build(CacheBinding<K, V> def);
+
+  <K, V> LoadingCache<K, V> build(
+      CacheBinding<K, V> def,
+      CacheLoader<K, V> loader);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyCache.java
deleted file mode 100644
index c1b0292..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyCache.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.cache;
-
-/** Proxy around a cache which has not yet been created. */
-public final class ProxyCache<K, V> implements Cache<K, V> {
-  private volatile Cache<K, V> self;
-
-  public void bind(Cache<K, V> self) {
-    this.self = self;
-  }
-
-  public V get(K key) {
-    return self.get(key);
-  }
-
-  public void put(K key, V value) {
-    self.put(key, value);
-  }
-
-  public void remove(K key) {
-    self.remove(key);
-  }
-
-  public void removeAll() {
-    self.removeAll();
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
deleted file mode 100644
index 43039e1..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.cache;
-
-
-/** Configure a cache declared within a {@link CacheModule} instance. */
-public interface UnnamedCacheBinding<K, V> {
-  /** Set the name of the cache. */
-  public NamedCacheBinding<K, V> name(String cacheName);
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java
index 83fa671..bc0ef61 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.mail.AbandonedSender;
 import com.google.gerrit.server.mail.EmailException;
 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.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
@@ -69,8 +68,8 @@
   }
 
   @Override
-  public ReviewResult call() throws EmailException,
-      InvalidChangeOperationException, NoSuchChangeException, OrmException {
+  public ReviewResult call() throws EmailException, NoSuchChangeException,
+      OrmException {
     final ReviewResult result = new ReviewResult();
     result.setChangeId(changeId);
 
@@ -102,8 +101,7 @@
           new AtomicUpdate<Change>() {
         @Override
         public Change update(Change change) {
-          if (change.getStatus().isOpen()
-              && change.currentPatchSetId().equals(patchSetId)) {
+          if (change.getStatus().isOpen()) {
             change.setStatus(Change.Status.ABANDONED);
             ChangeUtil.updated(change);
             return change;
@@ -112,9 +110,15 @@
           }
         }
       });
-      ChangeUtil.updatedChange(
-          db, currentUser, updatedChange, cmsg, abandonedSenderFactory,
-          "Change is no longer open or patchset is not latest");
+
+      if (updatedChange == null) {
+        result.addError(new ReviewResult.Error(
+            ReviewResult.Error.Type.CHANGE_IS_CLOSED));
+        return result;
+      }
+
+      ChangeUtil.updatedChange(db, currentUser, updatedChange, cmsg,
+                               abandonedSenderFactory);
       hooks.doChangeAbandonedHook(updatedChange, currentUser.getAccount(),
                                   changeComment, db);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
index 268e118..f466231 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
@@ -20,8 +20,8 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
@@ -44,7 +44,7 @@
   private final ChangeControl.Factory changeControlFactory;
   private final ReviewDb db;
   private final GitRepositoryManager gitManager;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated replication;
   private final PatchSetInfoFactory patchSetInfoFactory;
 
   private final PatchSet.Id patchSetId;
@@ -52,7 +52,7 @@
   @Inject
   DeleteDraftPatchSet(ChangeControl.Factory changeControlFactory,
       ReviewDb db, GitRepositoryManager gitManager,
-      ReplicationQueue replication, PatchSetInfoFactory patchSetInfoFactory,
+      GitReferenceUpdated replication, PatchSetInfoFactory patchSetInfoFactory,
       @Assisted final PatchSet.Id patchSetId) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java
index 966efce..859807c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.mail.RestoredSender;
 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.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
@@ -77,9 +76,8 @@
   }
 
   @Override
-  public ReviewResult call() throws EmailException,
-      InvalidChangeOperationException, NoSuchChangeException, OrmException,
-      RepositoryNotFoundException, IOException {
+  public ReviewResult call() throws EmailException, NoSuchChangeException,
+      OrmException, RepositoryNotFoundException, IOException {
     final ReviewResult result = new ReviewResult();
     result.setChangeId(changeId);
 
@@ -121,8 +119,7 @@
         new AtomicUpdate<Change>() {
           @Override
           public Change update(Change change) {
-            if (change.getStatus() == Change.Status.ABANDONED
-                && change.currentPatchSetId().equals(patchSetId)) {
+            if (change.getStatus() == Change.Status.ABANDONED) {
               change.setStatus(Change.Status.NEW);
               ChangeUtil.updated(change);
               return change;
@@ -132,10 +129,14 @@
           }
         });
 
-    ChangeUtil.updatedChange(
-        db, currentUser, updatedChange, cmsg, restoredSenderFactory,
-        "Change is not abandoned or patchset is not latest");
+    if (updatedChange == null) {
+      result.addError(new ReviewResult.Error(
+          ReviewResult.Error.Type.CHANGE_NOT_ABANDONED));
+      return result;
+    }
 
+    ChangeUtil.updatedChange(db, currentUser, updatedChange, cmsg,
+                             restoredSenderFactory);
     hooks.doChangeRestoreHook(updatedChange, currentUser.getAccount(),
                               changeComment, db);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.java
index 6648c7b..9cff992 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.java
@@ -113,6 +113,10 @@
                 errMsg.append("change " + changeId + ": needs " + lbl.label);
                 break;
 
+              case MAY:
+                // The MAY label didn't cause the NOT_READY status
+                break;
+
               case IMPOSSIBLE:
                 if (errMsg.length() > 0) errMsg.append("; ");
                 errMsg.append("change " + changeId + ": needs " + lbl.label
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 99dd54a..9e3aeca 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
@@ -16,14 +16,20 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
+import com.google.common.cache.Cache;
 import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.NewProjectCreatedListener;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.rules.PrologModule;
 import com.google.gerrit.rules.RulesCache;
+import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.FileTypeRegistry;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.MimeUtilFileTypeRegistry;
-import com.google.gerrit.server.ReplicationUser;
 import com.google.gerrit.server.account.AccountByEmailCacheImpl;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
@@ -32,19 +38,22 @@
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.DefaultRealm;
 import com.google.gerrit.server.account.EmailExpander;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupCacheImpl;
+import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.account.GroupIncludeCacheImpl;
 import com.google.gerrit.server.account.GroupInfoCacheFactory;
-import com.google.gerrit.server.account.MaterializedGroupMembership;
+import com.google.gerrit.server.account.IncludingGroupMembership;
+import com.google.gerrit.server.account.InternalGroupBackend;
 import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.account.UniversalGroupBackend;
 import com.google.gerrit.server.auth.ldap.LdapModule;
 import com.google.gerrit.server.events.EventFactory;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.ChangeMergeQueue;
 import com.google.gerrit.server.git.GitModule;
 import com.google.gerrit.server.git.MergeQueue;
-import com.google.gerrit.server.git.PushAllProjectsOp;
 import com.google.gerrit.server.git.ReloadSubmitQueueOp;
-import com.google.gerrit.server.git.SecureCredentialsProvider;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.mail.FromAddressGenerator;
@@ -62,8 +71,10 @@
 import com.google.gerrit.server.project.SectionSortCache;
 import com.google.gerrit.server.tools.ToolsCatalog;
 import com.google.gerrit.server.util.IdGenerator;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.workflow.FunctionState;
 import com.google.inject.Inject;
+import com.google.inject.TypeLiteral;
 
 import org.apache.velocity.runtime.RuntimeInstance;
 import org.eclipse.jgit.lib.Config;
@@ -115,26 +126,31 @@
     install(new AccessControlModule());
     install(new GitModule());
     install(new PrologModule());
+    install(ThreadLocalRequestContext.module());
 
     factory(AccountInfoCacheFactory.Factory.class);
     factory(CapabilityControl.Factory.class);
     factory(GroupInfoCacheFactory.Factory.class);
+    factory(InternalUser.Factory.class);
     factory(ProjectNode.Factory.class);
     factory(ProjectState.Factory.class);
-    factory(MaterializedGroupMembership.Factory.class);
     bind(PermissionCollection.Factory.class);
     bind(AccountVisibility.class)
         .toProvider(AccountVisibilityProvider.class)
         .in(SINGLETON);
 
+    bind(GroupControl.Factory.class).in(SINGLETON);
+    factory(IncludingGroupMembership.Factory.class);
+    bind(InternalGroupBackend.class).in(SINGLETON);
+    bind(GroupBackend.class).to(UniversalGroupBackend.class).in(SINGLETON);
+    DynamicSet.setOf(binder(), GroupBackend.class);
+    DynamicSet.bind(binder(), GroupBackend.class).to(InternalGroupBackend.class);
+
     bind(FileTypeRegistry.class).to(MimeUtilFileTypeRegistry.class);
     bind(ToolsCatalog.class);
     bind(EventFactory.class);
     bind(TransferConfig.class);
 
-    factory(SecureCredentialsProvider.Factory.class);
-    factory(PushAllProjectsOp.Factory.class);
-
     bind(ChangeMergeQueue.class).in(SINGLETON);
     bind(MergeQueue.class).to(ChangeMergeQueue.class).in(SINGLETON);
     factory(ReloadSubmitQueueOp.Factory.class);
@@ -150,6 +166,12 @@
     bind(ChangeControl.GenericFactory.class);
     bind(ProjectControl.GenericFactory.class);
     factory(FunctionState.Factory.class);
-    factory(ReplicationUser.Factory.class);
+
+    bind(GitReferenceUpdated.class);
+    DynamicMap.mapOf(binder(), new TypeLiteral<Cache<?, ?>>() {});
+    DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
+    DynamicSet.setOf(binder(), NewProjectCreatedListener.class);
+
+    bind(AnonymousUser.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
index f581adc..226f926 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
@@ -17,13 +17,11 @@
 import static com.google.inject.Scopes.SINGLETON;
 
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RequestCleanup;
 import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.account.GroupDetailFactory;
 import com.google.gerrit.server.account.GroupMembers;
 import com.google.gerrit.server.account.PerformCreateGroup;
@@ -77,10 +75,8 @@
     bind(ListProjects.class);
     bind(ApprovalsUtil.class);
 
-    bind(AnonymousUser.class).in(RequestScoped.class);
     bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
     bind(ChangeControl.Factory.class).in(SINGLETON);
-    bind(GroupControl.Factory.class).in(SINGLETON);
     bind(ProjectControl.Factory.class).in(SINGLETON);
     bind(AccountControl.Factory.class).in(SINGLETON);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
index c89f025..8b517a3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.config;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
@@ -24,9 +24,9 @@
 
 public class GitReceivePackGroupsProvider extends GroupSetProvider {
   @Inject
-  public GitReceivePackGroupsProvider(GroupCache gc,
+  public GitReceivePackGroupsProvider(GroupBackend gb,
       @GerritServerConfig Config config) {
-    super(gc, config, "receive", null, "allowGroup");
+    super(gb, config, "receive", null, "allowGroup");
 
     // If no group was set, default to "registered users"
     //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
index b5de742..c519902 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.config;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
@@ -25,9 +25,9 @@
 
 public class GitUploadPackGroupsProvider extends GroupSetProvider {
   @Inject
-  public GitUploadPackGroupsProvider(GroupCache gc,
+  public GitUploadPackGroupsProvider(GroupBackend gb,
       @GerritServerConfig Config config) {
-    super(gc, config, "upload", null, "allowGroup");
+    super(gb, config, "upload", null, "allowGroup");
 
     // If no group was set, default to "registered users" and "anonymous"
     //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
index 3619cda..5fa243b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
@@ -15,8 +15,10 @@
 package com.google.gerrit.server.config;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupBackends;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -34,17 +36,17 @@
   protected Set<AccountGroup.UUID> groupIds;
 
   @Inject
-  protected GroupSetProvider(GroupCache groupCache,
+  protected GroupSetProvider(GroupBackend groupBackend,
       @GerritServerConfig Config config, String section,
       String subsection, String name) {
     String[] groupNames = config.getStringList(section, subsection, name);
     ImmutableSet.Builder<AccountGroup.UUID> builder = ImmutableSet.builder();
     for (String n : groupNames) {
-      AccountGroup g = groupCache.get(new AccountGroup.NameKey(n));
-      if (g != null) {
-        builder.add(g.getGroupUUID());
-      } else {
+      GroupReference g = GroupBackends.findBestSuggestion(groupBackend, n);
+      if (g == null) {
         log.warn("Group \"{0}\" not in database, skipping.", n);
+      } else {
+        builder.add(g.getUUID());
       }
     }
     groupIds = builder.build();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
index 26c76c5..bb95dca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
@@ -14,14 +14,11 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.git.PushAllProjectsOp;
 import com.google.gerrit.server.git.ReloadSubmitQueueOp;
 import com.google.inject.Inject;
 
-import org.eclipse.jgit.lib.Config;
-
 import java.util.concurrent.TimeUnit;
 
 /** Configuration for a master node in a cluster of servers. */
@@ -32,26 +29,15 @@
   }
 
   static class OnStart implements LifecycleListener {
-    private final PushAllProjectsOp.Factory pushAll;
     private final ReloadSubmitQueueOp.Factory submit;
-    private final boolean replicateOnStartup;
 
     @Inject
-    OnStart(final PushAllProjectsOp.Factory pushAll,
-        final ReloadSubmitQueueOp.Factory submit,
-        final @GerritServerConfig Config cfg) {
-      this.pushAll = pushAll;
+    OnStart(final ReloadSubmitQueueOp.Factory submit) {
       this.submit = submit;
-
-      replicateOnStartup = cfg.getBoolean("gerrit", "replicateOnStartup", true);
     }
 
     @Override
     public void start() {
-      if (replicateOnStartup) {
-        pushAll.create(null).start(30, TimeUnit.SECONDS);
-      }
-
       submit.create().start(15, TimeUnit.SECONDS);
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
index 7172b6f..6622b0f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
@@ -32,8 +32,8 @@
  */
 public class ProjectOwnerGroupsProvider extends GroupSetProvider {
   @Inject
-  public ProjectOwnerGroupsProvider(GroupCache gc,
+  public ProjectOwnerGroupsProvider(GroupBackend gb,
       @GerritServerConfig final Config config) {
-    super(gc, config, "repository", "*", "ownerGroup");
+    super(gb, config, "repository", "*", "ownerGroup");
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
index 4205420..8d76e90 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
@@ -31,6 +31,7 @@
   public final File tmp_dir;
   public final File logs_dir;
   public final File plugins_dir;
+  public final File data_dir;
   public final File mail_dir;
   public final File hooks_dir;
   public final File static_dir;
@@ -40,7 +41,6 @@
 
   public final File gerrit_config;
   public final File secure_config;
-  public final File replication_config;
   public final File contact_information_pub;
 
   public final File ssl_keystore;
@@ -66,6 +66,7 @@
     lib_dir = new File(site_path, "lib");
     tmp_dir = new File(site_path, "tmp");
     plugins_dir = new File(site_path, "plugins");
+    data_dir = new File(site_path, "data");
     logs_dir = new File(site_path, "logs");
     mail_dir = new File(etc_dir, "mail");
     hooks_dir = new File(site_path, "hooks");
@@ -76,7 +77,6 @@
 
     gerrit_config = new File(etc_dir, "gerrit.config");
     secure_config = new File(etc_dir, "secure.config");
-    replication_config = new File(etc_dir, "replication.config");
     contact_information_pub = new File(etc_dir, "contact_information.pub");
 
     ssl_keystore = new File(etc_dir, "keystore");
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
new file mode 100644
index 0000000..6bb7ba8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
@@ -0,0 +1,145 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.documentation;
+
+import static org.pegdown.Extensions.ALL;
+import static org.pegdown.Extensions.HARDWRAPS;
+
+import com.google.common.base.Strings;
+
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.pegdown.LinkRenderer;
+import org.pegdown.PegDownProcessor;
+import org.pegdown.ToHtmlSerializer;
+import org.pegdown.ast.HeaderNode;
+import org.pegdown.ast.Node;
+import org.pegdown.ast.RootNode;
+import org.pegdown.ast.TextNode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class MarkdownFormatter {
+  private static final Logger log =
+      LoggerFactory.getLogger(MarkdownFormatter.class);
+
+  private static final String css;
+
+  static {
+    AtomicBoolean file = new AtomicBoolean();
+    String src;
+    try {
+      src = readPegdownCss(file);
+    } catch (IOException err) {
+      log.warn("Cannot load pegdown.css", err);
+      src = "";
+    }
+    css = file.get() ? null : src;
+  }
+
+  private static String readCSS() {
+    if (css != null) {
+      return css;
+    }
+    try {
+      return readPegdownCss(new AtomicBoolean());
+    } catch (IOException err) {
+      log.warn("Cannot load pegdown.css", err);
+      return "";
+    }
+  }
+
+  public byte[] markdownToDocHtml(String md, String charEnc)
+      throws UnsupportedEncodingException {
+    RootNode root = parseMarkdown(md);
+    String title = findTitle(root);
+
+    StringBuilder html = new StringBuilder();
+    html.append("<html>");
+    html.append("<head>");
+    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("</head>");
+    html.append("<body>\n");
+    html.append(new ToHtmlSerializer(new LinkRenderer()).toHtml(root));
+    html.append("\n</body></html>");
+    return html.toString().getBytes(charEnc);
+  }
+
+  public String extractTitleFromMarkdown(byte[] data, String charEnc) {
+    String md = RawParseUtils.decode(Charset.forName(charEnc), data);
+    return findTitle(parseMarkdown(md));
+  }
+
+  private String findTitle(Node root) {
+    if (root instanceof HeaderNode) {
+      HeaderNode h = (HeaderNode) root;
+      if (h.getLevel() == 1
+          && h.getChildren() != null
+          && !h.getChildren().isEmpty()) {
+        StringBuilder b = new StringBuilder();
+        for (Node n : root.getChildren()) {
+          if (n instanceof TextNode) {
+            b.append(((TextNode) n).getText());
+          }
+        }
+        return b.toString();
+      }
+    }
+
+    for (Node n : root.getChildren()) {
+      String title = findTitle(n);
+      if (title != null) {
+        return title;
+      }
+    }
+    return null;
+  }
+
+  private RootNode parseMarkdown(String md) {
+    return new PegDownProcessor(ALL & ~(HARDWRAPS))
+        .parseMarkdown(md.toCharArray());
+  }
+
+  private static String readPegdownCss(AtomicBoolean file)
+      throws IOException {
+    String name = "pegdown.css";
+    URL url = MarkdownFormatter.class.getResource(name);
+    if (url == null) {
+      throw new FileNotFoundException("Resource " + name);
+    }
+    file.set("file".equals(url.getProtocol()));
+    InputStream in = url.openStream();
+    try {
+      TemporaryBuffer.Heap tmp = new TemporaryBuffer.Heap(128 * 1024);
+      tmp.copy(in);
+      return new String(tmp.toByteArray(), "UTF-8");
+    } finally {
+      in.close();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index c538aa6..9fa582a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -232,16 +233,19 @@
 
   public void addPatchSetFileNames(PatchSetAttribute patchSetAttribute,
       Change change, PatchSet patchSet) {
-    PatchList patchList = patchListCache.get(change, patchSet);
-    for (PatchListEntry patch : patchList.getPatches()) {
-      if (patchSetAttribute.files == null) {
-        patchSetAttribute.files = new ArrayList<PatchAttribute>();
-      }
+    try {
+      PatchList patchList = patchListCache.get(change, patchSet);
+      for (PatchListEntry patch : patchList.getPatches()) {
+        if (patchSetAttribute.files == null) {
+          patchSetAttribute.files = new ArrayList<PatchAttribute>();
+        }
 
-      PatchAttribute p = new PatchAttribute();
-      p.file = patch.getNewName();
-      p.type = patch.getChangeType();
-      patchSetAttribute.files.add(p);
+        PatchAttribute p = new PatchAttribute();
+        p.file = patch.getNewName();
+        p.type = patch.getChangeType();
+        patchSetAttribute.files.add(p);
+      }
+    } catch (PatchListNotAvailableException e) {
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
new file mode 100644
index 0000000..833b611
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.extensions.events;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
+
+import java.util.Collections;
+import java.util.List;
+
+public class GitReferenceUpdated {
+  public static final GitReferenceUpdated DISABLED = new GitReferenceUpdated(
+      Collections.<GitReferenceUpdatedListener> emptyList());
+
+  private final Iterable<GitReferenceUpdatedListener> listeners;
+
+  @Inject
+  GitReferenceUpdated(DynamicSet<GitReferenceUpdatedListener> listeners) {
+    this.listeners = listeners;
+  }
+
+  GitReferenceUpdated(Iterable<GitReferenceUpdatedListener> listeners) {
+    this.listeners = listeners;
+  }
+
+  public void fire(Project.NameKey project, String ref) {
+    Event event = new Event(project, ref);
+    for (GitReferenceUpdatedListener l : listeners) {
+      l.onGitReferenceUpdated(event);
+    }
+  }
+
+  private static class Event implements GitReferenceUpdatedListener.Event {
+    private final String projectName;
+    private final String ref;
+
+    Event(Project.NameKey project, String ref) {
+      this.projectName = project.get();
+      this.ref = ref;
+    }
+
+    @Override
+    public String getProjectName() {
+      return projectName;
+    }
+
+    @Override
+    public List<GitReferenceUpdatedListener.Update> getUpdates() {
+      GitReferenceUpdatedListener.Update update =
+          new GitReferenceUpdatedListener.Update() {
+            public String getRefName() {
+              return ref;
+            }
+          };
+      return ImmutableList.of(update);
+    }
+  }
+}
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 c9c9753..4bfee9c 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
@@ -17,10 +17,8 @@
 import static com.google.gerrit.server.git.GitRepositoryManager.REF_REJECT_COMMITS;
 
 import com.google.gerrit.common.errors.PermissionDeniedException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -44,7 +42,9 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
+import java.util.Date;
 import java.util.List;
+import java.util.TimeZone;
 
 public class BanCommit {
 
@@ -55,25 +55,23 @@
     BanCommit create();
   }
 
-  private final Provider<CurrentUser> currentUser;
+  private final Provider<IdentifiedUser> currentUser;
   private final GitRepositoryManager repoManager;
-  private final AccountCache accountCache;
   private final PersonIdent gerritIdent;
 
   @Inject
-  BanCommit(final Provider<CurrentUser> currentUser,
-      final GitRepositoryManager repoManager, final AccountCache accountCache,
+  BanCommit(final Provider<IdentifiedUser> currentUser,
+      final GitRepositoryManager repoManager,
       @GerritPersonIdent final PersonIdent gerritIdent) {
     this.currentUser = currentUser;
     this.repoManager = repoManager;
-    this.accountCache = accountCache;
     this.gerritIdent = gerritIdent;
   }
 
   public BanCommitResult ban(final ProjectControl projectControl,
       final List<ObjectId> commitsToBan, final String reason)
       throws PermissionDeniedException, IOException,
-      IncompleteUserInfoException, InterruptedException, MergeException {
+      InterruptedException, MergeException {
     if (!projectControl.isOwner()) {
       throw new PermissionDeniedException(
           "No project owner: not permitted to ban commits");
@@ -148,16 +146,10 @@
     return result;
   }
 
-  private PersonIdent createPersonIdent() throws IncompleteUserInfoException {
-    final String userName = currentUser.get().getUserName();
-    final Account account = accountCache.getByUsername(userName).getAccount();
-    if (account.getFullName() == null) {
-      throw new IncompleteUserInfoException(userName, "full name");
-    }
-    if (account.getPreferredEmail() == null) {
-      throw new IncompleteUserInfoException(userName, "preferred email");
-    }
-    return new PersonIdent(account.getFullName(), account.getPreferredEmail());
+  private PersonIdent createPersonIdent() {
+    Date now = new Date();
+    TimeZone tz = gerritIdent.getTimeZone();
+    return currentUser.get().newCommitterIdent(now, tz);
   }
 
   private static ObjectId commit(final NoteMap noteMap,
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 86e0740..6d1b155 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
@@ -20,16 +20,17 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.config.GerritRequestModule;
 import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
+import com.google.inject.Provides;
 import com.google.inject.Singleton;
 import com.google.inject.servlet.RequestScoped;
 
@@ -43,6 +44,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
 
 @Singleton
@@ -57,6 +59,7 @@
 
   private final WorkQueue workQueue;
   private final Provider<MergeOp.Factory> bgFactory;
+  private final PerThreadRequestScope.Scoper threadScoper;
 
   @Inject
   ChangeMergeQueue(final WorkQueue wq, Injector parent) {
@@ -68,15 +71,9 @@
         bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
         bind(RequestScopePropagator.class)
             .to(PerThreadRequestScope.Propagator.class);
+        bind(PerThreadRequestScope.Propagator.class);
         install(new GerritRequestModule());
 
-        bind(CurrentUser.class).to(IdentifiedUser.class);
-        bind(IdentifiedUser.class).toProvider(new Provider<IdentifiedUser>() {
-          @Override
-          public IdentifiedUser get() {
-            throw new OutOfScopeException("No user on merge thread");
-          }
-        });
         bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
             new Provider<SocketAddress>() {
               @Override
@@ -91,8 +88,26 @@
           }
         });
       }
+
+      @Provides
+      public PerThreadRequestScope.Scoper provideScoper(
+          final PerThreadRequestScope.Propagator propagator) {
+        final RequestContext requestContext = new RequestContext() {
+          @Override
+          public CurrentUser getCurrentUser() {
+            throw new OutOfScopeException("No user on merge thread");
+          }
+        };
+        return new PerThreadRequestScope.Scoper() {
+          @Override
+          public <T> Callable<T> scope(Callable<T> callable) {
+            return propagator.scope(requestContext, callable);
+          }
+        };
+      }
     });
     bgFactory = child.getProvider(MergeOp.Factory.class);
+    threadScoper = child.getInstance(PerThreadRequestScope.Scoper.class);
   }
 
   @Override
@@ -186,19 +201,15 @@
     }
   }
 
-  private void mergeImpl(Branch.NameKey branch) {
+  private void mergeImpl(final Branch.NameKey branch) {
     try {
-      PerThreadRequestScope ctx = new PerThreadRequestScope();
-      PerThreadRequestScope old = PerThreadRequestScope.set(ctx);
-      try {
-        try {
+      threadScoper.scope(new Callable<Void>(){
+        @Override
+        public Void call() throws Exception {
           bgFactory.get().create(branch).merge();
-        } finally {
-          ctx.cleanup.run();
+          return null;
         }
-      } finally {
-        PerThreadRequestScope.set(old);
-      }
+      }).call();
     } catch (Throwable e) {
       log.error("Merge attempt for " + branch + " failed", e);
     } finally {
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 a2b0495..1bf157b 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
@@ -59,10 +59,11 @@
    * @return the cached Repository instance. Caller must call {@code close()}
    *         when done to decrement the resource handle.
    * @throws RepositoryNotFoundException the name does not denote an existing
-   *         repository, or the name cannot be read as a repository.
+   *         repository.
+   * @throws IOException the name cannot be read as a repository.
    */
   public abstract Repository openRepository(Project.NameKey name)
-      throws RepositoryNotFoundException;
+      throws RepositoryNotFoundException, IOException;
 
   /**
    * Create (and open) a repository by name.
@@ -73,9 +74,11 @@
    * @throws RepositoryCaseMismatchException the name collides with an existing
    *         repository name, but only in case of a character within the name.
    * @throws RepositoryNotFoundException the name is invalid.
+   * @throws IOException the repository cannot be created.
    */
   public abstract Repository createRepository(Project.NameKey name)
-      throws RepositoryCaseMismatchException, RepositoryNotFoundException;
+      throws RepositoryCaseMismatchException, 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/LocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index 9fa45e1..cfb2d57 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,7 +14,7 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.lifecycle.LifecycleListener;
+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;
@@ -23,6 +23,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import com.jcraft.jsch.Session;
+
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ConfigConstants;
@@ -34,6 +36,9 @@
 import org.eclipse.jgit.storage.file.LockFile;
 import org.eclipse.jgit.storage.file.WindowCache;
 import org.eclipse.jgit.storage.file.WindowCacheConfig;
+import org.eclipse.jgit.transport.JschConfigSessionFactory;
+import org.eclipse.jgit.transport.OpenSshConfig;
+import org.eclipse.jgit.transport.SshSessionFactory;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.RawParseUtils;
@@ -82,6 +87,15 @@
 
     @Override
     public void start() {
+      // Install our own factory which always runs in batch mode, as we
+      // have no UI available for interactive prompting.
+      SshSessionFactory.setInstance(new JschConfigSessionFactory() {
+        @Override
+        protected void configure(OpenSshConfig.Host hc, Session session) {
+          // Default configuration is batch mode.
+        }
+      });
+
       final WindowCacheConfig c = new WindowCacheConfig();
       c.fromConfig(cfg);
       WindowCache.reconfigure(c);
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 4773680..ecf98c2 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
@@ -37,6 +37,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.mail.MergeFailSender;
 import com.google.gerrit.server.mail.MergedSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -134,7 +135,7 @@
   private final SchemaFactory<ReviewDb> schemaFactory;
   private final ProjectCache projectCache;
   private final FunctionState.Factory functionState;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated replication;
   private final MergedSender.Factory mergedSenderFactory;
   private final MergeFailSender.Factory mergeFailSenderFactory;
   private final Provider<String> urlProvider;
@@ -158,6 +159,7 @@
   private CodeReviewCommit mergeTip;
   private Set<RevCommit> alreadyAccepted;
   private RefUpdate branchUpdate;
+  private ObjectInserter inserter;
 
   private final ChangeHooks hooks;
   private final AccountCache accountCache;
@@ -170,7 +172,7 @@
   @Inject
   MergeOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> sf,
       final ProjectCache pc, final FunctionState.Factory fs,
-      final ReplicationQueue rq, final MergedSender.Factory msf,
+      final GitReferenceUpdated rq, final MergedSender.Factory msf,
       final MergeFailSender.Factory mfsf,
       @CanonicalWebUrl @Nullable final Provider<String> cwu,
       final ApprovalTypes approvalTypes, final PatchSetInfoFactory psif,
@@ -281,6 +283,9 @@
     } catch (OrmException e) {
       throw new MergeException("Cannot query the database", e);
     } finally {
+      if (inserter != null) {
+        inserter.release();
+      }
       if (rw != null) {
         rw.release();
       }
@@ -319,6 +324,9 @@
     } catch (RepositoryNotFoundException notGit) {
       final String m = "Repository \"" + name.get() + "\" unknown.";
       throw new MergeException(m, notGit);
+    } catch (IOException err) {
+      final String m = "Error opening repository \"" + name.get() + '"';
+      throw new MergeException(m, err);
     }
 
     rw = new RevWalk(repo) {
@@ -330,6 +338,8 @@
     rw.sort(RevSort.TOPO);
     rw.sort(RevSort.COMMIT_TIME_DESC, true);
     CAN_MERGE = rw.newFlag("CAN_MERGE");
+
+    inserter = repo.newObjectInserter();
   }
 
   private void openBranch() throws MergeException {
@@ -501,21 +511,10 @@
   }
 
   private void mergeOneCommit(final CodeReviewCommit n) throws MergeException {
-    final ThreeWayMerger m;
-    if (destProject.isUseContentMerge()) {
-      // Settings for this project allow us to try and
-      // automatically resolve conflicts within files if needed.
-      // Use ResolveMerge and instruct to operate in core.
-      m = MergeStrategy.RESOLVE.newMerger(repo, true);
-    } else {
-      // No auto conflict resolving allowed. If any of the
-      // affected files was modified, merge will fail.
-      m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repo);
-    }
-
+    final ThreeWayMerger m = newThreeWayMerger();
     try {
       if (m.merge(new AnyObjectId[] {mergeTip, n})) {
-        writeMergeCommit(m, n);
+        writeMergeCommit(m.getResultTreeId(), n);
 
       } else {
         failed(n, CommitMergeStatus.PATH_CONFLICT);
@@ -533,6 +532,35 @@
     }
   }
 
+  private ThreeWayMerger newThreeWayMerger() {
+    ThreeWayMerger m;
+    if (destProject.isUseContentMerge()) {
+      // Settings for this project allow us to try and
+      // automatically resolve conflicts within files if needed.
+      // Use ResolveMerge and instruct to operate in core.
+      m = MergeStrategy.RESOLVE.newMerger(repo, true);
+    } else {
+      // No auto conflict resolving allowed. If any of the
+      // affected files was modified, merge will fail.
+      m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repo);
+    }
+    m.setObjectInserter(new ObjectInserter.Filter() {
+      @Override
+      protected ObjectInserter delegate() {
+        return inserter;
+      }
+
+      @Override
+      public void flush() {
+      }
+
+      @Override
+      public void release() {
+      }
+    });
+    return m;
+  }
+
   private CodeReviewCommit failed(final CodeReviewCommit n,
       final CommitMergeStatus failure) throws MissingObjectException,
       IncorrectObjectTypeException, IOException {
@@ -546,7 +574,7 @@
     return failed;
   }
 
-  private void writeMergeCommit(final Merger m, final CodeReviewCommit n)
+  private void writeMergeCommit(ObjectId treeId, CodeReviewCommit n)
       throws IOException, MissingObjectException, IncorrectObjectTypeException {
     final List<CodeReviewCommit> merged = new ArrayList<CodeReviewCommit>();
     rw.reset();
@@ -595,13 +623,13 @@
     PersonIdent authorIdent = computeAuthor(merged);
 
     final CommitBuilder mergeCommit = new CommitBuilder();
-    mergeCommit.setTreeId(m.getResultTreeId());
+    mergeCommit.setTreeId(treeId);
     mergeCommit.setParentIds(mergeTip, n);
     mergeCommit.setAuthor(authorIdent);
     mergeCommit.setCommitter(myIdent);
     mergeCommit.setMessage(msgbuf.toString());
 
-    mergeTip = (CodeReviewCommit) rw.parseCommit(commit(m, mergeCommit));
+    mergeTip = (CodeReviewCommit) rw.parseCommit(commit(mergeCommit));
   }
 
   private PersonIdent computeAuthor(
@@ -687,19 +715,7 @@
   private void cherryPickChanges() throws MergeException, OrmException {
     while (!toMerge.isEmpty()) {
       final CodeReviewCommit n = toMerge.remove(0);
-      final ThreeWayMerger m;
-
-      if (destProject.isUseContentMerge()) {
-        // Settings for this project allow us to try and
-        // automatically resolve conflicts within files if needed.
-        // Use ResolveMerge and instruct to operate in core.
-        m = MergeStrategy.RESOLVE.newMerger(repo, true);
-      } else {
-        // No auto conflict resolving allowed. If any of the
-        // affected files was modified, merge will fail.
-        m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repo);
-      }
-
+      final ThreeWayMerger m = newThreeWayMerger();
       try {
         if (mergeTip == null) {
           // The branch is unborn. Take a fast-forward resolution to
@@ -894,8 +910,9 @@
     mergeCommit.setCommitter(toCommitterIdent(submitAudit));
     mergeCommit.setMessage(msgbuf.toString());
 
-    final ObjectId id = commit(m, mergeCommit);
+    final ObjectId id = commit(mergeCommit);
     final CodeReviewCommit newCommit = (CodeReviewCommit) rw.parseCommit(id);
+    final Change oldChange = n.change;
 
     n.change =
         db.changes().atomicUpdate(n.change.getId(),
@@ -925,6 +942,9 @@
               }
             });
 
+    this.submitted.remove(oldChange);
+    this.submitted.add(n.change);
+
     if (approvalList != null) {
       for (PatchSetApproval a : approvalList) {
         db.patchSetApprovals().insert(
@@ -953,16 +973,11 @@
     db.patchSetAncestors().insert(toInsert);
   }
 
-  private ObjectId commit(final Merger m, final CommitBuilder mergeCommit)
+  private ObjectId commit(CommitBuilder mergeCommit)
       throws IOException, UnsupportedEncodingException {
-    ObjectInserter oi = m.getObjectInserter();
-    try {
-      ObjectId id = oi.insert(mergeCommit);
-      oi.flush();
-      return id;
-    } finally {
-      oi.release();
-    }
+    ObjectId id = inserter.insert(mergeCommit);
+    inserter.flush();
+    return id;
   }
 
   private boolean contains(List<FooterLine> footers, FooterKey key, String val) {
@@ -1026,8 +1041,7 @@
                   ps.getProject().getDescription());
             }
 
-            replication.scheduleUpdate(destBranch.getParentKey(), branchUpdate
-                .getName());
+            replication.fire(destBranch.getParentKey(), branchUpdate.getName());
 
             Account account = null;
             final PatchSetApproval submitter = getSubmitter(db, mergeTip.patchsetId);
@@ -1122,7 +1136,7 @@
     } catch (CodeReviewNoteCreationException e) {
       log.error(e.getMessage());
     }
-    replication.scheduleUpdate(destBranch.getParentKey(),
+    replication.fire(destBranch.getParentKey(),
         GitRepositoryManager.REFS_NOTES_REVIEW);
   }
 
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 fbe9d16..86dc19c 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
@@ -17,6 +17,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -25,6 +26,8 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 
+import java.io.IOException;
+
 /** Helps with the updating of a {@link VersionedMetaData}. */
 public class MetaDataUpdate {
   public static class User {
@@ -49,7 +52,7 @@
     }
 
     public MetaDataUpdate create(Project.NameKey name)
-        throws RepositoryNotFoundException {
+        throws RepositoryNotFoundException, IOException {
       MetaDataUpdate md = factory.create(name, mgr.openRepository(name));
       md.getCommitBuilder().setAuthor(userIdent);
       md.getCommitBuilder().setCommitter(serverIdent);
@@ -71,7 +74,7 @@
     }
 
     public MetaDataUpdate create(Project.NameKey name)
-        throws RepositoryNotFoundException {
+        throws RepositoryNotFoundException, IOException {
       MetaDataUpdate md = factory.create(name, mgr.openRepository(name));
       md.getCommitBuilder().setAuthor(serverIdent);
       md.getCommitBuilder().setCommitter(serverIdent);
@@ -84,13 +87,13 @@
         @Assisted Repository db);
   }
 
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated replication;
   private final Project.NameKey projectName;
   private final Repository db;
   private final CommitBuilder commit;
 
   @Inject
-  public MetaDataUpdate(ReplicationQueue replication,
+  public MetaDataUpdate(GitReferenceUpdated replication,
       @Assisted Project.NameKey projectName, @Assisted Repository db) {
     this.replication = replication;
     this.projectName = projectName;
@@ -121,8 +124,6 @@
   }
 
   void replicate(String ref) {
-    if (replication.isEnabled()) {
-      replication.scheduleUpdate(projectName, ref);
-    }
+    replication.fire(projectName, ref);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NoReplication.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NoReplication.java
deleted file mode 100644
index c5ee262..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/NoReplication.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import com.google.gerrit.reviewdb.client.Project;
-
-/** A disabled {@link ReplicationQueue}. */
-public final class NoReplication implements ReplicationQueue {
-  @Override
-  public boolean isEnabled() {
-    return false;
-  }
-
-  @Override
-  public void scheduleUpdate(Project.NameKey project, String ref) {
-  }
-
-  @Override
-  public void scheduleFullSync(Project.NameKey project, String urlMatch) {
-  }
-
-  @Override
-  public void replicateNewProject(Project.NameKey project, String head) {
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java
new file mode 100644
index 0000000..ba2833d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java
@@ -0,0 +1,104 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.server.mail.Address;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+public class NotifyConfig implements Comparable<NotifyConfig> {
+  private String name;
+  private EnumSet<NotifyType> types = EnumSet.of(NotifyType.ALL);
+  private String filter;
+
+  private Set<GroupReference> groups = Sets.newHashSet();
+  private Set<Address> addresses = Sets.newHashSet();
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public boolean isNotify(NotifyType type) {
+    return types.contains(type) || types.contains(NotifyType.ALL);
+  }
+
+  public EnumSet<NotifyType> getNotify() {
+    return types;
+  }
+
+  public void setTypes(EnumSet<NotifyType> newTypes) {
+    types = EnumSet.copyOf(newTypes);
+  }
+
+  public String getFilter() {
+    return filter;
+  }
+
+  public void setFilter(String filter) {
+    if ("*".equals(filter)) {
+      this.filter = null;
+    } else {
+      this.filter = Strings.emptyToNull(filter);
+    }
+  }
+
+  public Set<GroupReference> getGroups() {
+    return groups;
+  }
+
+  public Set<Address> getAddresses() {
+    return addresses;
+  }
+
+  public void addEmail(GroupReference group) {
+    groups.add(group);
+  }
+
+  public void addEmail(Address address) {
+    addresses.add(address);
+  }
+
+  @Override
+  public int compareTo(NotifyConfig o) {
+    return name.compareTo(o.name);
+  }
+
+  @Override
+  public int hashCode() {
+    return name.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof NotifyConfig) {
+      return compareTo((NotifyConfig) obj) == 0;
+    }
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return "NotifyConfig[" + name + " = " + addresses + " + " + groups + "]";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
index 057e80d..8aea73a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
@@ -14,50 +14,86 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.server.RequestCleanup;
+import com.google.common.collect.Maps;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
+import com.google.inject.Inject;
 import com.google.inject.Key;
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
 import com.google.inject.Scope;
 
-import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.Callable;
 
-class PerThreadRequestScope {
-  static class Propagator
-      extends ThreadLocalRequestScopePropagator<PerThreadRequestScope> {
-    Propagator() {
-      super(REQUEST, current);
+public class PerThreadRequestScope {
+  public interface Scoper {
+    <T> Callable<T> scope(Callable<T> callable);
+  }
+
+  private static class Context {
+    private final Map<Key<?>, Object> map;
+
+    private Context() {
+      map = Maps.newHashMap();
     }
 
-    @Override
-    protected PerThreadRequestScope continuingContext(
-        PerThreadRequestScope ctx) {
-      return new PerThreadRequestScope();
+    private <T> T get(Key<T> key, Provider<T> creator) {
+      @SuppressWarnings("unchecked")
+      T t = (T) map.get(key);
+      if (t == null) {
+        t = creator.get();
+        map.put(key, t);
+      }
+      return t;
     }
   }
 
-  private static final ThreadLocal<PerThreadRequestScope> current =
-      new ThreadLocal<PerThreadRequestScope>();
+  public static class Propagator extends ThreadLocalRequestScopePropagator<Context> {
+    @Inject
+    Propagator(ThreadLocalRequestContext local) {
+      super(REQUEST, current, local);
+    }
 
-  private static PerThreadRequestScope requireContext() {
-    final PerThreadRequestScope ctx = current.get();
+    @Override
+    protected Context continuingContext(Context ctx) {
+      return new Context();
+    }
+
+    public <T> Callable<T> scope(RequestContext requestContext, Callable<T> callable) {
+      final Context ctx = new Context();
+      final Callable<T> wrapped = context(requestContext, cleanup(callable));
+      return new Callable<T>() {
+        @Override
+        public T call() throws Exception {
+          Context old = current.get();
+          current.set(ctx);
+          try {
+            return wrapped.call();
+          } finally {
+            current.set(old);
+          }
+        }
+      };
+    }
+  }
+
+  private static final ThreadLocal<Context> current = new ThreadLocal<Context>();
+
+  private static Context requireContext() {
+    final Context ctx = current.get();
     if (ctx == null) {
       throw new OutOfScopeException("Not in command/request");
     }
     return ctx;
   }
 
-  static PerThreadRequestScope set(PerThreadRequestScope ctx) {
-    PerThreadRequestScope old = current.get();
-    current.set(ctx);
-    return old;
-  }
-
-  static final Scope REQUEST = new Scope() {
+  public static final Scope REQUEST = new Scope() {
+    @Override
     public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) {
       return new Provider<T>() {
+        @Override
         public T get() {
           return requireContext().get(key, creator);
         }
@@ -74,26 +110,4 @@
       return "PerThreadRequestScope.REQUEST";
     }
   };
-
-  private static final Key<RequestCleanup> RC_KEY =
-      Key.get(RequestCleanup.class);
-
-  final RequestCleanup cleanup;
-  private final Map<Key<?>, Object> map;
-
-  PerThreadRequestScope() {
-    cleanup = new RequestCleanup();
-    map = new HashMap<Key<?>, Object>();
-    map.put(RC_KEY, cleanup);
-  }
-
-  synchronized <T> T get(Key<T> key, Provider<T> creator) {
-    @SuppressWarnings("unchecked")
-    T t = (T) map.get(key);
-    if (t == null) {
-      t = creator.get();
-      map.put(key, t);
-    }
-    return t;
-  }
 }
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 0fe00ea..13e9967 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
@@ -16,24 +16,31 @@
 
 import static com.google.gerrit.common.data.Permission.isPermission;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.common.data.RefConfigSection;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.Project.State;
 import com.google.gerrit.reviewdb.client.Project.SubmitType;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.mail.Address;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.util.StringUtils;
 
 import java.io.BufferedReader;
 import java.io.IOException;
@@ -41,6 +48,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -67,6 +75,11 @@
   private static final String KEY_AUTO_VERIFY = "autoVerify";
   private static final String KEY_AGREEMENT_URL = "agreementUrl";
 
+  private static final String NOTIFY = "notify";
+  private static final String KEY_EMAIL = "email";
+  private static final String KEY_FILTER = "filter";
+  private static final String KEY_TYPE = "type";
+
   private static final String CAPABILITY = "capability";
 
   private static final String RECEIVE = "receive";
@@ -91,6 +104,7 @@
   private Map<AccountGroup.UUID, GroupReference> groupsByUUID;
   private Map<String, AccessSection> accessSections;
   private Map<String, ContributorAgreement> contributorAgreements;
+  private Map<String, NotifyConfig> notifySections;
   private List<ValidationError> validationErrors;
   private ObjectId rulesId;
 
@@ -185,6 +199,10 @@
     contributorAgreements.put(section.getName(), section);
   }
 
+  public Collection<NotifyConfig> getNotifyConfigs() {
+    return notifySections.values();
+  }
+
   public GroupReference resolve(AccountGroup group) {
     return resolve(GroupReference.forGroup(group));
   }
@@ -216,13 +234,13 @@
   /**
    * Check all GroupReferences use current group name, repairing stale ones.
    *
-   * @param groupCache cache to use when looking up group information by UUID.
+   * @param groupBackend cache to use when looking up group information by UUID.
    * @return true if one or more group names was stale.
    */
-  public boolean updateGroupNames(GroupCache groupCache) {
+  public boolean updateGroupNames(GroupBackend groupBackend) {
     boolean dirty = false;
     for (GroupReference ref : groupsByUUID.values()) {
-      AccountGroup g = groupCache.get(ref.getUUID());
+      GroupDescription.Basic g = groupBackend.get(ref.getUUID());
       if (g != null && !g.getName().equals(ref.getName())) {
         dirty = true;
         ref.setName(g.getName());
@@ -275,6 +293,7 @@
     loadAccountsSection(rc, groupsByName);
     loadContributorAgreements(rc, groupsByName);
     loadAccessSections(rc, groupsByName);
+    loadNotifySections(rc, groupsByName);
   }
 
   private void loadAccountsSection(
@@ -318,6 +337,67 @@
     }
   }
 
+  /**
+   * Parses the [notify] sections out of the configuration file.
+   *
+   * <pre>
+   *   [notify "reviewers"]
+   *     email = group Reviewers
+   *     type = new_changes
+   *
+   *   [notify "dev-team"]
+   *     email = dev-team@example.com
+   *     filter = branch:master
+   *
+   *   [notify "qa"]
+   *     email = qa@example.com
+   *     filter = branch:\"^(maint|stable)-.*\"
+   *     type = submitted_changes
+   * </pre>
+   */
+  private void loadNotifySections(
+      Config rc, Map<String, GroupReference> groupsByName) {
+    notifySections = Maps.newHashMap();
+    for (String sectionName : rc.getSubsections(NOTIFY)) {
+      NotifyConfig n = new NotifyConfig();
+      n.setName(sectionName);
+      n.setFilter(rc.getString(NOTIFY, sectionName, KEY_FILTER));
+
+      EnumSet<NotifyType> types = EnumSet.noneOf(NotifyType.class);
+      types.addAll(ConfigUtil.getEnumList(rc,
+          NOTIFY, sectionName, KEY_TYPE,
+          NotifyType.ALL));
+      n.setTypes(types);
+
+      for (String dst : rc.getStringList(NOTIFY, sectionName, KEY_EMAIL)) {
+        if (dst.startsWith("group ")) {
+          String groupName = dst.substring(6).trim();
+          GroupReference ref = groupsByName.get(groupName);
+          if (ref == null) {
+            ref = new GroupReference(null, groupName);
+            groupsByName.put(ref.getName(), ref);
+          }
+          if (ref.getUUID() != null) {
+            n.addEmail(ref);
+          } else {
+            error(new ValidationError(PROJECT_CONFIG,
+                "group \"" + ref.getName() + "\" not in " + GROUP_LIST));
+          }
+        } else if (dst.startsWith("user ")) {
+          error(new ValidationError(PROJECT_CONFIG, dst + " not supported"));
+        } else {
+          try {
+            n.addEmail(Address.parse(dst));
+          } catch (IllegalArgumentException err) {
+            error(new ValidationError(PROJECT_CONFIG,
+                "notify section \"" + sectionName + "\" has invalid email \"" + dst + "\""));
+          }
+        }
+      }
+      notifySections.put(sectionName, n);
+    }
+  }
+
   private void loadAccessSections(
       Config rc, Map<String, GroupReference> groupsByName) {
     accessSections = new HashMap<String, AccessSection>();
@@ -458,6 +538,7 @@
     saveAccountsSection(rc, keepGroups);
     saveContributorAgreements(rc, keepGroups);
     saveAccessSections(rc, keepGroups);
+    saveNotifySections(rc, keepGroups);
     groupsByUUID.keySet().retainAll(keepGroups);
 
     saveConfig(PROJECT_CONFIG, rc);
@@ -493,6 +574,47 @@
     }
   }
 
+  private void saveNotifySections(
+      Config rc, Set<AccountGroup.UUID> keepGroups) {
+    for (NotifyConfig nc : sort(notifySections.values())) {
+      List<String> email = Lists.newArrayList();
+      for (GroupReference gr : nc.getGroups()) {
+        if (gr.getUUID() != null) {
+          keepGroups.add(gr.getUUID());
+        }
+        email.add(new PermissionRule(gr).asString(false));
+      }
+      Collections.sort(email);
+
+      List<String> addrs = Lists.newArrayList();
+      for (Address addr : nc.getAddresses()) {
+        addrs.add(addr.toString());
+      }
+      Collections.sort(addrs);
+      email.addAll(addrs);
+
+      if (email.isEmpty()) {
+        rc.unset(NOTIFY, nc.getName(), KEY_EMAIL);
+      } else {
+        rc.setStringList(NOTIFY, nc.getName(), KEY_EMAIL, email);
+      }
+
+      if (nc.getNotify().equals(EnumSet.of(NotifyType.ALL))) {
+        rc.unset(NOTIFY, nc.getName(), KEY_TYPE);
+      } else {
+        List<String> types = Lists.newArrayListWithCapacity(4);
+        for (NotifyType t : NotifyType.values()) {
+          if (nc.isNotify(t)) {
+            types.add(StringUtils.toLowerCase(t.name()));
+          }
+        }
+        rc.setStringList(NOTIFY, nc.getName(), KEY_TYPE, types);
+      }
+
+      set(rc, NOTIFY, nc.getName(), KEY_FILTER, nc.getFilter());
+    }
+  }
+
   private List<String> ruleToStringList(
       List<PermissionRule> list, Set<AccountGroup.UUID> keepGroups) {
     List<String> rules = new ArrayList<String>();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java
deleted file mode 100644
index 845d037..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.concurrent.TimeUnit;
-
-import javax.annotation.Nullable;
-
-public class PushAllProjectsOp extends DefaultQueueOp {
-  public interface Factory {
-    PushAllProjectsOp create(String urlMatch);
-  }
-
-  private static final Logger log =
-      LoggerFactory.getLogger(PushAllProjectsOp.class);
-
-  private final ProjectCache projectCache;
-  private final ReplicationQueue replication;
-  private final String urlMatch;
-
-  @Inject
-  public PushAllProjectsOp(final WorkQueue wq, final ProjectCache projectCache,
-      final ReplicationQueue rq, @Assisted @Nullable final String urlMatch) {
-    super(wq);
-    this.projectCache = projectCache;
-    this.replication = rq;
-    this.urlMatch = urlMatch;
-  }
-
-  @Override
-  public void start(final int delay, final TimeUnit unit) {
-    if (replication.isEnabled()) {
-      super.start(delay, unit);
-    }
-  }
-
-  public void run() {
-    try {
-      for (final Project.NameKey nameKey : projectCache.all()) {
-        replication.scheduleFullSync(nameKey, urlMatch);
-      }
-    } catch (RuntimeException e) {
-      log.error("Cannot enumerate known projects", e);
-    }
-  }
-
-  @Override
-  public String toString() {
-    String s = "Replicate All Projects";
-    if (urlMatch != null) {
-      s = s + " to " + urlMatch;
-    }
-    return s;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
deleted file mode 100644
index 0868749..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
+++ /dev/null
@@ -1,433 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-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.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import com.jcraft.jsch.JSchException;
-
-import org.eclipse.jgit.errors.NoRemoteRepositoryException;
-import org.eclipse.jgit.errors.NotSupportedException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.errors.TransportException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.CredentialsProvider;
-import org.eclipse.jgit.transport.FetchConnection;
-import org.eclipse.jgit.transport.PushResult;
-import org.eclipse.jgit.transport.RefSpec;
-import org.eclipse.jgit.transport.RemoteConfig;
-import org.eclipse.jgit.transport.RemoteRefUpdate;
-import org.eclipse.jgit.transport.Transport;
-import org.eclipse.jgit.transport.URIish;
-import org.slf4j.Logger;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * A push to remote operation started by {@link ReplicationQueue}.
- * <p>
- * Instance members are protected by the lock within PushQueue. Callers must
- * take that lock to ensure they are working with a current view of the object.
- */
-class PushOp implements ProjectRunnable {
-  interface Factory {
-    PushOp create(Project.NameKey d, URIish u);
-  }
-
-  private static final Logger log = PushReplication.log;
-  static final String ALL_REFS = "..all..";
-
-  private final GitRepositoryManager repoManager;
-  private final SchemaFactory<ReviewDb> schema;
-  private final PushReplication.ReplicationConfig pool;
-  private final RemoteConfig config;
-  private final CredentialsProvider credentialsProvider;
-  private final TagCache tagCache;
-
-  private final Set<String> delta = new HashSet<String>();
-  private final Project.NameKey projectName;
-  private final URIish uri;
-  private boolean pushAllRefs;
-
-  private Repository db;
-
-  /**
-   * It indicates if the current instance is in fact retrying to push.
-   */
-  private boolean retrying;
-
-  private boolean canceled;
-
-  @Inject
-  PushOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> s,
-      final PushReplication.ReplicationConfig p, final RemoteConfig c,
-      final SecureCredentialsProvider.Factory cpFactory,
-      final TagCache tc,
-      @Assisted final Project.NameKey d, @Assisted final URIish u) {
-    repoManager = grm;
-    schema = s;
-    pool = p;
-    config = c;
-    credentialsProvider = cpFactory.create(c.getName());
-    tagCache = tc;
-    projectName = d;
-    uri = u;
-  }
-
-  public boolean isRetrying() {
-    return retrying;
-  }
-
-  public void setToRetry() {
-    retrying = true;
-  }
-
-  public void cancel() {
-    canceled = true;
-  }
-
-  public boolean wasCanceled() {
-    return canceled;
-  }
-
-  URIish getURI() {
-    return uri;
-  }
-
-  void addRef(final String ref) {
-    if (ALL_REFS.equals(ref)) {
-      delta.clear();
-      pushAllRefs = true;
-    } else if (!pushAllRefs) {
-      delta.add(ref);
-    }
-  }
-
-  public Set<String> getRefs() {
-    final Set<String> refs;
-
-    if (pushAllRefs) {
-      refs = new HashSet<String>(1);
-      refs.add(ALL_REFS);
-    } else {
-      refs = delta;
-    }
-
-    return refs;
-  }
-
-  public void addRefs(Set<String> refs) {
-    if (!pushAllRefs) {
-      for (String ref : refs) {
-        addRef(ref);
-      }
-    }
-  }
-
-  public void run() {
-    PerThreadRequestScope ctx = new PerThreadRequestScope();
-    PerThreadRequestScope old = PerThreadRequestScope.set(ctx);
-    try {
-      runPushOperation();
-    } finally {
-      PerThreadRequestScope.set(old);
-    }
-  }
-
-  private void runPushOperation() {
-    // Lock the queue, and remove ourselves, so we can't be modified once
-    // we start replication (instead a new instance, with the same URI, is
-    // created and scheduled for a future point in time.)
-    //
-    pool.notifyStarting(this);
-
-    // It should only verify if it was canceled after calling notifyStarting,
-    // since the canceled flag would be set locking the queue.
-    if (!canceled) {
-      try {
-        db = repoManager.openRepository(projectName);
-        runImpl();
-      } catch (RepositoryNotFoundException e) {
-        log.error("Cannot replicate " + projectName + "; " + e.getMessage());
-
-      } catch (NoRemoteRepositoryException e) {
-        log.error("Cannot replicate to " + uri + "; repository not found");
-
-      } catch (NotSupportedException e) {
-        log.error("Cannot replicate to " + uri, e);
-
-      } catch (TransportException e) {
-        final Throwable cause = e.getCause();
-        if (cause instanceof JSchException
-            && cause.getMessage().startsWith("UnknownHostKey:")) {
-          log.error("Cannot replicate to " + uri + ": " + cause.getMessage());
-        } else {
-          log.error("Cannot replicate to " + uri, e);
-        }
-
-        // The remote push operation should be retried.
-        pool.reschedule(this);
-      } catch (IOException e) {
-        log.error("Cannot replicate to " + uri, e);
-
-      } catch (RuntimeException e) {
-        log.error("Unexpected error during replication to " + uri, e);
-
-      } catch (Error e) {
-        log.error("Unexpected error during replication to " + uri, e);
-
-      } finally {
-        if (db != null) {
-          db.close();
-        }
-      }
-    }
-  }
-
-  @Override
-  public String toString() {
-    return "push " + uri;
-  }
-
-  private void runImpl() throws IOException {
-    final Transport tn = Transport.open(db, uri);
-    final PushResult res;
-    try {
-      res = pushVia(tn);
-    } finally {
-      try {
-        tn.close();
-      } catch (Throwable e2) {
-        log.warn("Unexpected error while closing " + uri, e2);
-      }
-    }
-
-    for (final RemoteRefUpdate u : res.getRemoteUpdates()) {
-      switch (u.getStatus()) {
-        case OK:
-        case UP_TO_DATE:
-        case NON_EXISTING:
-          break;
-
-        case NOT_ATTEMPTED:
-        case AWAITING_REPORT:
-        case REJECTED_NODELETE:
-        case REJECTED_NONFASTFORWARD:
-        case REJECTED_REMOTE_CHANGED:
-          log.error("Failed replicate of " + u.getRemoteName() + " to " + uri
-              + ": status " + u.getStatus().name());
-          break;
-
-        case REJECTED_OTHER_REASON:
-          if ("non-fast-forward".equals(u.getMessage())) {
-            log.error("Failed replicate of " + u.getRemoteName() + " to " + uri
-                + ", remote rejected non-fast-forward push."
-                + "  Check receive.denyNonFastForwards variable in config file"
-                + " of destination repository.");
-          } else {
-            log.error("Failed replicate of " + u.getRemoteName() + " to " + uri
-                + ", reason: " + u.getMessage());
-          }
-          break;
-      }
-    }
-  }
-
-  private PushResult pushVia(final Transport tn) throws IOException,
-      NotSupportedException, TransportException {
-    tn.applyConfig(config);
-    tn.setCredentialsProvider(credentialsProvider);
-
-    final List<RemoteRefUpdate> todo = generateUpdates(tn);
-    if (todo.isEmpty()) {
-      // If we have no commands selected, we have nothing to do.
-      // Calling JGit at this point would just redo the work we
-      // already did, and come up with the same answer. Instead
-      // send back an empty result.
-      //
-      return new PushResult();
-    }
-
-    return tn.push(NullProgressMonitor.INSTANCE, todo);
-  }
-
-  private List<RemoteRefUpdate> generateUpdates(final Transport tn)
-      throws IOException {
-    final ProjectControl pc;
-    try {
-      pc = pool.controlFor(projectName);
-    } catch (NoSuchProjectException e) {
-      return Collections.emptyList();
-    }
-
-    Map<String, Ref> local = db.getAllRefs();
-    if (!pc.allRefsAreVisible()) {
-      if (!pushAllRefs) {
-        // If we aren't mirroring, reduce the space we need to filter
-        // to only the references we will update during this operation.
-        //
-        Map<String, Ref> n = new HashMap<String, Ref>();
-        for (String src : delta) {
-          Ref r = local.get(src);
-          if (r != null) {
-            n.put(src, r);
-          }
-        }
-        local = n;
-      }
-
-      final ReviewDb meta;
-      try {
-        meta = schema.open();
-      } catch (OrmException e) {
-        log.error("Cannot read database to replicate to " + projectName, e);
-        return Collections.emptyList();
-      }
-      try {
-        local = new VisibleRefFilter(tagCache, db, pc, meta, true).filter(local, true);
-      } finally {
-        meta.close();
-      }
-    }
-
-    final boolean noPerms = !pool.isReplicatePermissions();
-    final List<RemoteRefUpdate> cmds = new ArrayList<RemoteRefUpdate>();
-    if (pushAllRefs) {
-      final Map<String, Ref> remote = listRemote(tn);
-
-      for (final Ref src : local.values()) {
-        if (noPerms && GitRepositoryManager.REF_CONFIG.equals(src.getName())) {
-          continue;
-        }
-
-        final RefSpec spec = matchSrc(src.getName());
-        if (spec != null) {
-          final Ref dst = remote.get(spec.getDestination());
-          if (dst == null || !src.getObjectId().equals(dst.getObjectId())) {
-            // Doesn't exist yet, or isn't the same value, request to push.
-            //
-            send(cmds, spec, src);
-          }
-        }
-      }
-
-      if (config.isMirror()) {
-        for (final Ref ref : remote.values()) {
-          if (!Constants.HEAD.equals(ref.getName())) {
-            final RefSpec spec = matchDst(ref.getName());
-            if (spec != null && !local.containsKey(spec.getSource())) {
-              // No longer on local side, request removal.
-              //
-              delete(cmds, spec);
-            }
-          }
-        }
-      }
-
-    } else {
-      for (final String src : delta) {
-        final RefSpec spec = matchSrc(src);
-        if (spec != null) {
-          // If the ref still exists locally, send it, otherwise delete it.
-          //
-          Ref srcRef = local.get(src);
-          if (srcRef != null &&
-              !(noPerms && GitRepositoryManager.REF_CONFIG.equals(src))) {
-            send(cmds, spec, srcRef);
-          } else if (config.isMirror()) {
-            delete(cmds, spec);
-          }
-        }
-      }
-    }
-
-    return cmds;
-  }
-
-  private Map<String, Ref> listRemote(final Transport tn)
-      throws NotSupportedException, TransportException {
-    final FetchConnection fc = tn.openFetch();
-    try {
-      return fc.getRefsMap();
-    } finally {
-      fc.close();
-    }
-  }
-
-  private RefSpec matchSrc(final String ref) {
-    for (final RefSpec s : config.getPushRefSpecs()) {
-      if (s.matchSource(ref)) {
-        return s.expandFromSource(ref);
-      }
-    }
-    return null;
-  }
-
-  private RefSpec matchDst(final String ref) {
-    for (final RefSpec s : config.getPushRefSpecs()) {
-      if (s.matchDestination(ref)) {
-        return s.expandFromDestination(ref);
-      }
-    }
-    return null;
-  }
-
-  private void send(final List<RemoteRefUpdate> cmds, final RefSpec spec,
-      final Ref src) throws IOException {
-    final String dst = spec.getDestination();
-    final boolean force = spec.isForceUpdate();
-    cmds.add(new RemoteRefUpdate(db, src, dst, force, null, null));
-  }
-
-  private void delete(final List<RemoteRefUpdate> cmds, final RefSpec spec)
-      throws IOException {
-    final String dst = spec.getDestination();
-    final boolean force = spec.isForceUpdate();
-    cmds.add(new RemoteRefUpdate(db, (Ref) null, dst, force, null, null));
-  }
-
-  @Override
-  public NameKey getProjectNameKey() {
-    return projectName;
-  }
-
-  @Override
-  public String getRemoteName() {
-    return config.getName();
-  }
-
-  @Override
-  public boolean hasCustomizedPrint() {
-    return true;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
deleted file mode 100644
index 5bff0ad..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
+++ /dev/null
@@ -1,680 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import com.google.common.collect.ImmutableSet;
-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.CurrentUser;
-import com.google.gerrit.server.ReplicationUser;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.account.ListGroupMembership;
-import com.google.gerrit.server.config.FactoryModule;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.PerRequestProjectControlCache;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.AbstractModule;
-import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.inject.Singleton;
-import com.google.inject.servlet.RequestScoped;
-
-import com.jcraft.jsch.Session;
-
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.storage.file.FileRepository;
-import org.eclipse.jgit.transport.JschConfigSessionFactory;
-import org.eclipse.jgit.transport.OpenSshConfig;
-import org.eclipse.jgit.transport.RefSpec;
-import org.eclipse.jgit.transport.RemoteConfig;
-import org.eclipse.jgit.transport.RemoteSession;
-import org.eclipse.jgit.transport.SshSessionFactory;
-import org.eclipse.jgit.transport.URIish;
-import org.eclipse.jgit.util.FS;
-import org.eclipse.jgit.util.QuotedString;
-import org.eclipse.jgit.util.io.StreamCopyThread;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
-import java.net.URISyntaxException;
-import java.net.URLEncoder;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
-
-/** Manages automatic replication to remote repositories. */
-@Singleton
-public class PushReplication implements ReplicationQueue {
-  static final Logger log = LoggerFactory.getLogger(PushReplication.class);
-
-  public static class Module extends AbstractModule {
-    @Override
-    protected void configure() {
-      bind(ReplicationQueue.class).to(PushReplication.class);
-    }
-  }
-
-  static {
-    // Install our own factory which always runs in batch mode, as we
-    // have no UI available for interactive prompting.
-    //
-    SshSessionFactory.setInstance(new JschConfigSessionFactory() {
-      @Override
-      protected void configure(OpenSshConfig.Host hc, Session session) {
-        // Default configuration is batch mode.
-      }
-    });
-  }
-
-  private final Injector injector;
-  private final WorkQueue workQueue;
-  private final List<ReplicationConfig> configs;
-  private final SchemaFactory<ReviewDb> database;
-  private final ReplicationUser.Factory replicationUserFactory;
-  private final GitRepositoryManager gitRepositoryManager;
-  private final GroupCache groupCache;
-
-  @Inject
-  PushReplication(final Injector i, final WorkQueue wq, final SitePaths site,
-      final ReplicationUser.Factory ruf, final SchemaFactory<ReviewDb> db,
-      final GitRepositoryManager grm, GroupCache gc)
-      throws ConfigInvalidException, IOException {
-    injector = i;
-    workQueue = wq;
-    database = db;
-    replicationUserFactory = ruf;
-    gitRepositoryManager = grm;
-    configs = allConfigs(site);
-    groupCache = gc;
-  }
-
-  @Override
-  public boolean isEnabled() {
-    return configs.size() > 0;
-  }
-
-  @Override
-  public void scheduleFullSync(final Project.NameKey project,
-      final String urlMatch) {
-    for (final ReplicationConfig cfg : configs) {
-      for (final URIish uri : cfg.getURIs(project, urlMatch)) {
-        cfg.schedule(project, PushOp.ALL_REFS, uri);
-      }
-    }
-  }
-
-  @Override
-  public void scheduleUpdate(final Project.NameKey project, final String ref) {
-    for (final ReplicationConfig cfg : configs) {
-      if (cfg.wouldPushRef(ref)) {
-        for (final URIish uri : cfg.getURIs(project, null)) {
-          cfg.schedule(project, ref, uri);
-        }
-      }
-    }
-  }
-
-  private static String replace(final String pat, final String key,
-      final String val) {
-    final int n = pat.indexOf("${" + key + "}");
-
-    if (n != -1) {
-      return pat.substring(0, n) + val + pat.substring(n + 3 + key.length());
-    } else {
-      return null;
-    }
-  }
-
-  private List<ReplicationConfig> allConfigs(final SitePaths site)
-      throws ConfigInvalidException, IOException {
-    final FileBasedConfig cfg =
-        new FileBasedConfig(site.replication_config, FS.DETECTED);
-
-    if (!cfg.getFile().exists()) {
-      log.warn("No " + cfg.getFile() + "; not replicating");
-      return Collections.emptyList();
-    }
-    if (cfg.getFile().length() == 0) {
-      log.info("Empty " + cfg.getFile() + "; not replicating");
-      return Collections.emptyList();
-    }
-
-    try {
-      cfg.load();
-    } catch (ConfigInvalidException e) {
-      throw new ConfigInvalidException("Config file " + cfg.getFile()
-          + " is invalid: " + e.getMessage(), e);
-    } catch (IOException e) {
-      throw new IOException("Cannot read " + cfg.getFile() + ": "
-          + e.getMessage(), e);
-    }
-
-    final List<ReplicationConfig> r = new ArrayList<ReplicationConfig>();
-    for (final RemoteConfig c : allRemotes(cfg)) {
-      if (c.getURIs().isEmpty()) {
-        continue;
-      }
-
-      for (final URIish u : c.getURIs()) {
-        if (u.getPath() == null || !u.getPath().contains("${name}")) {
-          throw new ConfigInvalidException("remote." + c.getName() + ".url"
-              + " \"" + u + "\" lacks ${name} placeholder in " + cfg.getFile());
-        }
-      }
-
-      // In case if refspec destination for push is not set then we assume it is
-      // equal to source
-      for (RefSpec ref : c.getPushRefSpecs()) {
-        if (ref.getDestination() == null) {
-          ref.setDestination(ref.getSource());
-        }
-      }
-
-      if (c.getPushRefSpecs().isEmpty()) {
-        RefSpec spec = new RefSpec();
-        spec = spec.setSourceDestination("refs/*", "refs/*");
-        spec = spec.setForceUpdate(true);
-        c.addPushRefSpec(spec);
-      }
-
-      r.add(new ReplicationConfig(injector, workQueue, c, cfg, database,
-          replicationUserFactory, gitRepositoryManager, groupCache));
-    }
-    return Collections.unmodifiableList(r);
-  }
-
-  private List<RemoteConfig> allRemotes(final FileBasedConfig cfg)
-      throws ConfigInvalidException {
-    List<String> names = new ArrayList<String>(cfg.getSubsections("remote"));
-    Collections.sort(names);
-
-    final List<RemoteConfig> result = new ArrayList<RemoteConfig>(names.size());
-    for (final String name : names) {
-      try {
-        result.add(new RemoteConfig(cfg, name));
-      } catch (URISyntaxException e) {
-        throw new ConfigInvalidException("remote " + name
-            + " has invalid URL in " + cfg.getFile());
-      }
-    }
-    return result;
-  }
-
-  @Override
-  public void replicateNewProject(Project.NameKey projectName, String head) {
-    if (!isEnabled()) {
-      return;
-    }
-
-    for (ReplicationConfig config : configs) {
-      List<URIish> uriList = config.getURIs(projectName, "*");
-      String[] adminUrls = config.getAdminUrls();
-      boolean adminURLUsed = false;
-
-      for (String url : adminUrls) {
-        URIish adminURI = null;
-        try {
-          if (url != null && !url.isEmpty()) {
-            adminURI = new URIish(url);
-          }
-        } catch (URISyntaxException e) {
-          log.error("The URL '" + url + "' is invalid");
-        }
-
-        if (adminURI != null) {
-          final String replacedPath =
-              replace(adminURI.getPath(), "name", projectName.get());
-          if (replacedPath != null) {
-            adminURI = adminURI.setPath(replacedPath);
-            if (usingSSH(adminURI)) {
-              replicateProject(adminURI, head);
-              adminURLUsed = true;
-            } else {
-              log.error("The adminURL '" + url
-                  + "' is non-SSH which is not allowed");
-            }
-          }
-        }
-      }
-
-      if (!adminURLUsed) {
-        for (URIish uri : uriList) {
-          replicateProject(uri, head);
-        }
-      }
-    }
-  }
-
-  private void replicateProject(final URIish replicateURI, final String head) {
-    if (!replicateURI.isRemote()) {
-      replicateProjectLocally(replicateURI, head);
-    } else if (usingSSH(replicateURI)) {
-      replicateProjectOverSsh(replicateURI, head);
-    } else {
-      log.warn("Cannot create new project on remote site since neither the "
-          + "connection method is SSH nor the replication target is local: "
-          + replicateURI.toString());
-      return;
-    }
-  }
-
-  private void replicateProjectLocally(final URIish replicateURI,
-      final String head) {
-    try {
-      final Repository repo = new FileRepository(replicateURI.getPath());
-      try {
-        repo.create(true /* bare */);
-
-        final RefUpdate u = repo.updateRef(Constants.HEAD);
-        u.disableRefLog();
-        u.link(head);
-      } finally {
-        repo.close();
-      }
-    } catch (IOException e) {
-      log.error("Failed to replicate project locally: "
-          + replicateURI.getPath());
-    }
-  }
-
-  private void replicateProjectOverSsh(final URIish replicateURI,
-      final String head) {
-    SshSessionFactory sshFactory = SshSessionFactory.getInstance();
-    RemoteSession sshSession;
-    String projectPath = QuotedString.BOURNE.quote(replicateURI.getPath());
-
-    OutputStream errStream = createErrStream();
-    String cmd =
-        "mkdir -p " + projectPath + "&& cd " + projectPath
-            + "&& git init --bare" + "&& git symbolic-ref HEAD "
-            + QuotedString.BOURNE.quote(head);
-
-    try {
-      sshSession = sshFactory.getSession(replicateURI, null, FS.DETECTED, 0);
-      Process proc = sshSession.exec(cmd, 0);
-      proc.getOutputStream().close();
-      StreamCopyThread out = new StreamCopyThread(proc.getInputStream(), errStream);
-      StreamCopyThread err = new StreamCopyThread(proc.getErrorStream(), errStream);
-      out.start();
-      err.start();
-      try {
-        proc.waitFor();
-        out.halt();
-        err.halt();
-      } catch (InterruptedException interrupted) {
-        // Don't wait, drop out immediately.
-      }
-      sshSession.disconnect();
-    } catch (IOException e) {
-      log.error("Communication error when trying to replicate to: "
-          + replicateURI.toString() + "\n" + "Error reported: "
-          + e.getMessage() + "\n" + "Error in communication: "
-          + errStream.toString());
-    }
-  }
-
-  private OutputStream createErrStream() {
-    return new OutputStream() {
-      private StringBuilder all = new StringBuilder();
-      private StringBuilder sb = new StringBuilder();
-
-      @Override
-      public String toString() {
-        String r = all.toString();
-        while (r.endsWith("\n"))
-          r = r.substring(0, r.length() - 1);
-        return r;
-      }
-
-      @Override
-      public synchronized void write(final int b) {
-        if (b == '\r') {
-          return;
-        }
-
-        sb.append((char) b);
-
-        if (b == '\n') {
-          all.append(sb);
-          sb.setLength(0);
-        }
-      }
-    };
-  }
-
-  private boolean usingSSH(final URIish uri) {
-    final String scheme = uri.getScheme();
-    if (!uri.isRemote()) return false;
-    if (scheme != null && scheme.toLowerCase().contains("ssh")) return true;
-    if (scheme == null && uri.getHost() != null && uri.getPath() != null)
-      return true;
-    return false;
-  }
-
-  static class ReplicationConfig {
-    private final RemoteConfig remote;
-    private final String[] adminUrls;
-    private final int delay;
-    private final int retryDelay;
-    private final WorkQueue.Executor pool;
-    private final Map<URIish, PushOp> pending = new HashMap<URIish, PushOp>();
-    private final PushOp.Factory opFactory;
-    private final ProjectControl.Factory projectControlFactory;
-    private final GitRepositoryManager mgr;
-    private final boolean replicatePermissions;
-
-    ReplicationConfig(final Injector injector, final WorkQueue workQueue,
-        final RemoteConfig rc, final Config cfg, SchemaFactory<ReviewDb> db,
-        final ReplicationUser.Factory replicationUserFactory,
-        final GitRepositoryManager gitRepositoryManager,
-        GroupCache groupCache) {
-
-      remote = rc;
-      delay = Math.max(0, getInt(rc, cfg, "replicationdelay", 15));
-      retryDelay = Math.max(0, getInt(rc, cfg, "replicationretry", 1));
-
-      final int poolSize = Math.max(0, getInt(rc, cfg, "threads", 1));
-      final String poolName = "ReplicateTo-" + rc.getName();
-      pool = workQueue.createQueue(poolSize, poolName);
-
-      String[] authGroupNames =
-          cfg.getStringList("remote", rc.getName(), "authGroup");
-      final GroupMembership authGroups;
-      if (authGroupNames.length > 0) {
-        ImmutableSet.Builder<AccountGroup.UUID> builder = ImmutableSet.builder();
-        for (String name : authGroupNames) {
-          AccountGroup g = groupCache.get(new AccountGroup.NameKey(name));
-          if (g != null) {
-            builder.add(g.getGroupUUID());
-          } else {
-            log.warn("Group \"{0}\" not in database, removing from authGroup", name);
-          }
-        }
-        authGroups = new ListGroupMembership(builder.build());
-      } else {
-        authGroups = ReplicationUser.EVERYTHING_VISIBLE;
-      }
-
-      adminUrls = cfg.getStringList("remote", rc.getName(), "adminUrl");
-      replicatePermissions = cfg.getBoolean("remote", rc.getName(),
-              "replicatePermissions", true);
-      mgr = gitRepositoryManager;
-
-      final ReplicationUser remoteUser =
-          replicationUserFactory.create(authGroups);
-
-      projectControlFactory =
-          injector.createChildInjector(new AbstractModule() {
-            @Override
-            protected void configure() {
-              bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
-              bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
-              bind(CurrentUser.class).toInstance(remoteUser);
-            }
-          }).getInstance(ProjectControl.Factory.class);
-
-      opFactory = injector.createChildInjector(new FactoryModule() {
-        @Override
-        protected void configure() {
-          bind(PushReplication.ReplicationConfig.class).toInstance(ReplicationConfig.this);
-          bind(RemoteConfig.class).toInstance(remote);
-          factory(PushOp.Factory.class);
-        }
-      }).getInstance(PushOp.Factory.class);
-    }
-
-    private int getInt(final RemoteConfig rc, final Config cfg,
-        final String name, final int defValue) {
-      return cfg.getInt("remote", rc.getName(), name, defValue);
-    }
-
-    void schedule(final Project.NameKey project, final String ref,
-        final URIish uri) {
-      PerThreadRequestScope ctx = new PerThreadRequestScope();
-      PerThreadRequestScope old = PerThreadRequestScope.set(ctx);
-      try {
-        try {
-          if (!controlFor(project).isVisible()) {
-            return;
-          }
-        } catch (NoSuchProjectException e1) {
-          log.error("Internal error: project " + project
-              + " not found during replication");
-          return;
-        }
-      } finally {
-        PerThreadRequestScope.set(old);
-      }
-
-      if (!replicatePermissions) {
-        PushOp e;
-        synchronized (pending) {
-          e = pending.get(uri);
-        }
-        if (e == null) {
-          Repository git;
-          try {
-            git = mgr.openRepository(project);
-          } catch (RepositoryNotFoundException err) {
-            log.error("Internal error: project " + project
-                + " not found during replication", err);
-            return;
-          }
-          try {
-            Ref head = git.getRef(Constants.HEAD);
-            if (head != null
-                && head.isSymbolic()
-                && GitRepositoryManager.REF_CONFIG.equals(head.getLeaf().getName())) {
-              return;
-            }
-          } catch (IOException err) {
-            log.error("Internal error: cannot check type of project " + project
-                + " during replication", err);
-            return;
-          } finally {
-            git.close();
-          }
-        }
-      }
-
-      synchronized (pending) {
-        PushOp e = pending.get(uri);
-        if (e == null) {
-          e = opFactory.create(project, uri);
-          pool.schedule(e, delay, TimeUnit.SECONDS);
-          pending.put(uri, e);
-        }
-        e.addRef(ref);
-      }
-    }
-
-    /**
-     * It schedules again a PushOp instance.
-     * <p>
-     * It is assumed to be previously scheduled and found a
-     * transport exception. It will schedule it as a push
-     * operation to be retried after the minutes count
-     * determined by class attribute retryDelay.
-     * <p>
-     * In case the PushOp instance to be scheduled has same
-     * URI than one also pending for retry, it adds to the one
-     * pending the refs list of the parameter instance.
-     * <p>
-     * In case the PushOp instance to be scheduled has same
-     * URI than one pending, but not pending for retry, it
-     * indicates the one pending should be canceled when it
-     * starts executing, removes it from pending list, and
-     * adds its refs to the parameter instance. The parameter
-     * instance is scheduled for retry.
-     * <p>
-     * Notice all operations to indicate a PushOp should be
-     * canceled, or it is retrying, or remove/add it from/to
-     * pending Map should be protected by the lock on pending
-     * Map class instance attribute.
-     *
-     * @param pushOp The PushOp instance to be scheduled.
-     */
-    void reschedule(final PushOp pushOp) {
-      // It locks access to pending variable.
-      synchronized (pending) {
-        URIish uri = pushOp.getURI();
-        PushOp pendingPushOp = pending.get(uri);
-
-        if (pendingPushOp != null) {
-          // There is one PushOp instance already pending to same URI.
-
-          if (pendingPushOp.isRetrying()) {
-            // The one pending is one already retrying, so it should
-            // maintain it and add to it the refs of the one passed
-            // as parameter to the method.
-
-            // This scenario would happen if a PushOp has started running
-            // and then before it failed due transport exception, another
-            // one to same URI started. The first one would fail and would
-            // be rescheduled, being present in pending list. When the
-            // second one fails, it will also be rescheduled and then,
-            // here, find out replication to its URI is already pending
-            // for retry (blocking).
-            pendingPushOp.addRefs(pushOp.getRefs());
-
-          } else {
-            // The one pending is one that is NOT retrying, it was just
-            // scheduled believing no problem would happen. The one pending
-            // should be canceled, and this is done by setting its canceled
-            // flag, removing it from pending list, and adding its refs to
-            // the pushOp instance that should then, later, in this method,
-            // be scheduled for retry.
-
-            // Notice that the PushOp found pending will start running and,
-            // when notifying it is starting (with pending lock protection),
-            // it will see it was canceled and then it will do nothing with
-            // pending list and it will not execute its run implementation.
-
-            pendingPushOp.cancel();
-            pending.remove(uri);
-
-            pushOp.addRefs(pendingPushOp.getRefs());
-          }
-        }
-
-        if (pendingPushOp == null || !pendingPushOp.isRetrying()) {
-          // The PushOp method param instance should be scheduled for retry.
-          // Remember when retrying it should be used different delay.
-
-          pushOp.setToRetry();
-
-          pending.put(uri, pushOp);
-          pool.schedule(pushOp, retryDelay, TimeUnit.MINUTES);
-        }
-      }
-    }
-
-    ProjectControl controlFor(final Project.NameKey project)
-        throws NoSuchProjectException {
-      return projectControlFactory.controlFor(project);
-    }
-
-    void notifyStarting(final PushOp op) {
-      synchronized (pending) {
-        if (!op.wasCanceled()) {
-          pending.remove(op.getURI());
-        }
-      }
-    }
-
-    boolean wouldPushRef(final String ref) {
-      if (!replicatePermissions && GitRepositoryManager.REF_CONFIG.equals(ref)) {
-        return false;
-      }
-      for (final RefSpec s : remote.getPushRefSpecs()) {
-        if (s.matchSource(ref)) {
-          return true;
-        }
-      }
-      return false;
-    }
-
-    boolean isReplicatePermissions() {
-      return replicatePermissions;
-    }
-
-    List<URIish> getURIs(final Project.NameKey project, final String urlMatch) {
-      final List<URIish> r = new ArrayList<URIish>(remote.getURIs().size());
-      for (URIish uri : remote.getURIs()) {
-        if (matches(uri, urlMatch)) {
-          String name = project.get();
-          if (needsUrlEncoding(uri)) {
-            name = encode(name);
-          }
-          String replacedPath = replace(uri.getPath(), "name", name);
-          if (replacedPath != null) {
-            uri = uri.setPath(replacedPath);
-            r.add(uri);
-          }
-        }
-      }
-      return r;
-    }
-
-    static boolean needsUrlEncoding(URIish uri) {
-      return "http".equalsIgnoreCase(uri.getScheme())
-        || "https".equalsIgnoreCase(uri.getScheme())
-        || "amazon-s3".equalsIgnoreCase(uri.getScheme());
-    }
-
-    static String encode(String str) {
-      try {
-        // Some cleanup is required. The '/' character is always encoded as %2F
-        // however remote servers will expect it to be not encoded as part of the
-        // path used to the repository. Space is incorrectly encoded as '+' for this
-        // context. In the path part of a URI space should be %20, but in form data
-        // space is '+'. Our cleanup replace fixes these two issues.
-        return URLEncoder.encode(str, "UTF-8")
-          .replaceAll("%2[fF]", "/")
-          .replace("+", "%20");
-      } catch (UnsupportedEncodingException e) {
-        throw new RuntimeException(e);
-      }
-    }
-
-    String[] getAdminUrls() {
-      return this.adminUrls;
-    }
-
-    private boolean matches(URIish uri, final String urlMatch) {
-      if (urlMatch == null || urlMatch.equals("") || urlMatch.equals("*")) {
-        return true;
-      }
-      return uri.toString().contains(urlMatch);
-    }
-  }
-}
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 095625d..e9a35b8 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,11 @@
 package com.google.gerrit.server.git;
 
 import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
+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 com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
@@ -41,6 +46,7 @@
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.MultiProgressMonitor.Task;
 import com.google.gerrit.server.mail.CreateChangeSender;
 import com.google.gerrit.server.mail.MergedSender;
@@ -79,7 +85,6 @@
 import org.eclipse.jgit.transport.AdvertiseRefsHook;
 import org.eclipse.jgit.transport.AdvertiseRefsHookChain;
 import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.transport.ReceiveCommand.Result;
 import org.eclipse.jgit.transport.ReceivePack;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -205,7 +210,7 @@
   private final CreateChangeSender.Factory createChangeSenderFactory;
   private final MergedSender.Factory mergedSenderFactory;
   private final ReplacePatchSetSender.Factory replacePatchSetFactory;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated replication;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final ChangeHooks hooks;
   private final ApprovalsUtil approvalsUtil;
@@ -254,7 +259,7 @@
       final CreateChangeSender.Factory createChangeSenderFactory,
       final MergedSender.Factory mergedSenderFactory,
       final ReplacePatchSetSender.Factory replacePatchSetFactory,
-      final ReplicationQueue replication,
+      final GitReferenceUpdated replication,
       final PatchSetInfoFactory patchSetInfoFactory,
       final ChangeHooks hooks,
       final ApprovalsUtil approvalsUtil,
@@ -455,8 +460,7 @@
     commandProgress = progress.beginSubTask("refs", UNKNOWN);
 
     parseCommands(commands);
-    if (newChange != null
-        && newChange.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) {
+    if (newChange != null && newChange.getResult() == NOT_ATTEMPTED) {
       createNewChanges();
     }
     newProgress.end();
@@ -473,7 +477,7 @@
     }
 
     for (final ReceiveCommand c : commands) {
-      if (c.getResult() == Result.OK) {
+      if (c.getResult() == OK) {
         switch (c.getType()) {
           case CREATE:
             if (isHead(c)) {
@@ -509,7 +513,7 @@
           // We only schedule direct refs updates for replication.
           // Change refs are scheduled when they are created.
           //
-          replication.scheduleUpdate(project.getNameKey(), c.getRefName());
+          replication.fire(project.getNameKey(), c.getRefName());
           Branch.NameKey destBranch = new Branch.NameKey(project.getNameKey(), c.getRefName());
           hooks.doRefUpdatedHook(destBranch, c.getOldId(), c.getNewId(), currentUser.getAccount());
           commandProgress.update(1);
@@ -572,7 +576,7 @@
 
   private void parseCommands(final Collection<ReceiveCommand> commands) {
     for (final ReceiveCommand cmd : commands) {
-      if (cmd.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) {
+      if (cmd.getResult() != NOT_ATTEMPTED) {
         // Already rejected by the core receive process.
         //
         continue;
@@ -620,7 +624,7 @@
           continue;
       }
 
-      if (cmd.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) {
+      if (cmd.getResult() != NOT_ATTEMPTED) {
         continue;
       }
 
@@ -686,7 +690,9 @@
     RefControl ctl = projectControl.controlForRef(cmd.getRefName());
     if (ctl.canCreate(rp.getRevWalk(), obj)) {
       validateNewCommits(ctl, cmd);
-      cmd.execute(rp);
+      if (cmd.getResult() == NOT_ATTEMPTED) {
+        cmd.execute(rp);
+      }
     } else {
       errors.put(Error.CREATE, ctl.getRefName());
       reject(cmd, "can not create new references");
@@ -701,7 +707,9 @@
       }
 
       validateNewCommits(ctl, cmd);
-      cmd.execute(rp);
+      if (cmd.getResult() == NOT_ATTEMPTED) {
+        cmd.execute(rp);
+      }
     } else {
       if (GitRepositoryManager.REF_CONFIG.equals(ctl.getRefName())) {
         errors.put(Error.CONFIG_UPDATE, GitRepositoryManager.REF_CONFIG);
@@ -734,7 +742,9 @@
   private void parseDelete(final ReceiveCommand cmd) {
     RefControl ctl = projectControl.controlForRef(cmd.getRefName());
     if (ctl.canDelete()) {
-      cmd.execute(rp);
+      if (cmd.getResult() == NOT_ATTEMPTED) {
+        cmd.execute(rp);
+      }
     } else {
       if (GitRepositoryManager.REF_CONFIG.equals(ctl.getRefName())) {
         reject(cmd, "cannot delete project configuration");
@@ -761,15 +771,17 @@
     RefControl ctl = projectControl.controlForRef(cmd.getRefName());
     if (newObject != null) {
       validateNewCommits(ctl, cmd);
-      if (cmd.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) {
+      if (cmd.getResult() != NOT_ATTEMPTED) {
         return;
       }
     }
 
     if (ctl.canForceUpdate()) {
-      cmd.execute(rp);
+      if (cmd.getResult() == NOT_ATTEMPTED) {
+        cmd.execute(rp);
+      }
     } else {
-      cmd.setResult(ReceiveCommand.Result.REJECTED_NONFASTFORWARD, " need '"
+      cmd.setResult(REJECTED_NONFASTFORWARD, " need '"
           + PermissionRule.FORCE_PUSH + "' privilege.");
     }
   }
@@ -876,7 +888,7 @@
         walk.setRevFilter(oldRevFilter);
       }
     } catch (IOException e) {
-      newChange.setResult(Result.REJECTED_MISSING_OBJECT);
+      newChange.setResult(REJECTED_MISSING_OBJECT);
       log.error("Invalid pack upload; one or more objects weren't sent", e);
       return;
     }
@@ -1051,7 +1063,7 @@
       // Should never happen, the core receive process would have
       // identified the missing object earlier before we got control.
       //
-      newChange.setResult(Result.REJECTED_MISSING_OBJECT);
+      newChange.setResult(REJECTED_MISSING_OBJECT);
       log.error("Invalid pack upload; one or more objects weren't sent", e);
       return;
     } catch (OrmException e) {
@@ -1079,7 +1091,7 @@
       }
       newProgress.update(1);
     }
-    newChange.setResult(ReceiveCommand.Result.OK);
+    newChange.setResult(OK);
   }
 
   private static boolean isValidChangeId(String idStr) {
@@ -1155,7 +1167,7 @@
       throw new IOException("Failed to create ref " + ps.getRefName() + " in "
           + repo.getDirectory() + ": " + ru.getResult());
     }
-    replication.scheduleUpdate(project.getNameKey(), ru.getName());
+    replication.fire(project.getNameKey(), ru.getName());
 
     allNewChanges.add(change);
 
@@ -1206,7 +1218,7 @@
             + request.ontoChange + ", commit " + request.newCommit.name(), err);
         reject(request.cmd, "database error");
       }
-      if (request.cmd.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) {
+      if (request.cmd.getResult() == NOT_ATTEMPTED) {
         log.error("Replacement patch for change " + request.ontoChange
             + ", commit " + request.newCommit.name() + " wasn't attempted."
             + "  This is a bug in the receive process implementation.");
@@ -1457,9 +1469,9 @@
       throw new IOException("Failed to create ref " + ps.getRefName() + " in "
           + repo.getDirectory() + ": " + ru.getResult());
     }
-    replication.scheduleUpdate(project.getNameKey(), ru.getName());
+    replication.fire(project.getNameKey(), ru.getName());
     hooks.doPatchsetCreatedHook(result.change, ps, db);
-    request.cmd.setResult(ReceiveCommand.Result.OK);
+    request.cmd.setResult(OK);
 
     workQueue.getDefaultQueue()
         .submit(requestScopePropagator.wrap(new Runnable() {
@@ -1598,7 +1610,7 @@
         }
       }
     } catch (IOException err) {
-      cmd.setResult(Result.REJECTED_MISSING_OBJECT);
+      cmd.setResult(REJECTED_MISSING_OBJECT);
       log.error("Invalid pack upload; one or more objects weren't sent", err);
     }
   }
@@ -2042,7 +2054,7 @@
   }
 
   private void reject(final ReceiveCommand cmd, final String why) {
-    cmd.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, why);
+    cmd.setResult(REJECTED_OTHER_REASON, why);
     commandProgress.update(1);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
index e87fe2b..ec8c080 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
@@ -16,7 +16,7 @@
 
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.transport.AdvertiseRefsHook;
-import org.eclipse.jgit.transport.ReceivePack;
+import org.eclipse.jgit.transport.BaseReceivePack;
 import org.eclipse.jgit.transport.UploadPack;
 
 import java.util.HashMap;
@@ -31,7 +31,7 @@
   }
 
   @Override
-  public void advertiseRefs(ReceivePack rp) {
+  public void advertiseRefs(BaseReceivePack rp) {
     Map<String, Ref> oldRefs = rp.getAdvertisedRefs();
     if (oldRefs == null) {
       oldRefs = rp.getRepository().getAllRefs();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplicationQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplicationQueue.java
deleted file mode 100644
index f7bb9f8..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplicationQueue.java
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import com.google.gerrit.reviewdb.client.Project;
-
-/** Manages replication to other nodes. */
-public interface ReplicationQueue {
-  /** Is replication to one or more other destinations configured? */
-  boolean isEnabled();
-
-  /**
-   * Schedule a full replication for a single project.
-   * <p>
-   * All remote URLs are checked to verify the are current with regards to the
-   * local project state. If not, they are updated by pushing new refs, updating
-   * existing ones which don't match, and deleting stale refs which have been
-   * removed from the local repository.
-   *
-   * @param project identity of the project to replicate.
-   * @param urlMatch substring that must appear in a URI to support replication.
-   */
-  void scheduleFullSync(Project.NameKey project, String urlMatch);
-
-  /**
-   * Schedule update of a single ref.
-   * <p>
-   * This method automatically tries to batch together multiple requests in the
-   * same project, to take advantage of Git's native ability to update multiple
-   * refs during a single push operation.
-   *
-   * @param project identity of the project to replicate.
-   * @param ref unique name of the ref; must start with {@code refs/}.
-   */
-  void scheduleUpdate(Project.NameKey project, String ref);
-
-  /**
-   * Create new empty project at the remote sites.
-   * <p>
-   * When a new project has been created locally call this method to make sure
-   * that the project will be created at the remote sites as well.
-   *
-   * @param project of the project to be created.
-   * @param head name HEAD should point at (must be {@code refs/heads/...}).
-   */
-  void replicateNewProject(Project.NameKey project, String head);
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SecureCredentialsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SecureCredentialsProvider.java
deleted file mode 100644
index d51936a..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SecureCredentialsProvider.java
+++ /dev/null
@@ -1,92 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.errors.UnsupportedCredentialItem;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.transport.CredentialItem;
-import org.eclipse.jgit.transport.CredentialsProvider;
-import org.eclipse.jgit.transport.URIish;
-
-/** Looks up a remote's password in secure.config. */
-public class SecureCredentialsProvider extends CredentialsProvider {
-  public interface Factory {
-    SecureCredentialsProvider create(String remoteName);
-  }
-
-  private final String cfgUser;
-  private final String cfgPass;
-
-  @Inject
-  SecureCredentialsProvider(@GerritServerConfig Config cfg,
-      @Assisted String remoteName) {
-    cfgUser = cfg.getString("remote", remoteName, "username");
-    cfgPass = cfg.getString("remote", remoteName, "password");
-  }
-
-  @Override
-  public boolean isInteractive() {
-    return false;
-  }
-
-  @Override
-  public boolean supports(CredentialItem... items) {
-    for (CredentialItem i : items) {
-      if (i instanceof CredentialItem.Username) {
-        continue;
-      } else if (i instanceof CredentialItem.Password) {
-        continue;
-      } else {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  @Override
-  public boolean get(URIish uri, CredentialItem... items)
-      throws UnsupportedCredentialItem {
-    String username = uri.getUser();
-    if (username == null) {
-      username = cfgUser;
-    }
-    if (username == null) {
-      return false;
-    }
-
-    String password = uri.getPass();
-    if (password == null) {
-      password = cfgPass;
-    }
-    if (password == null) {
-      return false;
-    }
-
-    for (CredentialItem i : items) {
-      if (i instanceof CredentialItem.Username) {
-        ((CredentialItem.Username) i).setValue(username);
-      } else if (i instanceof CredentialItem.Password) {
-        ((CredentialItem.Password) i).setValue(password.toCharArray());
-      } else {
-        throw new UnsupportedCredentialItem(uri, i.getPromptText());
-      }
-    }
-    return true;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index 706ba7d..0ca8793 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.util.SubmoduleSectionParser;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
@@ -84,7 +85,7 @@
   private final Map<Change.Id, CodeReviewCommit> commits;
   private final PersonIdent myIdent;
   private final GitRepositoryManager repoManager;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated replication;
   private final SchemaFactory<ReviewDb> schemaFactory;
   private final Set<Branch.NameKey> updatedSubscribers;
 
@@ -96,7 +97,7 @@
       @Assisted Project destProject, @Assisted List<Change> submitted,
       @Assisted final Map<Change.Id, CodeReviewCommit> commits,
       @GerritPersonIdent final PersonIdent myIdent,
-      GitRepositoryManager repoManager, ReplicationQueue replication) {
+      GitRepositoryManager repoManager, GitReferenceUpdated replication) {
     this.destBranch = destBranch;
     this.mergeTip = mergeTip;
     this.rw = rw;
@@ -201,6 +202,9 @@
       if (!subscribers.isEmpty()) {
         String msgbuf = msg;
         if (msgbuf == null) {
+          // Initialize the message buffer
+          msgbuf = "";
+
           // The first updatedBranch on a cascade event of automatic
           // updates of repos is added to updatedSubscribers set so
           // if we face a situation having
@@ -331,7 +335,7 @@
       switch (rfu.update()) {
         case NEW:
         case FAST_FORWARD:
-          replication.scheduleUpdate(subscriber.getParentKey(), rfu.getName());
+          replication.fire(subscriber.getParentKey(), rfu.getName());
           // TODO since this is performed "in the background" no mail will be
           // sent to inform users about the updated branch
           break;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java
index ac4882f..3c64229 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java
@@ -14,13 +14,12 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.common.cache.Cache;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Singleton;
-import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
 import org.eclipse.jgit.lib.ObjectId;
@@ -38,19 +37,17 @@
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<EntryKey, EntryVal>> type =
-            new TypeLiteral<Cache<EntryKey, EntryVal>>() {};
-        disk(type, CACHE_NAME);
+        persist(CACHE_NAME, String.class, EntryVal.class);
         bind(TagCache.class);
       }
     };
   }
 
-  private final Cache<EntryKey, EntryVal> cache;
+  private final Cache<String, EntryVal> cache;
   private final Object createLock = new Object();
 
   @Inject
-  TagCache(@Named(CACHE_NAME) Cache<EntryKey, EntryVal> cache) {
+  TagCache(@Named(CACHE_NAME) Cache<String, EntryVal> cache) {
     this.cache = cache;
   }
 
@@ -74,67 +71,43 @@
     // never fail with an exception. Some of these references can be null
     // (e.g. not all projects are cached, or the cache is not current).
     //
-    EntryVal val = cache.get(new EntryKey(name));
+    EntryVal val = cache.getIfPresent(name.get());
     if (val != null) {
       TagSetHolder holder = val.holder;
       if (holder != null) {
         TagSet tags = holder.getTagSet();
         if (tags != null) {
-          tags.updateFastForward(refName, oldValue, newValue);
+          if (tags.updateFastForward(refName, oldValue, newValue)) {
+            cache.put(name.get(), val);
+          }
         }
       }
     }
   }
 
   TagSetHolder get(Project.NameKey name) {
-    EntryKey key = new EntryKey(name);
-    EntryVal val = cache.get(key);
+    EntryVal val = cache.getIfPresent(name.get());
     if (val == null) {
       synchronized (createLock) {
-        val = cache.get(key);
+        val = cache.getIfPresent(name.get());
         if (val == null) {
           val = new EntryVal();
           val.holder = new TagSetHolder(name);
-          cache.put(key, val);
+          cache.put(name.get(), val);
         }
       }
     }
     return val.holder;
   }
 
-  static class EntryKey implements Serializable {
-    static final long serialVersionUID = 1L;
-
-    private transient String name;
-
-    EntryKey(Project.NameKey name) {
-      this.name = name.get();
-    }
-
-    @Override
-    public int hashCode() {
-      return name.hashCode();
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (o instanceof EntryKey) {
-        return name.equals(((EntryKey) o).name);
-      }
-      return false;
-    }
-
-    private void readObject(ObjectInputStream in) throws IOException {
-      name = in.readUTF();
-    }
-
-    private void writeObject(ObjectOutputStream out) throws IOException {
-      out.writeUTF(name);
-    }
+  void put(Project.NameKey name, TagSetHolder tags) {
+    EntryVal val = new EntryVal();
+    val.holder = tags;
+    cache.put(name.get(), val);
   }
 
   static class EntryVal implements Serializable {
-    static final long serialVersionUID = EntryKey.serialVersionUID;
+    static final long serialVersionUID = 1L;
 
     transient TagSetHolder holder;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java
index 6cf873d..7d95db2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java
@@ -30,15 +30,22 @@
   final List<Ref> newRefs = new ArrayList<Ref>();
   final List<LostRef> lostRefs = new ArrayList<LostRef>();
   final TagSetHolder holder;
+  final TagCache cache;
   final Repository db;
   final Collection<Ref> include;
   TagSet tags;
-  boolean updated;
+  final boolean updated;
   private boolean rebuiltForNewTags;
 
-  TagMatcher(TagSetHolder holder, Repository db, Collection<Ref> include,
-      TagSet tags, boolean updated) {
+  TagMatcher(
+      TagSetHolder holder,
+      TagCache cache,
+      Repository db,
+      Collection<Ref> include,
+      TagSet tags,
+      boolean updated) {
     this.holder = holder;
+    this.cache = cache;
     this.db = db;
     this.include = include;
     this.tags = tags;
@@ -63,7 +70,7 @@
       }
 
       rebuiltForNewTags = true;
-      holder.rebuildForNewTags(this);
+      holder.rebuildForNewTags(cache, this);
       return isReachable(tagRef);
     }
 
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 8830580..c57942c 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
@@ -58,7 +58,7 @@
     return tags.get(id);
   }
 
-  void updateFastForward(String refName, ObjectId oldValue,
+  boolean updateFastForward(String refName, ObjectId oldValue,
       ObjectId newValue) {
     CachedRef ref = refs.get(refName);
     if (ref != null) {
@@ -68,9 +68,10 @@
       //
       ObjectId cur = ref.get();
       if (cur.equals(oldValue)) {
-        ref.compareAndSet(cur, newValue);
+        return ref.compareAndSet(cur, newValue);
       }
     }
+    return false;
   }
 
   void prepare(TagMatcher m) {
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 91c8a5c..d5120e0 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
@@ -42,51 +42,52 @@
     this.tags = tags;
   }
 
-  TagMatcher matcher(Repository db, Collection<Ref> include) {
+  TagMatcher matcher(TagCache cache, Repository db, Collection<Ref> include) {
     TagSet tags = this.tags;
     if (tags == null) {
-      tags = build(db);
+      tags = build(cache, db);
     }
 
-    TagMatcher m = new TagMatcher(this, db, include, tags, false);
+    TagMatcher m = new TagMatcher(this, cache, db, include, tags, false);
     tags.prepare(m);
     if (!m.newRefs.isEmpty() || !m.lostRefs.isEmpty()) {
-      tags = rebuild(db, tags, m);
+      tags = rebuild(cache, db, tags, m);
 
-      m = new TagMatcher(this, db, include, tags, true);
+      m = new TagMatcher(this, cache, db, include, tags, true);
       tags.prepare(m);
     }
     return m;
   }
 
-  void rebuildForNewTags(TagMatcher m) {
-    m.tags = rebuild(m.db, m.tags, null);
-
+  void rebuildForNewTags(TagCache cache, TagMatcher m) {
+    m.tags = rebuild(cache, m.db, m.tags, null);
     m.mask.clear();
     m.newRefs.clear();
     m.lostRefs.clear();
     m.tags.prepare(m);
   }
 
-  private TagSet build(Repository db) {
+  private TagSet build(TagCache cache, Repository db) {
     synchronized (buildLock) {
       TagSet tags = this.tags;
       if (tags == null) {
         tags = new TagSet(projectName);
         tags.build(db, null, null);
         this.tags = tags;
+        cache.put(projectName, this);
       }
       return tags;
     }
   }
 
-  private TagSet rebuild(Repository db, TagSet old, TagMatcher m) {
+  private TagSet rebuild(TagCache cache, Repository db, TagSet old, TagMatcher m) {
     synchronized (buildLock) {
       TagSet cur = this.tags;
       if (cur == old) {
         cur = new TagSet(projectName);
         cur.build(db, old, m);
         this.tags = cur;
+        cache.put(projectName, this);
       }
       return cur;
     }
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 fc47f10..bc9d9f7 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
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
@@ -60,6 +62,13 @@
   }
 
   public Map<String, Ref> filter(Map<String, Ref> refs, boolean filterTagsSeperately) {
+    if (projectCtl.allRefsAreVisibleExcept(
+        ImmutableSet.of(GitRepositoryManager.REF_CONFIG))) {
+      Map<String, Ref> r = Maps.newHashMap(refs);
+      r.remove(GitRepositoryManager.REF_CONFIG);
+      return r;
+    }
+
     final Set<Change.Id> visibleChanges = visibleChanges();
     final Map<String, Ref> result = new HashMap<String, Ref>();
     final List<Ref> deferredTags = new ArrayList<Ref>();
@@ -92,8 +101,10 @@
     // to identify what tags we can actually reach, and what we cannot.
     //
     if (!deferredTags.isEmpty() && (!result.isEmpty() || filterTagsSeperately)) {
-      TagMatcher tags = tagCache.get(projectName).
-          matcher(db, filterTagsSeperately ? filter(db.getAllRefs()).values() : result.values());
+      TagMatcher tags = tagCache.get(projectName).matcher(
+          tagCache,
+          db,
+          filterTagsSeperately ? filter(refs).values() : result.values());
       for (Ref tag : deferredTags) {
         if (tags.isReachable(tag)) {
           result.put(tag.getName(), tag);
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 987ab7c..bb11e62 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
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.lifecycle.LifecycleListener;
+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.server.util.IdGenerator;
@@ -172,6 +172,10 @@
           );
     }
 
+    public void unregisterWorkQueue() {
+      queues.remove(this);
+    }
+
     @Override
     protected <V> RunnableScheduledFuture<V> decorateTask(
         final Runnable runnable, RunnableScheduledFuture<V> r) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/ColumnFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/ColumnFormatter.java
new file mode 100644
index 0000000..a73f1cb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/ColumnFormatter.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.ioutil;
+
+import com.google.gerrit.server.StringUtil;
+
+import java.io.PrintWriter;
+
+/**
+ * Simple output formatter for column-oriented data, writing its output to
+ * a {@link java.io.PrintWriter} object. Handles escaping of the column
+ * data so that the resulting output is unambiguous and reasonably safe and
+ * machine parsable.
+ */
+public class ColumnFormatter {
+  private char columnSeparator;
+  private boolean firstColumn;
+  private final PrintWriter out;
+
+  /**
+   * @param out The writer to which output should be sent.
+   * @param columnSeparator A character that should serve as the separator
+   *        token between columns of output. As only non-printable characters
+   *        in the column text are ever escaped, the column separator must be
+   *        a non-printable character if the output needs to be unambiguously
+   *        parsed.
+   */
+  public ColumnFormatter(final PrintWriter out, final char columnSeparator) {
+    this.out = out;
+    this.columnSeparator = columnSeparator;
+    this.firstColumn = true;
+  }
+
+  /**
+   * Adds a text string as a new column in the current line of output,
+   * taking care of escaping as necessary.
+   *
+   * @param content the string to add.
+   */
+  public void addColumn(final String content) {
+    if (!firstColumn) {
+      out.print(columnSeparator);
+    }
+    out.print(StringUtil.escapeString(content));
+    firstColumn = false;
+  }
+
+  /**
+   * Finishes the output by flushing the current line and takes care of any
+   * other cleanup action.
+   */
+  public void finish() {
+    nextLine();
+    out.flush();
+  }
+
+  /**
+   * Flushes the current line of output and makes the formatter ready to
+   * start receiving new column data for a new line (or end-of-file).
+   * If the current line is empty nothing is done, i.e. consecutive calls
+   * to this method without intervening calls to {@link #addColumn} will
+   * be squashed.
+   */
+  public void nextLine() {
+    if (!firstColumn) {
+      out.print('\n');
+      firstColumn = true;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
index 7fabcfe1..0eb3dfe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -38,7 +39,7 @@
 
     ccAllApprovals();
     bccStarredBy();
-    bccWatchesNotifyAllComments();
+    bccWatches(NotifyType.ALL_COMMENTS);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
index 624e626..4e9ed2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
@@ -55,6 +55,19 @@
   }
 
   @Override
+  public int hashCode() {
+    return email.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other instanceof Address) {
+      return email.equals(((Address) other).email);
+    }
+    return false;
+  }
+
+  @Override
   public String toString() {
     try {
       return toHeaderString();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
index 100275e..31c8bd5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
@@ -14,37 +14,46 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.StarredChange;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.NotifyConfig;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.SingleGroupUser;
 import com.google.gwtorm.server.OrmException;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.text.MessageFormat;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
-import java.util.List;
+import java.util.Queue;
 import java.util.Set;
 import java.util.TreeSet;
 
@@ -263,13 +272,12 @@
     }
   }
 
-
   /** Get the patch list corresponding to this patch set. */
-  protected PatchList getPatchList() {
+  protected PatchList getPatchList() throws PatchListNotAvailableException {
     if (patchSet != null) {
       return args.patchListCache.get(change, patchSet);
     }
-    return null;
+    throw new PatchListNotAvailableException("no patchSet specified");
   }
 
   /** Get the project entity the change is in; null if its been deleted. */
@@ -305,53 +313,149 @@
       // Just don't BCC everyone. Better to send a partial message to those
       // we already have queued up then to fail deliver entirely to people
       // who have a lower interest in the change.
+      log.warn("Cannot BCC users that starred updated change", err);
     }
   }
 
-  /** BCC any user who has set "notify all comments" on this project. */
-  protected void bccWatchesNotifyAllComments() {
+  /** BCC users and groups that want notification of events. */
+  protected void bccWatches(NotifyType type) {
     try {
-      // BCC anyone else who has interest in this project's changes
-      //
-      for (final AccountProjectWatch w : getWatches()) {
-        if (w.isNotify(NotifyType.ALL_COMMENTS)) {
-          add(RecipientType.BCC, w.getAccountId());
-        }
+      Watchers matching = getWatches(type);
+      for (Account.Id user : matching.accounts) {
+        add(RecipientType.BCC, user);
+      }
+      for (Address addr : matching.emails) {
+        add(RecipientType.BCC, addr);
       }
     } catch (OrmException err) {
       // Just don't CC everyone. Better to send a partial message to those
       // we already have queued up then to fail deliver entirely to people
       // who have a lower interest in the change.
+      log.warn("Cannot BCC watchers for " + type, err);
     }
   }
 
   /** Returns all watches that are relevant */
-  protected final List<AccountProjectWatch> getWatches() throws OrmException {
+  protected final Watchers getWatches(NotifyType type) throws OrmException {
+    Watchers matching = new Watchers();
     if (changeData == null) {
-      return Collections.emptyList();
+      return matching;
     }
 
-    List<AccountProjectWatch> matching = new ArrayList<AccountProjectWatch>();
     Set<Account.Id> projectWatchers = new HashSet<Account.Id>();
 
     for (AccountProjectWatch w : args.db.get().accountProjectWatches()
         .byProject(change.getProject())) {
       projectWatchers.add(w.getAccountId());
-      add(matching, w);
-    }
-
-    for (AccountProjectWatch w : args.db.get().accountProjectWatches()
-        .byProject(args.allProjectsName)) {
-      if (!projectWatchers.contains(w.getAccountId())) {
+      if (w.isNotify(type)) {
         add(matching, w);
       }
     }
 
-    return Collections.unmodifiableList(matching);
+    for (AccountProjectWatch w : args.db.get().accountProjectWatches()
+        .byProject(args.allProjectsName)) {
+      if (!projectWatchers.contains(w.getAccountId()) && w.isNotify(type)) {
+        add(matching, w);
+      }
+    }
+
+    ProjectState state = projectState;
+    while (state != null) {
+      for (NotifyConfig nc : state.getConfig().getNotifyConfigs()) {
+        if (nc.isNotify(type)) {
+          try {
+            add(matching, nc, state.getProject().getNameKey());
+          } catch (QueryParseException e) {
+            log.warn(String.format(
+                "Project %s has invalid notify %s filter \"%s\": %s",
+                state.getProject().getName(), nc.getName(),
+                nc.getFilter(), e.getMessage()));
+          }
+        }
+      }
+      state = state.getParentState();
+    }
+
+    return matching;
+  }
+
+  protected static class Watchers {
+    protected final Set<Account.Id> accounts = Sets.newHashSet();
+    protected final Set<Address> emails = Sets.newHashSet();
   }
 
   @SuppressWarnings("unchecked")
-  private void add(List<AccountProjectWatch> matching, AccountProjectWatch w)
+  private void add(Watchers matching, NotifyConfig nc, Project.NameKey project)
+      throws OrmException, QueryParseException {
+    for (GroupReference ref : nc.getGroups()) {
+      AccountGroup group =
+          GroupDescriptions.toAccountGroup(args.groupBackend.get(ref.getUUID()));
+      if (group == null) {
+        log.warn(String.format(
+            "Project %s has invalid group %s in notify section %s",
+            project.get(), ref.getName(), nc.getName()));
+        continue;
+      }
+
+      if (group.getType() != AccountGroup.Type.INTERNAL) {
+        log.warn(String.format(
+            "Project %s cannot use group %s of type %s in notify section %s",
+            project.get(), ref.getName(), group.getType(), nc.getName()));
+        continue;
+      }
+
+      ChangeQueryBuilder qb = args.queryBuilder.create(new SingleGroupUser(
+          args.capabilityControlFactory,
+          ref.getUUID()));
+      qb.setAllowFile(true);
+      Predicate<ChangeData> p = qb.is_visible();
+      if (nc.getFilter() != null) {
+        p = Predicate.and(qb.parse(nc.getFilter()), p);
+        p = args.queryRewriter.get().rewrite(p);
+      }
+      if (p.match(changeData)) {
+        recursivelyAddAllAccounts(matching, group);
+      }
+    }
+
+    if (!nc.getAddresses().isEmpty()) {
+      if (nc.getFilter() != null) {
+        ChangeQueryBuilder qb = args.queryBuilder.create(args.anonymousUser);
+        qb.setAllowFile(true);
+        Predicate<ChangeData> p = qb.parse(nc.getFilter());
+        p = args.queryRewriter.get().rewrite(p);
+        if (p.match(changeData)) {
+          matching.emails.addAll(nc.getAddresses());
+        }
+      } else {
+        matching.emails.addAll(nc.getAddresses());
+      }
+    }
+  }
+
+  private void recursivelyAddAllAccounts(Watchers matching, AccountGroup group)
+      throws OrmException {
+    Set<AccountGroup.Id> seen = Sets.newHashSet();
+    Queue<AccountGroup.Id> scan = Lists.newLinkedList();
+    scan.add(group.getId());
+    seen.add(group.getId());
+    while (!scan.isEmpty()) {
+      AccountGroup.Id next = scan.remove();
+      for (AccountGroupMember m : args.db.get().accountGroupMembers()
+          .byGroup(next)) {
+        matching.accounts.add(m.getAccountId());
+      }
+      for (AccountGroupInclude m : args.db.get().accountGroupIncludes()
+          .byGroup(next)) {
+        if (seen.add(m.getIncludeId())) {
+          scan.add(m.getIncludeId());
+        }
+      }
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private void add(Watchers matching, AccountProjectWatch w)
       throws OrmException {
     IdentifiedUser user =
         args.identifiedUserFactory.create(args.db, w.getAccountId());
@@ -363,13 +467,13 @@
         p = Predicate.and(qb.parse(w.getFilter()), p);
         p = args.queryRewriter.get().rewrite(p);
         if (p.match(changeData)) {
-          matching.add(w);
+          matching.accounts.add(w.getAccountId());
         }
       } catch (QueryParseException e) {
         // Ignore broken filter expressions.
       }
     } else if (p.match(changeData)) {
-      matching.add(w);
+      matching.accounts.add(w.getAccountId());
     }
   }
 
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 f054ee8..e7cc1ff 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
@@ -17,13 +17,14 @@
 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.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.patch.PatchFile;
 import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
@@ -68,7 +69,7 @@
 
     ccAllApprovals();
     bccStarredBy();
-    bccWatchesNotifyAllComments();
+    bccWatches(NotifyType.ALL_COMMENTS);
   }
 
   @Override
@@ -77,11 +78,22 @@
   }
 
   public String getInlineComments() {
+    return getInlineComments(1);
+  }
+
+  public String getInlineComments(int lines) {
     StringBuilder  cmts = new StringBuilder();
 
     final Repository repo = getRepository();
     try {
-      final PatchList patchList = repo != null ? getPatchList() : null;
+      PatchList patchList = null;
+      if (repo != null) {
+        try {
+          patchList = getPatchList();
+        } catch (PatchListNotAvailableException e) {
+          patchList = null;
+        }
+      }
 
       Patch.Key currentFileKey = null;
       PatchFile currentFileData = null;
@@ -113,19 +125,29 @@
           }
         }
 
-        cmts.append("Line " + lineNbr);
         if (currentFileData != null) {
+          int maxLines;
           try {
-            final String lineStr = currentFileData.getLine(side, lineNbr);
-            cmts.append(": ");
-            cmts.append(lineStr);
-          } catch (Throwable cce) {
-            // Don't quote the line if we can't safely convert it.
+            maxLines = currentFileData.getLineCount(side);
+          } catch (Throwable e) {
+            maxLines = lineNbr;
+          }
+
+          final int startLine = Math.max(1, lineNbr - lines + 1);
+          final int stopLine = Math.min(maxLines, lineNbr + lines);
+
+          for (int line = startLine; line <= lineNbr; ++line) {
+            appendFileLine(cmts, currentFileData, side, line);
+          }
+
+          cmts.append(c.getMessage().trim());
+          cmts.append("\n");
+
+          for (int line = lineNbr + 1; line < stopLine; ++line) {
+            appendFileLine(cmts, currentFileData, side, line);
           }
         }
-        cmts.append("\n");
 
-        cmts.append(c.getMessage().trim());
         cmts.append("\n\n");
       }
     } finally {
@@ -136,10 +158,22 @@
     return cmts.toString();
   }
 
+  private void appendFileLine(StringBuilder cmts, PatchFile fileData, short side, int line) {
+    cmts.append("Line " + line);
+    try {
+      final String lineStr = fileData.getLine(side, line);
+      cmts.append(": ");
+      cmts.append(lineStr);
+    } catch (Throwable e) {
+      // Don't quote the line if we can't safely convert it.
+    }
+    cmts.append("\n");
+  }
+
   private Repository getRepository() {
     try {
       return args.server.openRepository(projectState.getProject().getNameKey());
-    } catch (RepositoryNotFoundException e) {
+    } catch (IOException e) {
       return null;
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
index c6f716d..9b82c71 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
@@ -15,74 +15,65 @@
 package com.google.gerrit.server.mail;
 
 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.AccountProjectWatch;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import java.util.HashSet;
-import java.util.Set;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /** Notify interested parties of a brand new change. */
 public class CreateChangeSender extends NewChangeSender {
+  private static final Logger log =
+      LoggerFactory.getLogger(CreateChangeSender.class);
+
   public static interface Factory {
     public CreateChangeSender create(Change change);
   }
 
-  private final GroupCache groupCache;
-
   @Inject
   public CreateChangeSender(EmailArguments ea,
       @AnonymousCowardName String anonymousCowardName, SshInfo sshInfo,
-      GroupCache groupCache, @Assisted Change c) {
+      @Assisted Change c) {
     super(ea, anonymousCowardName, sshInfo, c);
-    this.groupCache = groupCache;
   }
 
   @Override
   protected void init() throws EmailException {
     super.init();
 
-    bccWatchers();
-  }
-
-  private void bccWatchers() {
     try {
+      // BCC anyone who has interest in this project's changes
       // Try to mark interested owners with a TO and not a BCC line.
       //
-      final Set<Account.Id> owners = new HashSet<Account.Id>();
-      for (AccountGroup.UUID uuid : getProjectOwners()) {
-        AccountGroup group = groupCache.get(uuid);
-        if (group != null) {
-          for (AccountGroupMember m : args.db.get().accountGroupMembers()
-              .byGroup(group.getId())) {
-            owners.add(m.getAccountId());
-          }
+      Watchers matching = getWatches(NotifyType.NEW_CHANGES);
+      for (Account.Id user : matching.accounts) {
+        if (isOwnerOfProjectOrBranch(user)) {
+          add(RecipientType.TO, user);
+        } else {
+          add(RecipientType.BCC, user);
         }
       }
-
-      // BCC anyone who has interest in this project's changes
-      //
-      for (final AccountProjectWatch w : getWatches()) {
-        if (w.isNotify(NotifyType.NEW_CHANGES)) {
-          if (owners.contains(w.getAccountId())) {
-            add(RecipientType.TO, w.getAccountId());
-          } else {
-            add(RecipientType.BCC, w.getAccountId());
-          }
-        }
+      for (Address addr : matching.emails) {
+        add(RecipientType.BCC, addr);
       }
     } catch (OrmException err) {
       // Just don't CC everyone. Better to send a partial message to those
       // we already have queued up then to fail deliver entirely to people
       // who have a lower interest in the change.
+      log.warn("Cannot BCC watchers for new change", err);
     }
   }
+
+  private boolean isOwnerOfProjectOrBranch(Account.Id user) {
+    return projectState != null
+        && change != null
+        && projectState.controlFor(args.identifiedUserFactory.create(user))
+          .controlForRef(change.getDest())
+          .isOwner();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
index 68f78d0..e6eda82 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
@@ -15,10 +15,12 @@
 package com.google.gerrit.server.mail;
 
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.IdentifiedUser.GenericFactory;
 import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -37,13 +39,15 @@
 class EmailArguments {
   final GitRepositoryManager server;
   final ProjectCache projectCache;
-  final GroupCache groupCache;
+  final GroupBackend groupBackend;
   final AccountCache accountCache;
   final PatchListCache patchListCache;
   final FromAddressGenerator fromAddressGenerator;
   final EmailSender emailSender;
   final PatchSetInfoFactory patchSetInfoFactory;
   final IdentifiedUser.GenericFactory identifiedUserFactory;
+  final CapabilityControl.Factory capabilityControlFactory;
+  final AnonymousUser anonymousUser;
   final Provider<String> urlProvider;
   final AllProjectsName allProjectsName;
 
@@ -51,32 +55,39 @@
   final Provider<ChangeQueryRewriter> queryRewriter;
   final Provider<ReviewDb> db;
   final RuntimeInstance velocityRuntime;
+  final EmailSettings settings;
 
   @Inject
   EmailArguments(GitRepositoryManager server, ProjectCache projectCache,
-      GroupCache groupCache, AccountCache accountCache,
+      GroupBackend groupBackend, AccountCache accountCache,
       PatchListCache patchListCache, FromAddressGenerator fromAddressGenerator,
       EmailSender emailSender, PatchSetInfoFactory patchSetInfoFactory,
       GenericFactory identifiedUserFactory,
+      CapabilityControl.Factory capabilityControlFactory,
+      AnonymousUser anonymousUser,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
       AllProjectsName allProjectsName,
       ChangeQueryBuilder.Factory queryBuilder,
       Provider<ChangeQueryRewriter> queryRewriter, Provider<ReviewDb> db,
-      RuntimeInstance velocityRuntime) {
+      RuntimeInstance velocityRuntime,
+      EmailSettings settings) {
     this.server = server;
     this.projectCache = projectCache;
-    this.groupCache = groupCache;
+    this.groupBackend = groupBackend;
     this.accountCache = accountCache;
     this.patchListCache = patchListCache;
     this.fromAddressGenerator = fromAddressGenerator;
     this.emailSender = emailSender;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.identifiedUserFactory = identifiedUserFactory;
+    this.capabilityControlFactory = capabilityControlFactory;
+    this.anonymousUser = anonymousUser;
     this.urlProvider = urlProvider;
     this.allProjectsName = allProjectsName;
     this.queryBuilder = queryBuilder;
     this.queryRewriter = queryRewriter;
     this.db = db;
     this.velocityRuntime = velocityRuntime;
+    this.settings = settings;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java
new file mode 100644
index 0000000..7e44877
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+class EmailSettings {
+  final boolean includeDiff;
+  final int maximumDiffSize;
+
+  @Inject
+  EmailSettings(@GerritServerConfig Config cfg) {
+    includeDiff = cfg.getBoolean("sendemail", "includeDiff", false);
+    maximumDiffSize = cfg.getInt("sendemail", "maximumDiffSize", 256 << 10);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
index bb50374..86cebdc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.common.base.Charsets;
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -24,9 +25,13 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.apache.commons.codec.binary.Base64;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
 /** Creates a {@link FromAddressGenerator} from the {@link GerritServerConfig} */
 @Singleton
 public class FromAddressGeneratorProvider implements
@@ -121,7 +126,7 @@
   }
 
   static final class PatternGen implements FromAddressGenerator {
-    private final String senderEmail;
+    private final ParameterizedString senderEmailPattern;
     private final Address serverAddress;
     private final AccountCache accountCache;
     private final String anonymousCowardName;
@@ -130,7 +135,7 @@
     PatternGen(final Address serverAddress, final AccountCache accountCache,
         final String anonymousCowardName,
         final ParameterizedString namePattern, final String senderEmail) {
-      this.senderEmail = senderEmail;
+      this.senderEmailPattern = new ParameterizedString(senderEmail);
       this.serverAddress = serverAddress;
       this.accountCache = accountCache;
       this.anonymousCowardName = anonymousCowardName;
@@ -158,7 +163,25 @@
         senderName = serverAddress.name;
       }
 
+      String senderEmail;
+      if (senderEmailPattern.getParameterNames().isEmpty()) {
+        senderEmail = senderEmailPattern.getRawPattern();
+      } else {
+        senderEmail = senderEmailPattern
+            .replace("userHash", hashOf(senderName))
+            .toString();
+      }
       return new Address(senderName, senderEmail);
     }
   }
+
+  private static String hashOf(String data) {
+    try {
+      MessageDigest hash = MessageDigest.getInstance("MD5");
+      byte[] bytes = hash.digest(data.getBytes(Charsets.UTF_8));
+      return Base64.encodeBase64URLSafeString(bytes);
+    } catch (NoSuchAlgorithmException e) {
+      throw new RuntimeException("No MD5 available", e);
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
index 3590b8a..70b2d7f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.ApprovalCategory;
 import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
 import com.google.gerrit.reviewdb.client.Change;
@@ -53,8 +52,8 @@
 
     ccAllApprovals();
     bccStarredBy();
-    bccWatchesNotifyAllComments();
-    bccWatchesNotifySubmittedChanges();
+    bccWatches(NotifyType.ALL_COMMENTS);
+    bccWatches(NotifyType.SUBMITTED_CHANGES);
   }
 
   @Override
@@ -140,20 +139,4 @@
     }
     m.put(ca.getCategoryId(), ca);
   }
-
-  private void bccWatchesNotifySubmittedChanges() {
-    try {
-      // BCC anyone else who has interest in this project's changes
-      //
-      for (final AccountProjectWatch w : getWatches()) {
-        if (w.isNotify(NotifyType.SUBMITTED_CHANGES)) {
-          add(RecipientType.BCC, w.getAccountId());
-        }
-      }
-    } catch (OrmException err) {
-      // Just don't CC everyone. Better to send a partial message to those
-      // we already have queued up then to fail deliver entirely to people
-      // who have a lower interest in the change.
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
index 2e459d4..82c1405 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
@@ -16,10 +16,21 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.ssh.SshInfo;
 
 import com.jcraft.jsch.HostKey;
 
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
@@ -28,6 +39,9 @@
 
 /** Sends an email alerting a user to a new change for them to review. */
 public abstract class NewChangeSender extends ChangeEmail {
+  private static final Logger log =
+      LoggerFactory.getLogger(NewChangeSender.class);
+
   private final SshInfo sshInfo;
   private final Set<Account.Id> reviewers = new HashSet<Account.Id>();
   private final Set<Account.Id> extraCC = new HashSet<Account.Id>();
@@ -85,4 +99,50 @@
     }
     return host;
   }
+
+  public boolean getIncludeDiff() {
+    return args.settings.includeDiff;
+  }
+
+  /** Show patch set as unified difference.  */
+  public String getUnifiedDiff() {
+    PatchList patchList;
+    try {
+      patchList = getPatchList();
+      if (patchList.getOldId() == null) {
+        // Octopus merges are not well supported for diff output by Gerrit.
+        // Currently these always have a null oldId in the PatchList.
+        return "";
+      }
+    } catch (PatchListNotAvailableException e) {
+      log.error("Cannot format patch", e);
+      return "";
+    }
+
+    TemporaryBuffer.Heap buf =
+        new TemporaryBuffer.Heap(args.settings.maximumDiffSize);
+    DiffFormatter fmt = new DiffFormatter(buf);
+    Repository git;
+    try {
+      git = args.server.openRepository(change.getProject());
+    } catch (IOException e) {
+      log.error("Cannot open repository to format patch", e);
+      return "";
+    }
+    try {
+      fmt.setRepository(git);
+      fmt.setDetectRenames(true);
+      fmt.format(patchList.getOldId(), patchList.getNewId());
+      return RawParseUtils.decode(buf.toByteArray());
+    } catch (IOException e) {
+      if (JGitText.get().inMemoryBufferLimitExceeded.equals(e.getMessage())) {
+        return "";
+      }
+      log.error("Cannot format patch", e);
+      return "";
+    } finally {
+      fmt.release();
+      git.close();
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index de8628f..caa441d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.server.account.AccountState;
@@ -34,14 +35,13 @@
 import java.io.StringWriter;
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
-import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /** Sends an email to one or more interested parties. */
 public abstract class OutgoingEmail {
@@ -53,7 +53,7 @@
   protected String messageClass;
   private final HashSet<Account.Id> rcptTo = new HashSet<Account.Id>();
   private final Map<String, EmailHeader> headers;
-  private final List<Address> smtpRcptTo = new ArrayList<Address>();
+  private final Set<Address> smtpRcptTo = Sets.newHashSet();
   private Address smtpFromAddress;
   private StringBuilder body;
   protected VelocityContext velocityContext;
@@ -282,7 +282,7 @@
       return false;
     }
 
-    if (rcptTo.size() == 1 && rcptTo.contains(fromId)) {
+    if (smtpRcptTo.size() == 1 && rcptTo.size() == 1 && rcptTo.contains(fromId)) {
       // If the only recipient is also the sender, don't bother.
       //
       return false;
@@ -324,14 +324,15 @@
   protected void add(final RecipientType rt, final Address addr) {
     if (addr != null && addr.email != null && addr.email.length() > 0) {
       if (args.emailSender.canEmail(addr.email)) {
-        smtpRcptTo.add(addr);
-        switch (rt) {
-          case TO:
-            ((EmailHeader.AddressList) headers.get(HDR_TO)).add(addr);
-            break;
-          case CC:
-            ((EmailHeader.AddressList) headers.get(HDR_CC)).add(addr);
-            break;
+        if (smtpRcptTo.add(addr)) {
+          switch (rt) {
+            case TO:
+              ((EmailHeader.AddressList) headers.get(HDR_TO)).add(addr);
+              break;
+            case CC:
+              ((EmailHeader.AddressList) headers.get(HDR_CC)).add(addr);
+              break;
+          }
         }
       } else {
         log.warn("Not emailing " + addr.email + " (prohibited by allowrcpt)");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
index c9afdde..946c29f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -38,7 +39,7 @@
 
     ccAllApprovals();
     bccStarredBy();
-    bccWatchesNotifyAllComments();
+    bccWatches(NotifyType.ALL_COMMENTS);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
index 964bfed..033bd56 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -37,7 +38,7 @@
 
     ccAllApprovals();
     bccStarredBy();
-    bccWatchesNotifyAllComments();
+    bccWatches(NotifyType.ALL_COMMENTS);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
index c5c5925..08af5e7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
@@ -70,11 +70,11 @@
     return edits;
   }
 
-  ObjectId getBlobA() {
+  public ObjectId getBlobA() {
     return aId;
   }
 
-  ObjectId getBlobB() {
+  public ObjectId getBlobB() {
     return bId;
   }
 
@@ -114,6 +114,9 @@
   public String toString() {
     StringBuilder n = new StringBuilder();
     n.append("IntraLineDiffKey[");
+    if (projectKey != null) {
+      n.append(projectKey.get()).append(" ");
+    }
     n.append(aId.name());
     n.append("..");
     n.append(bId.name());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
index 358d3ba..5b65920 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -15,7 +15,7 @@
 
 package com.google.gerrit.server.patch;
 
-import com.google.gerrit.server.cache.EntryCreator;
+import com.google.common.cache.CacheLoader;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
@@ -35,9 +35,8 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.regex.Pattern;
 
-class IntraLineLoader extends EntryCreator<IntraLineDiffKey, IntraLineDiff> {
-  private static final Logger log = LoggerFactory
-      .getLogger(IntraLineLoader.class);
+class IntraLineLoader extends CacheLoader<IntraLineDiffKey, IntraLineDiff> {
+  static final Logger log = LoggerFactory.getLogger(IntraLineLoader.class);
 
   private static final Pattern BLANK_LINE_RE = Pattern
       .compile("^[ \\t]*(|[{}]|/\\*\\*?|\\*)[ \\t]*$");
@@ -62,7 +61,7 @@
   }
 
   @Override
-  public IntraLineDiff createEntry(IntraLineDiffKey key) throws Exception {
+  public IntraLineDiff load(IntraLineDiffKey key) throws Exception {
     Worker w = workerPool.poll();
     if (w == null) {
       w = new Worker();
@@ -119,7 +118,7 @@
         throws Exception {
       if (!input.offer(new Input(key))) {
         log.error("Cannot enqueue task to thread " + thread.getName());
-        return null;
+        return Result.TIMEOUT;
       }
 
       Result r = result.poll(timeoutMillis, TimeUnit.MILLISECONDS);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWeigher.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWeigher.java
new file mode 100644
index 0000000..f6cff15
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWeigher.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.patch;
+
+import com.google.common.cache.Weigher;
+
+/** Approximates memory usage for IntralineDiff in bytes of memory used. */
+public class IntraLineWeigher implements
+    Weigher<IntraLineDiffKey, IntraLineDiff> {
+  @Override
+  public int weigh(IntraLineDiffKey key, IntraLineDiff value) {
+    return 16 + 8*8 + 2*36     // Size of IntraLineDiffKey, 64 bit JVM
+        + 16 + 2*8 + 16+8+4+20 // Size of IntraLineDiff, 64 bit JVM
+        + (8 + 16 + 4*4) * value.getEdits().size();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
index f120ebf..6fcf581 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
@@ -111,6 +111,35 @@
     }
   }
 
+  /**
+   * Return number of lines in file.
+   *
+   * @param file the file index to extract.
+   * @return number of lines in file.
+   * @throws CorruptEntityException the patch cannot be read.
+   * @throws IOException the patch or complete file content cannot be read.
+   * @throws NoSuchEntityException the file is not exist.
+   */
+  public int getLineCount(final int file)
+      throws CorruptEntityException, IOException, NoSuchEntityException {
+    switch (file) {
+      case 0:
+        if (a == null) {
+          a = load(aTree, entry.getOldName());
+        }
+        return a.size();
+
+      case 1:
+        if (b == null) {
+          b = load(bTree, entry.getNewName());
+        }
+        return b.size();
+
+      default:
+        throw new NoSuchEntityException();
+    }
+  }
+
   private Text load(final ObjectId tree, final String path)
       throws MissingObjectException, IncorrectObjectTypeException,
       CorruptObjectException, IOException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
index aab8e39..93d7bf7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
@@ -142,8 +142,12 @@
   }
 
   private int search(final String fileName) {
+    if (Patch.COMMIT_MSG.equals(fileName)) {
+      return 0;
+    }
+
     int high = patches.length;
-    int low = 0;
+    int low = 1;
     while (low < high) {
       final int mid = (low + high) >>> 1;
       final int cmp = patches[mid].getNewName().compareTo(fileName);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
index 8a61d30..fe77f5d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
@@ -19,9 +19,10 @@
 
 /** Provides a cached list of {@link PatchListEntry}. */
 public interface PatchListCache {
-  public PatchList get(PatchListKey key);
+  public PatchList get(PatchListKey key) throws PatchListNotAvailableException;
 
-  public PatchList get(Change change, PatchSet patchSet);
+  public PatchList get(Change change, PatchSet patchSet)
+      throws PatchListNotAvailableException;
 
   public IntraLineDiff getIntraLineDiff(IntraLineDiffKey key);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index 26dbe2d..967e6a7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -15,24 +15,23 @@
 
 package com.google.gerrit.server.patch;
 
-
+import com.google.common.cache.LoadingCache;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 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.AccountDiffPreference.Whitespace;
-import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EvictionPolicy;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Singleton;
-import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 
+import java.util.concurrent.ExecutionException;
+
 /** Provides a cached list of {@link PatchListEntry}. */
 @Singleton
 public class PatchListCacheImpl implements PatchListCache {
@@ -43,21 +42,15 @@
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<PatchListKey, PatchList>> fileType =
-            new TypeLiteral<Cache<PatchListKey, PatchList>>() {};
-        disk(fileType, FILE_NAME) //
-            .memoryLimit(128) // very large items, cache only a few
-            .evictionPolicy(EvictionPolicy.LRU) // prefer most recent
-            .populateWith(PatchListLoader.class) //
-        ;
+        persist(FILE_NAME, PatchListKey.class, PatchList.class)
+            .maximumWeight(10 << 20)
+            .loader(PatchListLoader.class)
+            .weigher(PatchListWeigher.class);
 
-        final TypeLiteral<Cache<IntraLineDiffKey, IntraLineDiff>> intraType =
-            new TypeLiteral<Cache<IntraLineDiffKey, IntraLineDiff>>() {};
-        disk(intraType, INTRA_NAME) //
-            .memoryLimit(128) // very large items, cache only a few
-            .evictionPolicy(EvictionPolicy.LRU) // prefer most recent
-            .populateWith(IntraLineLoader.class) //
-        ;
+        persist(INTRA_NAME, IntraLineDiffKey.class, IntraLineDiff.class)
+            .maximumWeight(10 << 20)
+            .loader(IntraLineLoader.class)
+            .weigher(IntraLineWeigher.class);
 
         bind(PatchListCacheImpl.class);
         bind(PatchListCache.class).to(PatchListCacheImpl.class);
@@ -65,14 +58,14 @@
     };
   }
 
-  private final Cache<PatchListKey, PatchList> fileCache;
-  private final Cache<IntraLineDiffKey, IntraLineDiff> intraCache;
+  private final LoadingCache<PatchListKey, PatchList> fileCache;
+  private final LoadingCache<IntraLineDiffKey, IntraLineDiff> intraCache;
   private final boolean computeIntraline;
 
   @Inject
   PatchListCacheImpl(
-      @Named(FILE_NAME) final Cache<PatchListKey, PatchList> fileCache,
-      @Named(INTRA_NAME) final Cache<IntraLineDiffKey, IntraLineDiff> intraCache,
+      @Named(FILE_NAME) LoadingCache<PatchListKey, PatchList> fileCache,
+      @Named(INTRA_NAME) LoadingCache<IntraLineDiffKey, IntraLineDiff> intraCache,
       @GerritServerConfig Config cfg) {
     this.fileCache = fileCache;
     this.intraCache = intraCache;
@@ -82,11 +75,19 @@
             cfg.getBoolean("cache", "diff", "intraline", true));
   }
 
-  public PatchList get(final PatchListKey key) {
-    return fileCache.get(key);
+  @Override
+  public PatchList get(PatchListKey key) throws PatchListNotAvailableException {
+    try {
+      return fileCache.get(key);
+    } catch (ExecutionException e) {
+      PatchListLoader.log.warn("Error computing " + key, e);
+      throw new PatchListNotAvailableException(e.getCause());
+    }
   }
 
-  public PatchList get(final Change change, final PatchSet patchSet) {
+  @Override
+  public PatchList get(final Change change, final PatchSet patchSet)
+      throws PatchListNotAvailableException {
     final Project.NameKey projectKey = change.getProject();
     final ObjectId a = null;
     final ObjectId b = ObjectId.fromString(patchSet.getRevision().get());
@@ -97,11 +98,12 @@
   @Override
   public IntraLineDiff getIntraLineDiff(IntraLineDiffKey key) {
     if (computeIntraline) {
-      IntraLineDiff d = intraCache.get(key);
-      if (d == null) {
-        d = new IntraLineDiff(IntraLineDiff.Status.ERROR);
+      try {
+        return intraCache.get(key);
+      } catch (ExecutionException e) {
+        IntraLineLoader.log.warn("Error computing " + key, e);
+        return new IntraLineDiff(IntraLineDiff.Status.ERROR);
       }
-      return d;
     } else {
       return new IntraLineDiff(IntraLineDiff.Status.DISABLED);
     }
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 33ed54e..ff9e6cf 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
@@ -122,6 +122,22 @@
     this.deletions = deletions;
   }
 
+  int weigh() {
+    int size = 16 + 6*8 + 2*4 + 20 + 16+8+4+20;
+    size += stringSize(oldName);
+    size += stringSize(newName);
+    size += header.length;
+    size += (8 + 16 + 4*4) * edits.size();
+    return size;
+  }
+
+  private static int stringSize(String str) {
+    if (str != null) {
+      return 16 + 3*4 + 16 + str.length() * 2;
+    }
+    return 0;
+  }
+
   public ChangeType getChangeType() {
     return changeType;
   }
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 5bba42b..d27c205 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
@@ -15,9 +15,9 @@
 
 package com.google.gerrit.server.patch;
 
-import com.google.gerrit.reviewdb.client.Patch;
+import com.google.common.cache.CacheLoader;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
-import com.google.gerrit.server.cache.EntryCreator;
+import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 
@@ -54,6 +54,8 @@
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
 import org.eclipse.jgit.util.TemporaryBuffer;
 import org.eclipse.jgit.util.io.DisabledOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -62,7 +64,9 @@
 import java.util.List;
 import java.util.Map;
 
-class PatchListLoader extends EntryCreator<PatchListKey, PatchList> {
+class PatchListLoader extends CacheLoader<PatchListKey, PatchList> {
+  static final Logger log = LoggerFactory.getLogger(PatchListLoader.class);
+
   private final GitRepositoryManager repoManager;
 
   @Inject
@@ -71,7 +75,7 @@
   }
 
   @Override
-  public PatchList createEntry(final PatchListKey key) throws Exception {
+  public PatchList load(final PatchListKey key) throws Exception {
     final Repository repo = repoManager.openRepository(key.projectKey);
     try {
       return readPatchList(key, repo);
@@ -251,10 +255,24 @@
 
     ObjectId treeId;
     ResolveMerger m = (ResolveMerger) MergeStrategy.RESOLVE.newMerger(repo, true);
-    ObjectInserter ins = m.getObjectInserter();
+    final ObjectInserter ins = repo.newObjectInserter();
     try {
       DirCache dc = DirCache.newInCore();
       m.setDirCache(dc);
+      m.setObjectInserter(new ObjectInserter.Filter() {
+        @Override
+        protected ObjectInserter delegate() {
+          return ins;
+        }
+
+        @Override
+        public void flush() {
+        }
+
+        @Override
+        public void release() {
+        }
+      });
 
       boolean couldMerge = false;
       try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java
similarity index 71%
rename from gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java
index 204d777..2ccc9f1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java
@@ -12,12 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.server.patch;
 
-public class IncompleteUserInfoException extends Exception {
+public class PatchListNotAvailableException extends Exception {
   private static final long serialVersionUID = 1L;
 
-  public IncompleteUserInfoException(final String userName, final String missingInfo) {
-    super("For the user \"" + userName + "\" " + missingInfo + " is not set.");
+  public PatchListNotAvailableException(String message) {
+    super(message);
+  }
+
+  public PatchListNotAvailableException(Throwable cause) {
+    super(cause);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java
new file mode 100644
index 0000000..d715246
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.patch;
+
+import com.google.common.cache.Weigher;
+
+/** Approximates memory usage for PatchList in bytes of memory used. */
+public class PatchListWeigher implements Weigher<PatchListKey, PatchList> {
+  @Override
+  public int weigh(PatchListKey key, PatchList value) {
+    int size = 16 + 4*8 + 2*36 // Size of PatchListKey, 64 bit JVM
+        + 16 + 3*8 + 3*4 + 20; // Size of PatchList, 64 bit JVM
+    for (PatchListEntry e : value.getPatches()) {
+      size += e.weigh();
+    }
+    return size;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
index 2619e00..8165bf2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
@@ -28,7 +28,6 @@
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
@@ -83,7 +82,7 @@
     Repository repo;
     try {
       repo = repoManager.openRepository(change.getProject());
-    } catch (RepositoryNotFoundException e) {
+    } catch (IOException e) {
       throw new PatchSetInfoNotAvailableException(e);
     }
     try {
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 5aee9bf..e8af060 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
@@ -16,7 +16,8 @@
 
 import static com.google.gerrit.server.plugins.PluginGuiceEnvironment.is;
 
-import com.google.common.collect.Maps;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
 import com.google.gerrit.extensions.annotations.Export;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
@@ -58,7 +59,7 @@
   private final ModuleGenerator httpGen;
 
   private Set<Class<?>> sysSingletons;
-  private Map<TypeLiteral<?>, Class<?>> sysListen;
+  private Multimap<TypeLiteral<?>, Class<?>> sysListen;
 
   Module sysModule;
   Module sshModule;
@@ -78,7 +79,7 @@
 
   AutoRegisterModules discover() throws InvalidPluginException {
     sysSingletons = Sets.newHashSet();
-    sysListen = Maps.newHashMap();
+    sysListen = LinkedListMultimap.create();
 
     if (sshGen != null) {
       sshGen.setPluginName(pluginName);
@@ -108,7 +109,7 @@
         for (Class<?> clazz : sysSingletons) {
           bind(clazz).in(Scopes.SINGLETON);
         }
-        for (Map.Entry<TypeLiteral<?>, Class<?>> e : sysListen.entrySet()) {
+        for (Map.Entry<TypeLiteral<?>, Class<?>> e : sysListen.entries()) {
           @SuppressWarnings("unchecked")
           TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
index e18d840..7018a3b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
@@ -40,6 +40,8 @@
     }
     if (!tmpFile.delete() && tmpFile.exists()) {
       PluginLoader.log.warn("Cannot delete " + tmpFile.getAbsolutePath());
+    } else {
+      PluginLoader.log.info("Cleaned plugin " + tmpFile.getName());
     }
   }
 }
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
new file mode 100644
index 0000000..dca47e0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
@@ -0,0 +1,107 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+
+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;
+import java.util.Map;
+
+/** List projects visible to the calling user. */
+public class ListPlugins {
+  private final PluginLoader pluginLoader;
+
+  @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+  private OutputFormat format = OutputFormat.TEXT;
+
+  @Inject
+  protected ListPlugins(PluginLoader pluginLoader) {
+    this.pluginLoader = pluginLoader;
+  }
+
+  public OutputFormat getFormat() {
+    return format;
+  }
+
+  public ListPlugins setFormat(OutputFormat fmt) {
+    this.format = fmt;
+    return this;
+  }
+
+  public void display(OutputStream out) {
+    final PrintWriter stdout;
+    try {
+      stdout =
+          new PrintWriter(new BufferedWriter(new OutputStreamWriter(out,
+              "UTF-8")));
+    } catch (UnsupportedEncodingException e) {
+      // Our encoding is required by the specifications for the runtime.
+      throw new RuntimeException("JVM lacks UTF-8 encoding", e);
+    }
+
+    Map<String, PluginInfo> output = Maps.newTreeMap();
+
+    List<Plugin> plugins = Lists.newArrayList(pluginLoader.getPlugins());
+    Collections.sort(plugins, new Comparator<Plugin>() {
+      @Override
+      public int compare(Plugin a, Plugin b) {
+        return a.getName().compareTo(b.getName());
+      }
+    });
+
+    if (!format.isJson()) {
+      stdout.format("%-30s %-10s\n", "Name", "Version");
+      stdout
+          .print("----------------------------------------------------------------------\n");
+    }
+
+    for (Plugin p : plugins) {
+      PluginInfo info = new PluginInfo();
+      info.version = p.getVersion();
+
+      if (format.isJson()) {
+        output.put(p.getName(), info);
+      } else {
+        stdout.format("%-30s %-10s\n", p.getName(),
+            Strings.nullToEmpty(info.version));
+      }
+    }
+
+    if (format.isJson()) {
+      format.newGson().toJson(output,
+          new TypeToken<Map<String, PluginInfo>>() {}.getType(), stdout);
+      stdout.print('\n');
+    }
+    stdout.flush();
+  }
+
+  private static class PluginInfo {
+    String version;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
index c47f370..a3c363b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -16,15 +16,18 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.annotations.PluginData;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
 
 import org.eclipse.jgit.storage.file.FileSnapshot;
 
@@ -38,6 +41,25 @@
 import javax.annotation.Nullable;
 
 public class Plugin {
+  public static enum ApiType {
+    EXTENSION, PLUGIN;
+  }
+
+  /** Unique key that changes whenever a plugin reloads. */
+  public static final class CacheKey {
+    private final String name;
+
+    CacheKey(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      int id = System.identityHashCode(this);
+      return String.format("Plugin[%s@%x]", name, id);
+    }
+  }
+
   static {
     // Guice logs warnings about multiple injectors being created.
     // Silence this in case HTTP plugins are used.
@@ -45,11 +67,27 @@
         .setLevel(java.util.logging.Level.OFF);
   }
 
+  static ApiType getApiType(Manifest manifest) throws InvalidPluginException {
+    Attributes main = manifest.getMainAttributes();
+    String v = main.getValue("Gerrit-ApiType");
+    if (Strings.isNullOrEmpty(v)
+        || ApiType.EXTENSION.name().equalsIgnoreCase(v)) {
+      return ApiType.EXTENSION;
+    } else if (ApiType.PLUGIN.name().equalsIgnoreCase(v)) {
+      return ApiType.PLUGIN;
+    } else {
+      throw new InvalidPluginException("Invalid Gerrit-ApiType: " + v);
+    }
+  }
+
+  private final CacheKey cacheKey;
   private final String name;
   private final File srcJar;
   private final FileSnapshot snapshot;
   private final JarFile jarFile;
   private final Manifest manifest;
+  private final File dataDir;
+  private final ApiType apiType;
   private final ClassLoader classLoader;
   private Class<? extends Module> sysModule;
   private Class<? extends Module> sshModule;
@@ -66,15 +104,20 @@
       FileSnapshot snapshot,
       JarFile jarFile,
       Manifest manifest,
+      File dataDir,
+      ApiType apiType,
       ClassLoader classLoader,
       @Nullable Class<? extends Module> sysModule,
       @Nullable Class<? extends Module> sshModule,
       @Nullable Class<? extends Module> httpModule) {
+    this.cacheKey = new CacheKey(name);
     this.name = name;
     this.srcJar = srcJar;
     this.snapshot = snapshot;
     this.jarFile = jarFile;
     this.manifest = manifest;
+    this.dataDir = dataDir;
+    this.apiType = apiType;
     this.classLoader = classLoader;
     this.sysModule = sysModule;
     this.sshModule = sshModule;
@@ -85,15 +128,24 @@
     return srcJar;
   }
 
+  public CacheKey getCacheKey() {
+    return cacheKey;
+  }
+
   public String getName() {
     return name;
   }
 
+  @Nullable
   public String getVersion() {
     Attributes main = manifest.getMainAttributes();
     return main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
   }
 
+  public ApiType getApiType() {
+    return apiType;
+  }
+
   boolean canReload() {
     Attributes main = manifest.getMainAttributes();
     String v = main.getValue("Gerrit-ReloadMode");
@@ -134,29 +186,33 @@
     }
 
     if (env.hasSshModule()) {
+      List<Module> modules = Lists.newLinkedList();
+      if (apiType == ApiType.PLUGIN) {
+        modules.add(env.getSshModule());
+      }
       if (sshModule != null) {
-        sshInjector = sysInjector.createChildInjector(
-            env.getSshModule(),
-            sysInjector.getInstance(sshModule));
+        modules.add(sysInjector.getInstance(sshModule));
+        sshInjector = sysInjector.createChildInjector(modules);
         manager.add(sshInjector);
       } else if (auto != null && auto.sshModule != null) {
-        sshInjector = sysInjector.createChildInjector(
-            env.getSshModule(),
-            auto.sshModule);
+        modules.add(auto.sshModule);
+        sshInjector = sysInjector.createChildInjector(modules);
         manager.add(sshInjector);
       }
     }
 
     if (env.hasHttpModule()) {
+      List<Module> modules = Lists.newLinkedList();
+      if (apiType == ApiType.PLUGIN) {
+        modules.add(env.getHttpModule());
+      }
       if (httpModule != null) {
-        httpInjector = sysInjector.createChildInjector(
-            env.getHttpModule(),
-            sysInjector.getInstance(httpModule));
+        modules.add(sysInjector.getInstance(httpModule));
+        httpInjector = sysInjector.createChildInjector(modules);
         manager.add(httpInjector);
       } else if (auto != null && auto.httpModule != null) {
-        httpInjector = sysInjector.createChildInjector(
-            env.getHttpModule(),
-            auto.httpModule);
+        modules.add(auto.httpModule);
+        httpInjector = sysInjector.createChildInjector(modules);
         manager.add(httpInjector);
       }
     }
@@ -164,17 +220,49 @@
     manager.start();
   }
 
-  private Injector newRootInjector(PluginGuiceEnvironment env) {
-    return Guice.createInjector(
-        env.getSysModule(),
-        new AbstractModule() {
-          @Override
-          protected void configure() {
-            bind(String.class)
-              .annotatedWith(PluginName.class)
-              .toInstance(name);
-          }
-        });
+  private Injector newRootInjector(final PluginGuiceEnvironment env) {
+    List<Module> modules = Lists.newArrayListWithCapacity(4);
+    modules.add(env.getSysModule());
+    if (apiType == ApiType.PLUGIN) {
+      modules.add(env.getSysModule());
+    } else {
+      modules.add(new AbstractModule() {
+        @Override
+        protected void configure() {
+          bind(ServerInformation.class).toInstance(env.getServerInformation());
+        }
+      });
+    }
+    modules.add(new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(String.class)
+          .annotatedWith(PluginName.class)
+          .toInstance(name);
+
+        bind(File.class)
+          .annotatedWith(PluginData.class)
+          .toProvider(new Provider<File>() {
+            private volatile boolean ready;
+
+            @Override
+            public File get() {
+              if (!ready) {
+                synchronized (dataDir) {
+                  if (!dataDir.exists() && !dataDir.mkdirs()) {
+                    throw new ProvisionException(String.format(
+                        "Cannot create %s for plugin %s",
+                        dataDir.getAbsolutePath(), name));
+                  }
+                  ready = true;
+                }
+              }
+              return dataDir;
+            }
+          });
+      }
+    });
+    return Guice.createInjector(modules);
   }
 
   public void stop() {
@@ -205,28 +293,14 @@
     return httpInjector;
   }
 
-  public void add(final RegistrationHandle handle) {
+  public void add(RegistrationHandle handle) {
     if (handle instanceof ReloadableRegistrationHandle) {
       if (reloadableHandles == null) {
         reloadableHandles = Lists.newArrayList();
       }
       reloadableHandles.add((ReloadableRegistrationHandle<?>) handle);
     }
-
-    add(new LifecycleListener() {
-      @Override
-      public void start() {
-      }
-
-      @Override
-      public void stop() {
-        handle.remove();
-      }
-    });
-  }
-
-  public void add(LifecycleListener listener) {
-    manager.add(listener);
+    manager.add(handle);
   }
 
   List<ReloadableRegistrationHandle<?>> getReloadableHandles() {
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
new file mode 100644
index 0000000..7081d70
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
@@ -0,0 +1,100 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+@Singleton
+class PluginCleanerTask implements Runnable {
+  private final WorkQueue workQueue;
+  private final PluginLoader loader;
+  private volatile int pending;
+  private Future<?> self;
+  private int attempts;
+  private long start;
+
+  @Inject
+  PluginCleanerTask(WorkQueue workQueue, PluginLoader loader) {
+    this.workQueue = workQueue;
+    this.loader = loader;
+  }
+
+  @Override
+  public void run() {
+    try {
+      for (int t = 0; t < 2 * (attempts + 1); t++) {
+        System.gc();
+        Thread.sleep(50);
+      }
+    } catch (InterruptedException e) {
+    }
+
+    int left = loader.processPendingCleanups();
+    synchronized (this) {
+      pending = left;
+      self = null;
+
+      if (0 < left) {
+        long waiting = System.currentTimeMillis() - start;
+        PluginLoader.log.warn(String.format(
+            "%d plugins still waiting to be reclaimed after %d minutes",
+            pending,
+            TimeUnit.MILLISECONDS.toMinutes(waiting)));
+        attempts = Math.min(attempts + 1, 15);
+        ensureScheduled();
+      } else {
+        attempts = 0;
+      }
+    }
+  }
+
+  @Override
+  public String toString() {
+    int p = pending;
+    if (0 < p) {
+      return String.format("Plugin Cleaner (waiting for %d plugins)", p);
+    }
+    return "Plugin Cleaner";
+  }
+
+  synchronized void clean(int expect) {
+    if (self == null && pending == 0) {
+      start = System.currentTimeMillis();
+    }
+    pending = expect;
+    ensureScheduled();
+  }
+
+  private void ensureScheduled() {
+    if (self == null && 0 < pending) {
+      if (attempts == 1) {
+        self = workQueue.getDefaultQueue().schedule(
+            this,
+            30,
+            TimeUnit.SECONDS);
+      } else {
+        self = workQueue.getDefaultQueue().schedule(
+            this,
+            attempts + 1,
+            TimeUnit.MINUTES);
+      }
+    }
+  }
+}
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 1b94c0c..f16131c 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
@@ -13,18 +13,22 @@
 // limitations under the License.
 
 package com.google.gerrit.server.plugins;
+import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.dynamicMapsOf;
+import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.dynamicSetsOf;
 
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
+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.extensions.registration.PrivateInternals_DynamicMapImpl;
+import com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
 import com.google.inject.AbstractModule;
 import com.google.inject.Binding;
 import com.google.inject.Injector;
@@ -56,6 +60,7 @@
 @Singleton
 public class PluginGuiceEnvironment {
   private final Injector sysInjector;
+  private final ServerInformation srvInfo;
   private final CopyConfigModule copyConfigModule;
   private final List<StartPluginListener> onStart;
   private final List<ReloadPluginListener> onReload;
@@ -76,8 +81,12 @@
   private Map<TypeLiteral<?>, DynamicMap<?>> httpMaps;
 
   @Inject
-  PluginGuiceEnvironment(Injector sysInjector, CopyConfigModule ccm) {
+  PluginGuiceEnvironment(
+      Injector sysInjector,
+      ServerInformation srvInfo,
+      CopyConfigModule ccm) {
     this.sysInjector = sysInjector;
+    this.srvInfo = srvInfo;
     this.copyConfigModule = ccm;
 
     onStart = new CopyOnWriteArrayList<StartPluginListener>();
@@ -90,6 +99,10 @@
     sysMaps = dynamicMapsOf(sysInjector);
   }
 
+  ServerInformation getServerInformation() {
+    return srvInfo;
+  }
+
   boolean hasDynamicSet(TypeLiteral<?> type) {
     return sysSets.containsKey(type)
         || (sshSets != null && sshSets.containsKey(type))
@@ -178,40 +191,18 @@
   private void attachSet(Map<TypeLiteral<?>, DynamicSet<?>> sets,
       @Nullable Injector src,
       Plugin plugin) {
-    if (src != null && sets != null && !sets.isEmpty()) {
-      for (Map.Entry<TypeLiteral<?>, DynamicSet<?>> e : sets.entrySet()) {
-        @SuppressWarnings("unchecked")
-        TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
-
-        @SuppressWarnings("unchecked")
-        DynamicSet<Object> set = (DynamicSet<Object>) e.getValue();
-
-        for (Binding<Object> b : bindings(src, type)) {
-          plugin.add(set.add(b.getKey(), b.getProvider().get()));
-        }
-      }
+    for (RegistrationHandle h : PrivateInternals_DynamicTypes
+        .attachSets(src, sets)) {
+      plugin.add(h);
     }
   }
 
   private void attachMap(Map<TypeLiteral<?>, DynamicMap<?>> maps,
       @Nullable Injector src,
       Plugin plugin) {
-    if (src != null && maps != null && !maps.isEmpty()) {
-      for (Map.Entry<TypeLiteral<?>, DynamicMap<?>> e : maps.entrySet()) {
-        @SuppressWarnings("unchecked")
-        TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
-
-        @SuppressWarnings("unchecked")
-        PrivateInternals_DynamicMapImpl<Object> set =
-            (PrivateInternals_DynamicMapImpl<Object>) e.getValue();
-
-        for (Binding<Object> b : bindings(src, type)) {
-          plugin.add(set.put(
-              plugin.getName(),
-              b.getKey(),
-              b.getProvider().get()));
-        }
-      }
+    for (RegistrationHandle h : PrivateInternals_DynamicTypes
+        .attachMaps(src, plugin.getName(), maps)) {
+      plugin.add(h);
     }
   }
 
@@ -267,6 +258,9 @@
         @SuppressWarnings("unchecked")
         Binding<Object> b = (Binding<Object>) binding;
         Key<Object> key = b.getKey();
+        if (key.getAnnotation() == null) {
+          continue;
+        }
 
         @SuppressWarnings("unchecked")
         ReloadableRegistrationHandle<Object> h =
@@ -278,7 +272,7 @@
           newPlugin.add(map.put(
               newPlugin.getName(),
               b.getKey(),
-              b.getProvider().get()));
+              b.getProvider()));
         }
       }
     }
@@ -332,6 +326,9 @@
         @SuppressWarnings("unchecked")
         Binding<Object> b = (Binding<Object>) binding;
         Key<Object> key = b.getKey();
+        if (key.getAnnotation() == null) {
+          continue;
+        }
 
         @SuppressWarnings("unchecked")
         ReloadableRegistrationHandle<Object> h1 =
@@ -345,7 +342,7 @@
           oi.remove();
           replace(newPlugin, h2, b);
         } else {
-          newPlugin.add(set.add(b.getKey(), b.getProvider().get()));
+          newPlugin.add(set.add(b.getKey(), b.getProvider()));
         }
       }
     }
@@ -353,7 +350,7 @@
 
   private static <T> void replace(Plugin newPlugin,
       ReloadableRegistrationHandle<T> h, Binding<T> b) {
-    RegistrationHandle n = h.replace(b.getKey(), b.getProvider().get());
+    RegistrationHandle n = h.replace(b.getKey(), b.getProvider());
     if (n != null){
       newPlugin.add(n);
     }
@@ -375,32 +372,6 @@
     return src.findBindingsByType(type);
   }
 
-  private static Map<TypeLiteral<?>, DynamicSet<?>> dynamicSetsOf(Injector src) {
-    Map<TypeLiteral<?>, DynamicSet<?>> m = Maps.newHashMap();
-    for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
-      TypeLiteral<?> type = e.getKey().getTypeLiteral();
-      if (type.getRawType() == DynamicSet.class) {
-        ParameterizedType p = (ParameterizedType) type.getType();
-        m.put(TypeLiteral.get(p.getActualTypeArguments()[0]),
-            (DynamicSet<?>) e.getValue().getProvider().get());
-      }
-    }
-    return m;
-  }
-
-  private static Map<TypeLiteral<?>, DynamicMap<?>> dynamicMapsOf(Injector src) {
-    Map<TypeLiteral<?>, DynamicMap<?>> m = Maps.newHashMap();
-    for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
-      TypeLiteral<?> type = e.getKey().getTypeLiteral();
-      if (type.getRawType() == DynamicMap.class) {
-        ParameterizedType p = (ParameterizedType) type.getType();
-        m.put(TypeLiteral.get(p.getActualTypeArguments()[0]),
-            (DynamicMap<?>) e.getValue().getProvider().get());
-      }
-    }
-    return m;
-  }
-
   private static Module copy(Injector src) {
     Set<TypeLiteral<?>> dynamicTypes = Sets.newHashSet();
     for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
@@ -414,8 +385,15 @@
 
     final Map<Key<?>, Binding<?>> bindings = Maps.newLinkedHashMap();
     for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
-      if (!dynamicTypes.contains(e.getKey().getTypeLiteral())
-          && shouldCopy(e.getKey())) {
+      if (dynamicTypes.contains(e.getKey().getTypeLiteral())
+          && e.getKey().getAnnotation() != null) {
+        // A type used in DynamicSet or DynamicMap that has an annotation
+        // must be picked up by the set/map itself. A type used in either
+        // but without an annotation may be magic glue implementing F and
+        // using DynamicSet<F> or DynamicMap<F> internally. That should be
+        // exported to plugins.
+        continue;
+      } else if (shouldCopy(e.getKey())) {
         bindings.put(e.getKey(), e.getValue());
       }
     }
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 16cd78c..67d715f 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
@@ -14,16 +14,20 @@
 
 package com.google.gerrit.server.plugins;
 
+import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 import com.google.inject.Module;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.Config;
@@ -58,25 +62,33 @@
   static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
 
   private final File pluginsDir;
+  private final File dataDir;
   private final File tmpDir;
   private final PluginGuiceEnvironment env;
-  private final Map<String, Plugin> running;
+  private final ServerInformationImpl srvInfoImpl;
+  private final ConcurrentMap<String, Plugin> running;
   private final Map<String, FileSnapshot> broken;
   private final ReferenceQueue<ClassLoader> cleanupQueue;
   private final ConcurrentMap<CleanupHandle, Boolean> cleanupHandles;
+  private final Provider<PluginCleanerTask> cleaner;
   private final PluginScannerThread scanner;
 
   @Inject
   public PluginLoader(SitePaths sitePaths,
       PluginGuiceEnvironment pe,
+      ServerInformationImpl sii,
+      Provider<PluginCleanerTask> pct,
       @GerritServerConfig Config cfg) {
     pluginsDir = sitePaths.plugins_dir;
+    dataDir = sitePaths.data_dir;
     tmpDir = sitePaths.tmp_dir;
     env = pe;
-    running = Maps.newHashMap();
+    srvInfoImpl = sii;
+    running = Maps.newConcurrentMap();
     broken = Maps.newHashMap();
     cleanupQueue = new ReferenceQueue<ClassLoader>();
     cleanupHandles = Maps.newConcurrentMap();
+    cleaner = pct;
 
     long checkFrequency = ConfigUtil.getTimeUnit(cfg,
         "plugins", null, "checkFrequency",
@@ -88,8 +100,8 @@
     }
   }
 
-  public synchronized List<Plugin> getPlugins() {
-    return Lists.newArrayList(running.values());
+  public Iterable<Plugin> getPlugins() {
+    return running.values();
   }
 
   public void installPluginFromStream(String name, InputStream in)
@@ -103,7 +115,6 @@
 
     File old = new File(pluginsDir, ".last_" + name + ".zip");
     File tmp = asTemp(in, ".next_" + name, ".zip", pluginsDir);
-    boolean clean = false;
     synchronized (this) {
       Plugin active = running.get(name);
       if (active != null) {
@@ -118,18 +129,13 @@
         runPlugin(name, jar, active);
         if (active == null) {
           log.info(String.format("Installed plugin %s", name));
-        } else {
-          clean = true;
         }
       } catch (PluginInstallException e) {
         jar.delete();
         throw e;
       }
-    }
 
-    if (clean) {
-      System.gc();
-      processPendingCleanups();
+      cleanInBackground();
     }
   }
 
@@ -159,7 +165,6 @@
   }
 
   public void disablePlugins(Set<String> names) {
-    boolean clean = false;
     synchronized (this) {
       for (String name : names) {
         Plugin active = running.get(name);
@@ -173,19 +178,17 @@
 
         active.stop();
         running.remove(name);
-        clean = true;
       }
-    }
-    if (clean) {
-      System.gc();
-      processPendingCleanups();
+      cleanInBackground();
     }
   }
 
   @Override
   public synchronized void start() {
     log.info("Loading plugins from " + pluginsDir.getAbsolutePath());
-    rescan(false);
+    srvInfoImpl.state = ServerInformation.State.STARTUP;
+    rescan();
+    srvInfoImpl.state = ServerInformation.State.RUNNING;
     if (scanner != null) {
       scanner.start();
     }
@@ -196,30 +199,57 @@
     if (scanner != null) {
       scanner.end();
     }
+    srvInfoImpl.state = ServerInformation.State.SHUTDOWN;
     synchronized (this) {
-      boolean clean = !running.isEmpty();
       for (Plugin p : running.values()) {
         p.stop();
       }
       running.clear();
       broken.clear();
-      if (clean) {
+      if (cleanupHandles.size() > running.size()) {
         System.gc();
         processPendingCleanups();
       }
     }
   }
 
-  public void rescan(boolean forceCleanup) {
-    if (rescanImp() || forceCleanup) {
-      System.gc();
-      processPendingCleanups();
+  public void reload(List<String> names)
+      throws InvalidPluginException, PluginInstallException {
+    synchronized (this) {
+      List<Plugin> reload = Lists.newArrayListWithCapacity(names.size());
+      List<String> bad = Lists.newArrayListWithExpectedSize(4);
+      for (String name : names) {
+        Plugin active = running.get(name);
+        if (active != null) {
+          reload.add(active);
+        } else {
+          bad.add(name);
+        }
+      }
+      if (!bad.isEmpty()) {
+        throw new InvalidPluginException(String.format(
+            "Plugin(s) \"%s\" not running",
+            Joiner.on("\", \"").join(bad)));
+      }
+
+      for (Plugin active : reload) {
+        String name = active.getName();
+        try {
+          log.info(String.format("Reloading plugin %s", name));
+          runPlugin(name, active.getSrcJar(), active);
+        } catch (PluginInstallException e) {
+          log.warn(String.format("Cannot reload plugin %s", name), e.getCause());
+          throw e;
+        }
+      }
+
+      cleanInBackground();
     }
   }
 
-  private synchronized boolean rescanImp() {
+  public synchronized void rescan() {
     List<File> jars = scanJarsInPluginsDirectory();
-    boolean clean = stopRemovedPlugins(jars);
+    stopRemovedPlugins(jars);
 
     for (File jar : jars) {
       String name = nameOf(jar);
@@ -241,14 +271,13 @@
         runPlugin(name, jar, active);
         if (active == null) {
           log.info(String.format("Loaded plugin %s", name));
-        } else {
-          clean = true;
         }
       } catch (PluginInstallException e) {
         log.warn(String.format("Cannot load plugin %s", name), e.getCause());
       }
     }
-    return clean;
+
+    cleanInBackground();
   }
 
   private void runPlugin(String name, File jar, Plugin oldPlugin)
@@ -278,7 +307,7 @@
     }
   }
 
-  private boolean stopRemovedPlugins(List<File> jars) {
+  private void stopRemovedPlugins(List<File> jars) {
     Set<String> unload = Sets.newHashSet(running.keySet());
     for (File jar : jars) {
       unload.remove(nameOf(jar));
@@ -287,15 +316,22 @@
       log.info(String.format("Unloading plugin %s", name));
       running.remove(name).stop();
     }
-    return !unload.isEmpty();
   }
 
-  private synchronized void processPendingCleanups() {
+  synchronized int processPendingCleanups() {
     CleanupHandle h;
     while ((h = (CleanupHandle) cleanupQueue.poll()) != null) {
       h.cleanup();
       cleanupHandles.remove(h);
     }
+    return Math.max(0, cleanupHandles.size() - running.size());
+  }
+
+  private void cleanInBackground() {
+    int cnt = Math.max(0, cleanupHandles.size() - running.size());
+    if (0 < cnt) {
+      cleaner.get().clean(cnt);
+    }
   }
 
   private static String nameOf(File jar) {
@@ -305,7 +341,7 @@
   }
 
   private Plugin loadPlugin(String name, File srcJar, FileSnapshot snapshot)
-      throws IOException, ClassNotFoundException {
+      throws IOException, ClassNotFoundException, InvalidPluginException {
     File tmp;
     FileInputStream in = new FileInputStream(srcJar);
     try {
@@ -318,13 +354,20 @@
     boolean keep = false;
     try {
       Manifest manifest = jarFile.getManifest();
+      Plugin.ApiType type = Plugin.getApiType(manifest);
       Attributes main = manifest.getMainAttributes();
       String sysName = main.getValue("Gerrit-Module");
       String sshName = main.getValue("Gerrit-SshModule");
       String httpName = main.getValue("Gerrit-HttpModule");
 
+      if (!Strings.isNullOrEmpty(sshName) && type != Plugin.ApiType.PLUGIN) {
+        throw new InvalidPluginException(String.format(
+            "Using Gerrit-SshModule requires Gerrit-ApiType: %s",
+            Plugin.ApiType.PLUGIN));
+      }
+
       URL[] urls = {tmp.toURI().toURL()};
-      ClassLoader parentLoader = PluginLoader.class.getClassLoader();
+      ClassLoader parentLoader = parentFor(type);
       ClassLoader pluginLoader = new URLClassLoader(urls, parentLoader);
       cleanupHandles.put(
           new CleanupHandle(tmp, jarFile, pluginLoader, cleanupQueue),
@@ -337,7 +380,7 @@
       return new Plugin(name,
           srcJar, snapshot,
           jarFile, manifest,
-          pluginLoader,
+          new File(dataDir, name), type, pluginLoader,
           sysModule, sshModule, httpModule);
     } finally {
       if (!keep) {
@@ -346,6 +389,18 @@
     }
   }
 
+  private static ClassLoader parentFor(Plugin.ApiType type)
+      throws InvalidPluginException {
+    switch (type) {
+      case EXTENSION:
+        return PluginName.class.getClassLoader();
+      case PLUGIN:
+        return PluginLoader.class.getClassLoader();
+      default:
+        throw new InvalidPluginException("Unsupported ApiType " + type);
+    }
+  }
+
   private static String tempNameFor(String name) {
     SimpleDateFormat fmt = new SimpleDateFormat("yyMMdd_HHmm");
     return "plugin_" + name + "_" + fmt.format(new Date()) + "_";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
index 0431ee1..ab7dc3c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
@@ -14,13 +14,19 @@
 
 package com.google.gerrit.server.plugins;
 
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
 import com.google.gerrit.lifecycle.LifecycleModule;
 
 public class PluginModule extends LifecycleModule {
   @Override
   protected void configure() {
+    bind(ServerInformationImpl.class);
+    bind(ServerInformation.class).to(ServerInformationImpl.class);
+
+    bind(PluginCleanerTask.class);
     bind(PluginGuiceEnvironment.class);
     bind(PluginLoader.class);
+
     bind(CopyConfigModule.class);
     listener().to(PluginLoader.class);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java
index b2e3fed..a484c5d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java
@@ -38,7 +38,7 @@
         }
       } catch (InterruptedException e) {
       }
-      loader.rescan(false);
+      loader.rescan();
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerInformationImpl.java
similarity index 65%
copy from gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerInformationImpl.java
index 204d777..c4a8900 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerInformationImpl.java
@@ -12,12 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.server.plugins;
 
-public class IncompleteUserInfoException extends Exception {
-  private static final long serialVersionUID = 1L;
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.inject.Singleton;
 
-  public IncompleteUserInfoException(final String userName, final String missingInfo) {
-    super("For the user \"" + userName + "\" " + missingInfo + " is not set.");
+@Singleton
+class ServerInformationImpl implements ServerInformation {
+  volatile State state = State.STARTUP;
+
+  @Override
+  public State getState() {
+    return state;
   }
 }
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 f232c5c..7387fd1 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
@@ -189,6 +189,7 @@
         || getRefControl().isOwner() // branch owner can abandon
         || getProjectControl().isOwner() // project owner can abandon
         || getCurrentUser().getCapabilities().canAdministrateServer() // site administers are god
+        || getRefControl().canAbandon() // user can abandon a specific ref
     ;
   }
 
@@ -292,13 +293,17 @@
     return false;
   }
 
+  public List<SubmitRecord> getSubmitRecords(ReviewDb db, PatchSet patchSet) {
+    return canSubmit(db, patchSet, null, false, true);
+  }
+
   public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet patchSet) {
-    return canSubmit(db, patchSet, null, false);
+    return canSubmit(db, patchSet, null, false, false);
   }
 
   public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet patchSet,
-      @Nullable ChangeData cd, boolean fastEvalLabels) {
-    if (change.getStatus().isClosed()) {
+      @Nullable ChangeData cd, boolean fastEvalLabels, boolean allowClosed) {
+    if (!allowClosed && change.getStatus().isClosed()) {
       SubmitRecord rec = new SubmitRecord();
       rec.status = SubmitRecord.Status.CLOSED;
       return Collections.singletonList(rec);
@@ -495,6 +500,9 @@
         } else if ("need".equals(status.name())) {
           lbl.status = SubmitRecord.Label.Status.NEED;
 
+        } else if ("may".equals(status.name())) {
+          lbl.status = SubmitRecord.Label.Status.MAY;
+
         } else if ("impossible".equals(status.name())) {
           lbl.status = SubmitRecord.Label.Status.IMPOSSIBLE;
 
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 65bdc1e..879f772 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
@@ -15,21 +15,24 @@
 package com.google.gerrit.server.project;
 
 import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.errors.ProjectCreationFailedException;
+import com.google.gerrit.extensions.events.NewProjectCreatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
 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.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.config.ProjectOwnerGroups;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 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.git.ReplicationQueue;
 import com.google.gerrit.server.git.RepositoryCaseMismatchException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -49,6 +52,8 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.Set;
 
 
@@ -64,28 +69,32 @@
   private final Set<AccountGroup.UUID> projectOwnerGroups;
   private final IdentifiedUser currentUser;
   private final GitRepositoryManager repoManager;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated referenceUpdated;
+  private final DynamicSet<NewProjectCreatedListener> createdListener;
   private final PersonIdent serverIdent;
   private CreateProjectArgs createProjectArgs;
   private ProjectCache projectCache;
-  private GroupCache groupCache;
+  private GroupBackend groupBackend;
   private MetaDataUpdate.User metaDataUpdateFactory;
 
   @Inject
   CreateProject(@ProjectOwnerGroups Set<AccountGroup.UUID> pOwnerGroups,
       IdentifiedUser identifiedUser, GitRepositoryManager gitRepoManager,
-      ReplicationQueue replicateq, ReviewDb db,
-      @GerritPersonIdent PersonIdent personIdent, final GroupCache groupCache,
-      final MetaDataUpdate.User metaDataUpdateFactory,
+      GitReferenceUpdated referenceUpdated,
+      DynamicSet<NewProjectCreatedListener> createdListener,
+      ReviewDb db,
+      @GerritPersonIdent PersonIdent personIdent, GroupBackend groupBackend,
+      MetaDataUpdate.User metaDataUpdateFactory,
       @Assisted CreateProjectArgs createPArgs, ProjectCache pCache) {
     this.projectOwnerGroups = pOwnerGroups;
     this.currentUser = identifiedUser;
     this.repoManager = gitRepoManager;
-    this.replication = replicateq;
+    this.referenceUpdated = referenceUpdated;
+    this.createdListener = createdListener;
     this.serverIdent = personIdent;
     this.createProjectArgs = createPArgs;
     this.projectCache = pCache;
-    this.groupCache = groupCache;
+    this.groupBackend = groupBackend;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
   }
 
@@ -95,10 +104,23 @@
     try {
       final String head =
           createProjectArgs.permissionsOnly ? GitRepositoryManager.REF_CONFIG
-              : createProjectArgs.branch;
+              : createProjectArgs.branch.get(0);
       final Repository repo = repoManager.createRepository(nameKey);
       try {
-        replication.replicateNewProject(nameKey, head);
+        NewProjectCreatedListener.Event event = new NewProjectCreatedListener.Event() {
+          @Override
+          public String getProjectName() {
+            return nameKey.get();
+          }
+
+          @Override
+          public String getHeadName() {
+            return head;
+          }
+        };
+        for (NewProjectCreatedListener l : createdListener) {
+          l.onNewProjectCreated(event);
+        }
 
         final RefUpdate u = repo.updateRef(Constants.HEAD);
         u.disableRefLog();
@@ -108,7 +130,7 @@
 
         if (!createProjectArgs.permissionsOnly
             && createProjectArgs.createEmptyCommit) {
-          createEmptyCommit(repo, nameKey, createProjectArgs.branch);
+          createEmptyCommits(repo, nameKey, createProjectArgs.branch);
         }
       } finally {
         repo.close();
@@ -130,10 +152,10 @@
         } finally {
           repo.close();
         }
-      } catch (RepositoryNotFoundException doesNotExist) {
+      } catch (IOException ioErr) {
         final String msg = "Cannot create " + nameKey;
         log.error(msg, err);
-        throw new ProjectCreationFailedException(msg, err);
+        throw new ProjectCreationFailedException(msg, ioErr);
       }
     } catch (Exception e) {
       final String msg = "Cannot create " + nameKey;
@@ -166,9 +188,9 @@
         final AccessSection all =
             config.getAccessSection(AccessSection.ALL, true);
         for (AccountGroup.UUID ownerId : createProjectArgs.ownerIds) {
-          AccountGroup accountGroup = groupCache.get(ownerId);
-          if (accountGroup != null) {
-            GroupReference group = config.resolve(accountGroup);
+          GroupDescription.Basic g = groupBackend.get(ownerId);
+          if (g != null) {
+            GroupReference group = config.resolve(GroupReference.forGroup(g));
             all.getPermission(Permission.OWNER, true).add(
                 new PermissionRule(group));
           }
@@ -186,7 +208,7 @@
     projectCache.onCreateProject(createProjectArgs.getProject());
     repoManager.setProjectDescription(createProjectArgs.getProject(),
         createProjectArgs.projectDescription);
-    replication.scheduleUpdate(createProjectArgs.getProject(),
+    referenceUpdated.fire(createProjectArgs.getProject(),
         GitRepositoryManager.REF_CONFIG);
   }
 
@@ -216,20 +238,32 @@
           new ArrayList<AccountGroup.UUID>(projectOwnerGroups);
     }
 
-    while (createProjectArgs.branch.startsWith("/")) {
-      createProjectArgs.branch = createProjectArgs.branch.substring(1);
+    List<String> transformedBranches = new ArrayList<String>();
+    if (createProjectArgs.branch == null ||
+        createProjectArgs.branch.isEmpty()) {
+      createProjectArgs.branch = Collections.singletonList(Constants.MASTER);
     }
-    if (!createProjectArgs.branch.startsWith(Constants.R_HEADS)) {
-      createProjectArgs.branch = Constants.R_HEADS + createProjectArgs.branch;
+    for (String branch : createProjectArgs.branch) {
+      while (branch.startsWith("/")) {
+        branch = branch.substring(1);
+      }
+      if (!branch.startsWith(Constants.R_HEADS)) {
+        branch = Constants.R_HEADS + branch;
+      }
+      if (!Repository.isValidRefName(branch)) {
+        throw new ProjectCreationFailedException(String.format(
+            "Branch \"%s\" is not a valid name.", branch));
+      }
+      if (!transformedBranches.contains(branch)) {
+        transformedBranches.add(branch);
+      }
     }
-    if (!Repository.isValidRefName(createProjectArgs.branch)) {
-      throw new ProjectCreationFailedException(String.format(
-          "Branch \"%s\" is not a valid name.", createProjectArgs.branch));
-    }
+    createProjectArgs.branch = transformedBranches;
   }
 
-  private void createEmptyCommit(final Repository repo,
-      final Project.NameKey project, final String ref) throws IOException {
+  private void createEmptyCommits(final Repository repo,
+      final Project.NameKey project, final List<String> refs)
+      throws IOException {
     ObjectInserter oi = repo.newObjectInserter();
     try {
       CommitBuilder cb = new CommitBuilder();
@@ -241,15 +275,18 @@
       ObjectId id = oi.insert(cb);
       oi.flush();
 
-      RefUpdate ru = repo.updateRef(Constants.HEAD);
-      ru.setNewObjectId(id);
-      final Result result = ru.update();
-      switch (result) {
-        case NEW:
-          replication.scheduleUpdate(project, ref);
-          break;
-        default: {
-          throw new IOException(result.name());
+      for (String ref : refs) {
+        RefUpdate ru = repo.updateRef(ref);
+        ru.setNewObjectId(id);
+        final Result result = ru.update();
+        switch (result) {
+          case NEW:
+            referenceUpdated.fire(project, ref);
+            break;
+          default: {
+            throw new IOException(String.format(
+              "Failed to create ref \"%s\": %s", ref, result.name()));
+          }
         }
       }
     } catch (IOException e) {
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 98adf85..2dee4f4 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
@@ -30,7 +30,7 @@
   public boolean contributorAgreements;
   public boolean signedOffBy;
   public boolean permissionsOnly;
-  public String branch;
+  public List<String> branch;
   public boolean contentMerge;
   public boolean changeIdRequired;
   public boolean createEmptyCommit;
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 716a5a8..8b4e000 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
@@ -19,6 +19,7 @@
 import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.StringUtil;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.util.TreeFormatter;
 import com.google.gson.reflect.TypeToken;
@@ -269,7 +270,7 @@
 
         if (info.description != null) {
           // We still want to list every project as one-liners, hence escaping \n.
-          stdout.print(" - " + info.description.replace("\n", "\\n"));
+          stdout.print(" - " + StringUtil.escapeString(info.description));
         }
         stdout.print('\n');
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
index 70f4013..7a111311 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
@@ -32,6 +32,12 @@
   /** Invalidate the cached information about the given project. */
   public void evict(Project p);
 
+  /**
+   * Remove information about the given project from the cache. It will no
+   * longer be returned from {@link #all()}.
+   */
+  void remove(Project p);
+
   /** @return sorted iteration of projects. */
   public abstract Iterable<Project.NameKey> all();
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 51d8fb2..cb18398 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
@@ -27,20 +28,24 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 import java.util.SortedSet;
-import java.util.TreeSet;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
 /** Cache of project information, including access rights. */
 @Singleton
 public class ProjectCacheImpl implements ProjectCache {
+  private static final Logger log = LoggerFactory
+      .getLogger(ProjectCacheImpl.class);
+
   private static final String CACHE_NAME = "projects";
   private static final String CACHE_LIST = "project_list";
 
@@ -48,13 +53,14 @@
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<Project.NameKey, ProjectState>> nameType =
-            new TypeLiteral<Cache<Project.NameKey, ProjectState>>() {};
-        core(nameType, CACHE_NAME).populateWith(Loader.class);
+        cache(CACHE_NAME, String.class, ProjectState.class)
+          .loader(Loader.class);
 
-        final TypeLiteral<Cache<ListKey, SortedSet<Project.NameKey>>> listType =
-            new TypeLiteral<Cache<ListKey, SortedSet<Project.NameKey>>>() {};
-        core(listType, CACHE_LIST).populateWith(Lister.class);
+        cache(CACHE_LIST,
+            ListKey.class,
+            new TypeLiteral<SortedSet<Project.NameKey>>() {})
+          .maximumWeight(1)
+          .loader(Lister.class);
 
         bind(ProjectCacheImpl.class);
         bind(ProjectCache.class).to(ProjectCacheImpl.class);
@@ -63,16 +69,16 @@
   }
 
   private final AllProjectsName allProjectsName;
-  private final Cache<Project.NameKey, ProjectState> byName;
-  private final Cache<ListKey,SortedSet<Project.NameKey>> list;
+  private final LoadingCache<String, ProjectState> byName;
+  private final LoadingCache<ListKey, SortedSet<Project.NameKey>> list;
   private final Lock listLock;
   private final ProjectCacheClock clock;
 
   @Inject
   ProjectCacheImpl(
       final AllProjectsName allProjectsName,
-      @Named(CACHE_NAME) final Cache<Project.NameKey, ProjectState> byName,
-      @Named(CACHE_LIST) final Cache<ListKey, SortedSet<Project.NameKey>> list,
+      @Named(CACHE_NAME) LoadingCache<String, ProjectState> byName,
+      @Named(CACHE_LIST) LoadingCache<ListKey, SortedSet<Project.NameKey>> list,
       ProjectCacheClock clock) {
     this.allProjectsName = allProjectsName;
     this.byName = byName;
@@ -99,29 +105,53 @@
    * @return the cached data; null if no such project exists.
    */
   public ProjectState get(final Project.NameKey projectName) {
-    ProjectState state = byName.get(projectName);
-    if (state != null && state.needsRefresh(clock.read())) {
-      byName.remove(projectName);
-      state = byName.get(projectName);
+    if (projectName == null) {
+      return null;
     }
-    return state;
+    try {
+      ProjectState state = byName.get(projectName.get());
+      if (state != null && state.needsRefresh(clock.read())) {
+        byName.invalidate(projectName.get());
+        state = byName.get(projectName.get());
+      }
+      return state;
+    } catch (ExecutionException e) {
+      log.warn(String.format("Cannot read project %s", projectName.get()), e);
+      return null;
+    }
   }
 
   /** Invalidate the cached information about the given project. */
   public void evict(final Project p) {
     if (p != null) {
-      byName.remove(p.getNameKey());
+      byName.invalidate(p.getNameKey().get());
     }
   }
 
   @Override
+  public void remove(final Project p) {
+    listLock.lock();
+    try {
+      SortedSet<Project.NameKey> n = Sets.newTreeSet(list.get(ListKey.ALL));
+      n.remove(p.getNameKey());
+      list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n));
+    } catch (ExecutionException e) {
+      log.warn("Cannot list avaliable projects", e);
+    } finally {
+      listLock.unlock();
+    }
+    evict(p);
+  }
+
+  @Override
   public void onCreateProject(Project.NameKey newProjectName) {
     listLock.lock();
     try {
-      SortedSet<Project.NameKey> n = list.get(ListKey.ALL);
-      n = new TreeSet<Project.NameKey>(n);
+      SortedSet<Project.NameKey> n = Sets.newTreeSet(list.get(ListKey.ALL));
       n.add(newProjectName);
       list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n));
+    } catch (ExecutionException e) {
+      log.warn("Cannot list avaliable projects", e);
     } finally {
       listLock.unlock();
     }
@@ -129,18 +159,28 @@
 
   @Override
   public Iterable<Project.NameKey> all() {
-    return list.get(ListKey.ALL);
+    try {
+      return list.get(ListKey.ALL);
+    } catch (ExecutionException e) {
+      log.warn("Cannot list available projects", e);
+      return Collections.emptyList();
+    }
   }
 
   @Override
   public Iterable<Project.NameKey> byName(final String pfx) {
+    final Iterable<Project.NameKey> src;
+    try {
+      src = list.get(ListKey.ALL).tailSet(new Project.NameKey(pfx));
+    } catch (ExecutionException e) {
+      return Collections.emptyList();
+    }
     return new Iterable<Project.NameKey>() {
       @Override
       public Iterator<Project.NameKey> iterator() {
         return new Iterator<Project.NameKey>() {
+          private Iterator<Project.NameKey> itr = src.iterator();
           private Project.NameKey next;
-          private Iterator<Project.NameKey> itr =
-              list.get(ListKey.ALL).tailSet(new Project.NameKey(pfx)).iterator();
 
           @Override
           public boolean hasNext() {
@@ -182,7 +222,7 @@
     };
   }
 
-  static class Loader extends EntryCreator<Project.NameKey, ProjectState> {
+  static class Loader extends CacheLoader<String, ProjectState> {
     private final ProjectState.Factory projectStateFactory;
     private final GitRepositoryManager mgr;
 
@@ -193,19 +233,15 @@
     }
 
     @Override
-    public ProjectState createEntry(Project.NameKey key) throws Exception {
+    public ProjectState load(String projectName) throws Exception {
+      Project.NameKey key = new Project.NameKey(projectName);
+      Repository git = mgr.openRepository(key);
       try {
-        Repository git = mgr.openRepository(key);
-        try {
-          final ProjectConfig cfg = new ProjectConfig(key);
-          cfg.load(git);
-          return projectStateFactory.create(cfg);
-        } finally {
-          git.close();
-        }
-
-      } catch (RepositoryNotFoundException notFound) {
-        return null;
+        ProjectConfig cfg = new ProjectConfig(key);
+        cfg.load(git);
+        return projectStateFactory.create(cfg);
+      } finally {
+        git.close();
       }
     }
   }
@@ -217,7 +253,7 @@
     }
   }
 
-  static class Lister extends EntryCreator<ListKey, SortedSet<Project.NameKey>> {
+  static class Lister extends CacheLoader<ListKey, SortedSet<Project.NameKey>> {
     private final GitRepositoryManager mgr;
 
     @Inject
@@ -226,7 +262,7 @@
     }
 
     @Override
-    public SortedSet<Project.NameKey> createEntry(ListKey key) throws Exception {
+    public SortedSet<Project.NameKey> load(ListKey key) throws Exception {
       return mgr.list();
     }
   }
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 85b2251..513f1b1 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
@@ -29,7 +29,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.ReplicationUser;
+import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GitReceivePackGroups;
 import com.google.gerrit.server.config.GitUploadPackGroups;
@@ -38,6 +38,7 @@
 import com.google.inject.assistedinject.Assisted;
 
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -185,7 +186,7 @@
 
   /** Can this user see this project exists? */
   public boolean isVisible() {
-    return (visibleForReplication()
+    return (user instanceof InternalUser
         || canPerformOnAnyRef(Permission.READ)) && !isHidden();
   }
 
@@ -196,14 +197,12 @@
 
   /** Can this user see all the refs in this projects? */
   public boolean allRefsAreVisible() {
-    return visibleForReplication()
-        || canPerformOnAllRefs(Permission.READ);
+    return allRefsAreVisibleExcept(Collections.<String> emptySet());
   }
 
-  /** Is this project completely visible for replication? */
-  boolean visibleForReplication() {
-    return user instanceof ReplicationUser
-        && ((ReplicationUser) user).isEverythingVisible();
+  public boolean allRefsAreVisibleExcept(Set<String> except) {
+    return user instanceof InternalUser
+        || canPerformOnAllRefs(Permission.READ, except);
   }
 
   /** Is this user a project owner? Ownership does not imply {@link #isVisible()} */
@@ -353,7 +352,7 @@
     return false;
   }
 
-  private boolean canPerformOnAllRefs(String permission) {
+  private boolean canPerformOnAllRefs(String permission, Set<String> except) {
     boolean canPerform = false;
     Set<String> patterns = allRefPatterns(permission);
     if (patterns.contains(AccessSection.ALL)) {
@@ -364,6 +363,8 @@
       for (final String pattern : patterns) {
         if (controlForRef(pattern).canPerform(permission)) {
           canPerform = true;
+        } else if (except.contains(pattern)) {
+          continue;
         } else {
           return false;
         }
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 db370e0..2f99271 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
@@ -23,6 +23,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 
 import dk.brics.automaton.RegExp;
@@ -101,7 +102,7 @@
 
   /** Can this user see this reference exists? */
   public boolean isVisible() {
-    return (projectControl.visibleForReplication() || canPerform(Permission.READ))
+    return (getCurrentUser() instanceof InternalUser || canPerform(Permission.READ))
         && canRead();
   }
 
@@ -309,6 +310,11 @@
     return canPerform(Permission.FORGE_SERVER);
   }
 
+  /** @return true if this user can abandon a change for this ref */
+  public boolean canAbandon() {
+    return canPerform(Permission.ABANDON);
+  }
+
   /** All value ranges of any allowed label permission. */
   public List<PermissionRange> getLabelRanges() {
     List<PermissionRange> r = new ArrayList<PermissionRange>();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
index 40d4290..686ff59 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
@@ -14,14 +14,13 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.cache.Cache;
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.util.MostSpecificComparator;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Singleton;
-import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
 import java.util.Arrays;
@@ -38,9 +37,7 @@
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<EntryKey, EntryVal>> type =
-            new TypeLiteral<Cache<EntryKey, EntryVal>>() {};
-        core(type, CACHE_NAME);
+        cache(CACHE_NAME, EntryKey.class, EntryVal.class);
         bind(SectionSortCache.class);
       }
     };
@@ -60,7 +57,7 @@
     }
 
     EntryKey key = new EntryKey(ref, sections);
-    EntryVal val = cache.get(key);
+    EntryVal val = cache.getIfPresent(key);
     if (val != null) {
       int[] srcIdx = val.order;
       if (srcIdx != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
index 77e082d..b0f12b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
@@ -43,6 +43,12 @@
  * @type <T> type of object the predicate can evaluate in memory.
  */
 public abstract class Predicate<T> {
+  /** A predicate that matches any input, always, with no cost. */
+  @SuppressWarnings("unchecked")
+  public static <T> Predicate<T> any() {
+    return (Predicate<T>) Any.INSTANCE;
+  }
+
   /** Combine the passed predicates into a single AND node. */
   public static <T> Predicate<T> and(final Predicate<T>... that) {
     if (that.length == 1) {
@@ -120,4 +126,36 @@
 
   @Override
   public abstract boolean equals(Object other);
+
+  private static class Any<T> extends Predicate<T> {
+    private static final Any<Object> INSTANCE = new Any<Object>();
+
+    private Any() {
+    }
+
+    @Override
+    public Predicate<T> copy(Collection<? extends Predicate<T>> children) {
+      return this;
+    }
+
+    @Override
+    public boolean match(T object) {
+      return true;
+    }
+
+    @Override
+    public int getCost() {
+      return 0;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      return other == this;
+    }
+  }
 }
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 db3470e..d6762db 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
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
@@ -142,7 +143,14 @@
         return null;
       }
 
-      PatchList p = cache.get(c, ps);
+      PatchList p;
+      try {
+        p = cache.get(c, ps);
+      } catch (PatchListNotAvailableException e) {
+        currentFiles = new String[0];
+        return currentFiles;
+      }
+
       List<String> r = new ArrayList<String>(p.getPatches().size());
       for (PatchListEntry e : p.getPatches()) {
         if (Patch.COMMIT_MSG.equals(e.getNewName())) {
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 8c1157e..ddc4c28 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
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
@@ -26,7 +27,8 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.CapabilityControl;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupBackends;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchListCache;
@@ -105,7 +107,7 @@
     final ChangeControl.Factory changeControlFactory;
     final ChangeControl.GenericFactory changeControlGenericFactory;
     final AccountResolver accountResolver;
-    final GroupCache groupCache;
+    final GroupBackend groupBackend;
     final ApprovalTypes approvalTypes;
     final AllProjectsName allProjectsName;
     final PatchListCache patchListCache;
@@ -119,7 +121,8 @@
         CapabilityControl.Factory capabilityControlFactory,
         ChangeControl.Factory changeControlFactory,
         ChangeControl.GenericFactory changeControlGenericFactory,
-        AccountResolver accountResolver, GroupCache groupCache,
+        AccountResolver accountResolver,
+        GroupBackend groupBackend,
         ApprovalTypes approvalTypes,
         AllProjectsName allProjectsName,
         PatchListCache patchListCache,
@@ -132,7 +135,7 @@
       this.changeControlFactory = changeControlFactory;
       this.changeControlGenericFactory = changeControlGenericFactory;
       this.accountResolver = accountResolver;
-      this.groupCache = groupCache;
+      this.groupBackend = groupBackend;
       this.approvalTypes = approvalTypes;
       this.allProjectsName = allProjectsName;
       this.patchListCache = patchListCache;
@@ -367,18 +370,11 @@
 
     // If its not an account, maybe its a group?
     //
-    AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(who));
-    if (g != null) {
-      return visibleto(new SingleGroupUser(args.capabilityControlFactory,
-          g.getGroupUUID()));
-    }
-
-    Collection<AccountGroup> matches =
-        args.groupCache.get(new AccountGroup.ExternalNameKey(who));
-    if (matches != null && !matches.isEmpty()) {
+    Collection<GroupReference> suggestions = args.groupBackend.suggest(who);
+    if (!suggestions.isEmpty()) {
       HashSet<AccountGroup.UUID> ids = new HashSet<AccountGroup.UUID>();
-      for (AccountGroup group : matches) {
-        ids.add(group.getGroupUUID());
+      for (GroupReference ref : suggestions) {
+        ids.add(ref.getUUID());
       }
       return visibleto(new SingleGroupUser(args.capabilityControlFactory, ids));
     }
@@ -410,11 +406,11 @@
   @Operator
   public Predicate<ChangeData> ownerin(String group)
       throws QueryParseException {
-    AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(group));
+    GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
     if (g == null) {
       throw error("Group " + group + " not found");
     }
-    return new OwnerinPredicate(args.dbProvider, args.userFactory, g.getGroupUUID());
+    return new OwnerinPredicate(args.dbProvider, args.userFactory, g.getUUID());
   }
 
   @Operator
@@ -431,11 +427,11 @@
   @Operator
   public Predicate<ChangeData> reviewerin(String group)
       throws QueryParseException {
-    AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(group));
+    GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
     if (g == null) {
       throw error("Group " + group + " not found");
     }
-    return new ReviewerinPredicate(args.dbProvider, args.userFactory, g.getGroupUUID());
+    return new ReviewerinPredicate(args.dbProvider, args.userFactory, g.getUUID());
   }
 
   @Operator
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
index bf2e4cc..34ec2f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
@@ -119,50 +119,23 @@
     return a.getValue().compareTo(b.getValue()) >= 0 ? a : b;
   }
 
-  @Rewrite("status:open P=(project:*) B=(branch:*) S=(sortkey_after:*) L=(limit:*)")
-  public Predicate<ChangeData> r05_byBranchOpenPrev(
+  @Rewrite("status:open P=(project:*) B=(branch:*)")
+  public Predicate<ChangeData> r05_byBranchOpen(
       @Named("P") final ProjectPredicate p,
-      @Named("B") final BranchPredicate b,
-      @Named("S") final SortKeyPredicate.After s,
-      @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(500, s.getValue(), l.intValue()) {
+      @Named("B") final BranchPredicate b) {
+    return new ChangeSource(500) {
       @Override
-      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+      ResultSet<Change> scan(ChangeAccess a)
           throws OrmException {
-        return a.byBranchOpenAll(new Branch.NameKey(p.getValueKey(), b
-            .getValue()));
+        return a.byBranchOpenAll(
+            new Branch.NameKey(p.getValueKey(), b.getValue()));
       }
 
       @Override
       public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus().isOpen() //
-            && p.match(cd) //
-            && b.match(cd) //
-            && s.match(cd);
-      }
-    };
-  }
-
-  @Rewrite("status:open P=(project:*) B=(branch:*) S=(sortkey_before:*) L=(limit:*)")
-  public Predicate<ChangeData> r05_byBranchOpenNext(
-      @Named("P") final ProjectPredicate p,
-      @Named("B") final BranchPredicate b,
-      @Named("S") final SortKeyPredicate.Before s,
-      @Named("L") final IntPredicate<ChangeData> l) {
-    return new PaginatedSource(500, s.getValue(), l.intValue()) {
-      @Override
-      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
-          throws OrmException {
-        return a.byBranchOpenAll(new Branch.NameKey(p.getValueKey(), b
-            .getValue()));
-      }
-
-      @Override
-      public boolean match(ChangeData cd) throws OrmException {
-        return cd.change(dbProvider).getStatus().isOpen() //
-            && p.match(cd) //
-            && b.match(cd) //
-            && s.match(cd);
+        return cd.change(dbProvider).getStatus().isOpen()
+            && p.match(cd)
+            && b.match(cd);
       }
     };
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ListChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ListChanges.java
index 6f9094a..ba4a74e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ListChanges.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ListChanges.java
@@ -22,10 +22,12 @@
 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.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.events.AccountAttribute;
 import com.google.gerrit.server.project.ChangeControl;
@@ -43,6 +45,7 @@
 import java.sql.Timestamp;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 
@@ -200,11 +203,15 @@
     out._number = in.getId().get();
     out._sortkey = in.getSortKey();
     out.starred = user.getStarredChanges().contains(in.getId()) ? true : null;
+    out.reviewed = isChangeReviewed(cd) ? true : null;
     out.labels = labelsFor(cd);
     return out;
   }
 
   private AccountAttribute asAccountAttribute(Account.Id user) {
+    if (user == null) {
+      return null;
+    }
     AccountAttribute a = accounts.get(user);
     if (a == null) {
       a = new AccountAttribute();
@@ -226,7 +233,7 @@
 
     PatchSet ps = cd.currentPatchSet(db);
     Map<String, LabelInfo> labels = Maps.newLinkedHashMap();
-    for (SubmitRecord rec : ctl.canSubmit(db.get(), ps, cd, true)) {
+    for (SubmitRecord rec : ctl.canSubmit(db.get(), ps, cd, true, false)) {
       if (rec.labels == null) {
         continue;
       }
@@ -243,6 +250,7 @@
               n.rejected = asAccountAttribute(r.appliedBy);
               break;
           }
+          n.optional = n._status == SubmitRecord.Label.Status.MAY ? true : null;
           labels.put(r.label, n);
         }
       }
@@ -287,6 +295,37 @@
     return labels;
   }
 
+  private boolean isChangeReviewed(ChangeData cd) throws OrmException {
+    if (user instanceof IdentifiedUser) {
+      PatchSet.Id patchSetId = cd.currentPatchSet(db).getId();
+      List<ChangeMessage> messages =
+          db.get().changeMessages().byPatchSet(patchSetId).toList();
+
+      if (messages.isEmpty()) {
+        return false;
+      }
+
+      // Sort messages to let the most recent ones at the beginning.
+      Collections.sort(messages, new Comparator<ChangeMessage>() {
+        @Override
+        public int compare(ChangeMessage a, ChangeMessage b) {
+          return b.getWrittenOn().compareTo(a.getWrittenOn());
+        }
+      });
+
+      Account.Id currentUserId = ((IdentifiedUser) user).getAccountId();
+      Account.Id changeOwnerId = cd.change(db).getOwner();
+      for (ChangeMessage cm : messages) {
+        if (currentUserId.equals(cm.getAuthor())) {
+          return true;
+        } else if (changeOwnerId.equals(cm.getAuthor())) {
+          return false;
+        }
+      }
+    }
+    return false;
+  }
+
   static class ChangeInfo {
     String project;
     String branch;
@@ -297,6 +336,7 @@
     Timestamp created;
     Timestamp updated;
     Boolean starred;
+    Boolean reviewed;
 
     String _sortkey;
     int _number;
@@ -314,5 +354,6 @@
     AccountAttribute recommended;
     AccountAttribute disliked;
     Short value;
+    Boolean optional;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
index cdce217..270b2e7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
@@ -27,15 +27,15 @@
 import java.util.Collections;
 import java.util.Set;
 
-final class SingleGroupUser extends CurrentUser {
+public final class SingleGroupUser extends CurrentUser {
   private final GroupMembership groups;
 
-  SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
+  public SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
       AccountGroup.UUID groupId) {
     this(capabilityControlFactory, Collections.singleton(groupId));
   }
 
-  SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
+  public SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
       Set<AccountGroup.UUID> groups) {
     super(capabilityControlFactory, AccessPath.UNKNOWN);
     this.groups = new ListGroupMembership(groups);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
index 2a98e96..cc48019 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
@@ -18,7 +18,7 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
index d8809da..ff6dc6c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -32,9 +32,9 @@
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.NoReplication;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gwtorm.jdbc.JdbcExecutor;
 import com.google.gwtorm.jdbc.JdbcSchema;
@@ -161,7 +161,7 @@
     anonymous =
         newGroup(c, "Anonymous Users", AccountGroup.ANONYMOUS_USERS);
     anonymous.setDescription("Any user, signed-in or not");
-    anonymous.setOwnerGroupId(admin.getId());
+    anonymous.setOwnerGroupUUID(admin.getGroupUUID());
     anonymous.setType(AccountGroup.Type.SYSTEM);
     c.accountGroups().insert(Collections.singleton(anonymous));
     c.accountGroupNames().insert(
@@ -170,7 +170,7 @@
     registered =
         newGroup(c, "Registered Users", AccountGroup.REGISTERED_USERS);
     registered.setDescription("Any signed-in user");
-    registered.setOwnerGroupId(admin.getId());
+    registered.setOwnerGroupUUID(admin.getGroupUUID());
     registered.setType(AccountGroup.Type.SYSTEM);
     c.accountGroups().insert(Collections.singleton(registered));
     c.accountGroupNames().insert(
@@ -178,7 +178,7 @@
 
     final AccountGroup batchUsers = newGroup(c, "Non-Interactive Users", null);
     batchUsers.setDescription("Users who perform batch actions on Gerrit");
-    batchUsers.setOwnerGroupId(admin.getId());
+    batchUsers.setOwnerGroupUUID(admin.getGroupUUID());
     batchUsers.setType(AccountGroup.Type.INTERNAL);
     c.accountGroups().insert(Collections.singleton(batchUsers));
     c.accountGroupNames().insert(
@@ -186,7 +186,7 @@
 
     owners = newGroup(c, "Project Owners", AccountGroup.PROJECT_OWNERS);
     owners.setDescription("Any owner of the project");
-    owners.setOwnerGroupId(admin.getId());
+    owners.setOwnerGroupUUID(admin.getGroupUUID());
     owners.setType(AccountGroup.Type.SYSTEM);
     c.accountGroups().insert(Collections.singleton(owners));
     c.accountGroupNames().insert(
@@ -219,7 +219,8 @@
       }
     }
     try {
-      MetaDataUpdate md = new MetaDataUpdate(new NoReplication(), allProjectsName, git);
+      MetaDataUpdate md =
+          new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjectsName, git);
       md.getCommitBuilder().setAuthor(serverUser);
       md.getCommitBuilder().setCommitter(serverUser);
 
@@ -251,8 +252,9 @@
       all.getPermission(Permission.FORGE_AUTHOR, true) //
           .add(rule(config, registered));
 
-      meta.getPermission(Permission.READ, true) //
-          .add(rule(config, owners));
+      Permission metaReadPermission = meta.getPermission(Permission.READ, true);
+      metaReadPermission.setExclusiveGroup(true);
+      metaReadPermission.add(rule(config, owners));
 
       md.setMessage("Initialized Gerrit Code Review " + Version.getVersion());
       if (!config.commit(md)) {
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 4779e10..0a34b44 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_66> C = Schema_66.class;
+  public static final Class<Schema_69> C = Schema_69.class;
 
   public static class Module extends AbstractModule {
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
index 75d8a39..133b856 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
 import com.google.gerrit.reviewdb.server.ReviewDb;
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 d49b34e..54ee9ab 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
@@ -37,9 +37,9 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.GroupUUID;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.NoReplication;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
@@ -158,13 +158,15 @@
         // inheritable permissions. For example 'All-Projects'.
         try {
           git = mgr.createRepository(nameKey);
-        } catch (RepositoryNotFoundException err) {
+        } catch (IOException err) {
           throw new OrmException("Cannot create repository " + name, err);
         }
+      } catch (IOException e) {
+        throw new OrmException(e);
       }
       try {
         MetaDataUpdate md =
-            new MetaDataUpdate(new NoReplication(), nameKey, git);
+            new MetaDataUpdate(GitReferenceUpdated.DISABLED, nameKey, git);
         md.getCommitBuilder().setAuthor(serverUser);
         md.getCommitBuilder().setCommitter(serverUser);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
index 2247fdb..4699a00 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
@@ -26,9 +26,9 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.NoReplication;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -81,7 +81,8 @@
     try {
       Repository git = mgr.openRepository(allProjects);
       try {
-        MetaDataUpdate md = new MetaDataUpdate(new NoReplication(), allProjects, git);
+        MetaDataUpdate md =
+            new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
         md.getCommitBuilder().setAuthor(serverUser);
         md.getCommitBuilder().setCommitter(serverUser);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java
index 26890a3..127f9c3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java
@@ -23,9 +23,9 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.NoReplication;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
@@ -33,7 +33,6 @@
 import com.google.inject.Provider;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 
@@ -88,12 +87,12 @@
     Repository git;
     try {
       git = mgr.openRepository(allProjects);
-    } catch (RepositoryNotFoundException e) {
+    } catch (IOException e) {
       throw new OrmException(e);
     }
     try {
       MetaDataUpdate md =
-          new MetaDataUpdate(new NoReplication(), allProjects, git);
+          new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
       md.getCommitBuilder().setAuthor(serverUser);
       md.getCommitBuilder().setCommitter(serverUser);
 
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 d6567a9..3383364 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
@@ -34,17 +34,17 @@
 import com.google.gerrit.server.account.GroupUUID;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.NoReplication;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
@@ -87,12 +87,12 @@
     Repository git;
     try {
       git = mgr.openRepository(allProjects);
-    } catch (RepositoryNotFoundException e) {
+    } catch (IOException e) {
       throw new OrmException(e);
     }
     try {
       MetaDataUpdate md =
-          new MetaDataUpdate(new NoReplication(), allProjects, git);
+          new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
       ProjectConfig config = ProjectConfig.read(md);
       Map<Integer, ContributorAgreement> agreements = getAgreementToAdd(db, config);
       if (agreements.isEmpty()) {
@@ -101,15 +101,15 @@
       ui.message("Moved contributor agreements to project.config");
 
       // Create the auto verify groups.
-      List<AccountGroup.Id> adminGroupIds = getAdministrateServerGroups(db, config);
+      List<AccountGroup.UUID> adminGroupUUIDs = getAdministrateServerGroups(db, config);
       for (ContributorAgreement agreement : agreements.values()) {
         if (agreement.getAutoVerify() != null) {
-          getOrCreateGroupForIndividuals(db, config, adminGroupIds, agreement);
+          getOrCreateGroupForIndividuals(db, config, adminGroupUUIDs, agreement);
         }
       }
 
       // Scan AccountAgreement
-      long minTime = addAccountAgreements(db, config, adminGroupIds, agreements);
+      long minTime = addAccountAgreements(db, config, adminGroupUUIDs, agreements);
 
       ProjectConfig base = ProjectConfig.read(md, null);
       for (ContributorAgreement agreement : agreements.values()) {
@@ -257,14 +257,14 @@
   }
 
   private AccountGroup createGroup(ReviewDb db, String groupName,
-      AccountGroup.Id adminGroupId, String description)
+      AccountGroup.UUID adminGroupUUID, String description)
           throws OrmException {
     final AccountGroup.Id groupId =
         new AccountGroup.Id(db.nextAccountGroupId());
     final AccountGroup.NameKey nameKey = new AccountGroup.NameKey(groupName);
     final AccountGroup.UUID uuid = GroupUUID.make(groupName, serverUser);
     final AccountGroup group = new AccountGroup(nameKey, groupId, uuid);
-    group.setOwnerGroupId(adminGroupId);
+    group.setOwnerGroupUUID(adminGroupUUID);
     group.setDescription(description);
     final AccountGroupName gn = new AccountGroupName(group);
     // first insert the group name to validate that the group name hasn't
@@ -274,21 +274,17 @@
     return group;
   }
 
-  private List<AccountGroup.Id> getAdministrateServerGroups(
-      ReviewDb db, ProjectConfig cfg) throws OrmException {
+  private List<AccountGroup.UUID> getAdministrateServerGroups(
+      ReviewDb db, ProjectConfig cfg) {
     List<PermissionRule> rules = cfg.getAccessSection(AccessSection.GLOBAL_CAPABILITIES)
        .getPermission(GlobalCapability.ADMINISTRATE_SERVER)
        .getRules();
 
-    List<AccountGroup.Id> groups =
+    List<AccountGroup.UUID> groups =
         Lists.newArrayListWithExpectedSize(rules.size());
     for (PermissionRule rule : rules) {
       if (rule.getAction() == Action.ALLOW) {
-        groups.add(db.accountGroups()
-            .byUUID(rule.getGroup().getUUID())
-            .toList()
-            .get(0)
-            .getId());
+        groups.add(rule.getGroup().getUUID());
       }
     }
     if (groups.isEmpty()) {
@@ -299,7 +295,7 @@
   }
 
   private GroupReference getOrCreateGroupForIndividuals(ReviewDb db,
-      ProjectConfig config, List<AccountGroup.Id> adminGroupIds,
+      ProjectConfig config, List<AccountGroup.UUID> adminGroupUUIDs,
       ContributorAgreement agreement)
           throws OrmException {
     if (!agreement.getAccepted().isEmpty()) {
@@ -317,12 +313,12 @@
             "account group name exists but account group does not: " + name);
       }
 
-      if (!adminGroupIds.contains(ag.getOwnerGroupId())) {
+      if (!adminGroupUUIDs.contains(ag.getOwnerGroupUUID())) {
         throw new IllegalStateException(
             "individual group exists with non admin owner group: " + name);
       }
     } else {
-      ag = createGroup(db, name, adminGroupIds.get(0),
+      ag = createGroup(db, name, adminGroupUUIDs.get(0),
           String.format("Users who have accepted the %s CLA", agreement.getName()));
     }
     GroupReference group = config.resolve(ag);
@@ -344,7 +340,7 @@
   }
 
   private long addAccountAgreements(ReviewDb db, ProjectConfig config,
-      List<AccountGroup.Id> adminGroupIds,
+      List<AccountGroup.UUID> adminGroupUUIDs,
       Map<Integer, ContributorAgreement> agreements)
           throws SQLException, OrmException {
     Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
@@ -373,7 +369,7 @@
 
           // Enter Agreement
           GroupReference individualGroup =
-              getOrCreateGroupForIndividuals(db, config, adminGroupIds, agreement);
+              getOrCreateGroupForIndividuals(db, config, adminGroupUUIDs, agreement);
           AccountGroup.Id groupId = db.accountGroups()
               .byUUID(individualGroup.getUUID())
               .toList()
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
new file mode 100644
index 0000000..bec2f3f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_67.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+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;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+public class Schema_67 extends SchemaVersion {
+
+  @Inject
+  Schema_67(Provider<Schema_66> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui)
+      throws OrmException, SQLException {
+    ui.message("Update ownerGroupId to ownerGroupUUID");
+
+    // Scan all AccountGroup, and find the ones that need the owner_group_id
+    // migrated to owner_group_uuid.
+    Map<AccountGroup.Id, AccountGroup.Id> idMap = Maps.newHashMap();
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      ResultSet rs = stmt.executeQuery(
+          "SELECT group_id, owner_group_id FROM account_groups"
+          + " WHERE owner_group_uuid is NULL or owner_group_uuid =''");
+      try {
+        while (rs.next()) {
+          AccountGroup.Id groupId = new AccountGroup.Id(rs.getInt(1));
+          AccountGroup.Id ownerId = new AccountGroup.Id(rs.getInt(2));
+          idMap.put(groupId, ownerId);
+        }
+      } finally {
+        rs.close();
+      }
+    } finally {
+      stmt.close();
+    }
+
+    // Lookup up all groups by ID.
+    Set<AccountGroup.Id> all =
+        Sets.newHashSet(Iterables.concat(idMap.keySet(), idMap.values()));
+    Map<AccountGroup.Id, AccountGroup> groups = Maps.newHashMap();
+    com.google.gwtorm.server.ResultSet<AccountGroup> rs =
+        db.accountGroups().get(all);
+    try {
+      for (AccountGroup group : rs) {
+        groups.put(group.getId(), group);
+      }
+    } finally {
+      rs.close();
+    }
+
+    // Update the ownerGroupUUID.
+    List<AccountGroup> toUpdate = Lists.newArrayListWithCapacity(idMap.size());
+    for (Entry<AccountGroup.Id, AccountGroup.Id> entry : idMap.entrySet()) {
+      AccountGroup group = groups.get(entry.getKey());
+      AccountGroup owner = groups.get(entry.getValue());
+      group.setOwnerGroupUUID(owner.getGroupUUID());
+      toUpdate.add(group);
+    }
+
+    db.accountGroups().update(toUpdate);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_68.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_68.java
new file mode 100644
index 0000000..4dc2b6e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_68.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class Schema_68 extends SchemaVersion {
+  @Inject
+  Schema_68(Provider<Schema_67> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(final ReviewDb db, final UpdateUI ui)
+      throws SQLException {
+    final Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      stmt.execute("CREATE INDEX submodule_subscription_access_bySubscription"
+          + " ON submodule_subscriptions (submodule_project_name, submodule_branch_name)");
+    } catch (SQLException e) {
+      // the index creation might have failed because the index exists already,
+      // in this case the exception can be safely ignored,
+      // but there are also other possible reasons for an exception here that
+      // should not be ignored,
+      // -> ask the user whether to ignore this exception or not
+      ui.message("warning: Cannot create index for submodule subscriptions");
+      ui.message(e.getMessage());
+
+      if (ui.isBatch()) {
+        ui.message("you may ignore this warning when running in interactive mode");
+        throw e;
+      } else {
+        final boolean answer = ui.yesno(false, "Ignore warning and proceed with schema upgrade");
+        if (!answer) {
+          throw e;
+        }
+      }
+    } finally {
+      stmt.close();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_69.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_69.java
new file mode 100644
index 0000000..3d6b93a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_69.java
@@ -0,0 +1,230 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+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.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.naming.NamingException;
+import javax.naming.ldap.LdapName;
+
+public class Schema_69 extends SchemaVersion {
+  private final GitRepositoryManager mgr;
+  private final PersonIdent serverUser;
+
+  @Inject
+  Schema_69(Provider<Schema_68> prior,
+      GitRepositoryManager mgr,
+      @GerritPersonIdent PersonIdent serverUser) {
+    super(prior);
+    this.mgr = mgr;
+    this.serverUser = serverUser;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui)
+      throws OrmException, SQLException {
+
+    // Find all groups that have an LDAP type.
+    Map<AccountGroup.UUID, GroupReference> ldapUUIDMap = Maps.newHashMap();
+    Set<AccountGroup.UUID> toResolve = Sets.newHashSet();
+    List<AccountGroup.Id> toDelete = Lists.newArrayList();
+    List<AccountGroup.NameKey> namesToDelete = Lists.newArrayList();
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      ResultSet rs = stmt.executeQuery(
+          "SELECT group_id, group_uuid, external_name, name FROM account_groups"
+          + " WHERE group_type ='LDAP'");
+      try {
+        while (rs.next()) {
+          AccountGroup.Id groupId = new AccountGroup.Id(rs.getInt(1));
+          AccountGroup.UUID groupUUID = new AccountGroup.UUID(rs.getString(2));
+          AccountGroup.NameKey name = new AccountGroup.NameKey(rs.getString(4));
+          String dn = rs.getString(3);
+
+          if (isNullOrEmpty(dn)) {
+            // The LDAP group does not have a DN. Determine if the UUID is used.
+            toResolve.add(groupUUID);
+          } else {
+            toDelete.add(groupId);
+            namesToDelete.add(name);
+            GroupReference ref = groupReference(dn);
+            ldapUUIDMap.put(groupUUID, ref);
+          }
+        }
+      } catch (NamingException e) {
+        throw new RuntimeException(e);
+      } finally {
+        rs.close();
+      }
+    } finally {
+      stmt.close();
+    }
+    if (toDelete.isEmpty() && toResolve.isEmpty()) {
+      return; // No ldap groups. Nothing to do.
+    }
+
+    ui.message("Update LDAP groups to be GroupReferences.");
+
+    // Update the groupOwnerUUID for LDAP groups to point to the new UUID.
+    List<AccountGroup> toUpdate = Lists.newArrayList();
+    Set<AccountGroup.UUID> resolveToUpdate = Sets.newHashSet();
+    Map<AccountGroup.UUID, AccountGroup> resolveGroups = Maps.newHashMap();
+    for (AccountGroup g : db.accountGroups().all()) {
+      if (ldapUUIDMap.containsKey(g.getGroupUUID())) {
+        continue; // Ignore the LDAP groups with a valid DN.
+      } else if (toResolve.contains(g.getGroupUUID())) {
+        resolveGroups.put(g.getGroupUUID(), g); // Keep the ones to resolve.
+        continue;
+      }
+
+      GroupReference ref = ldapUUIDMap.get(g.getOwnerGroupUUID());
+      if (ref != null) {
+        // Update the owner group UUID to the new ldap UUID scheme.
+        g.setOwnerGroupUUID(ref.getUUID());
+        toUpdate.add(g);
+      } else if (toResolve.contains(g.getOwnerGroupUUID())) {
+        // The unresolved group is used as an owner.
+        // Add to the list of LDAP groups to be made INTERNAL.
+        resolveToUpdate.add(g.getOwnerGroupUUID());
+      }
+    }
+
+    toResolve.removeAll(resolveToUpdate);
+
+    // Update project.config group references to use the new LDAP GroupReference
+    for (Project.NameKey name : mgr.list()) {
+      Repository git;
+      try {
+        git = mgr.openRepository(name);
+      } catch (RepositoryNotFoundException e) {
+        throw new OrmException(e);
+      } catch (IOException e) {
+        throw new OrmException(e);
+      }
+
+      try {
+        MetaDataUpdate md =
+            new MetaDataUpdate(GitReferenceUpdated.DISABLED, name, git);
+        md.getCommitBuilder().setAuthor(serverUser);
+        md.getCommitBuilder().setCommitter(serverUser);
+
+        ProjectConfig config = ProjectConfig.read(md);
+
+        // Update the existing refences to the new reference.
+        boolean updated = false;
+        for (Map.Entry<AccountGroup.UUID, GroupReference> entry: ldapUUIDMap.entrySet()) {
+          GroupReference ref = config.getGroup(entry.getKey());
+          if (ref != null) {
+            updated = true;
+            ref.setName(entry.getValue().getName());
+            ref.setUUID(entry.getValue().getUUID());
+            config.resolve(ref);
+          }
+        }
+
+        // Determine if a toResolve group is used and should be made INTERNAL.
+        Iterator<AccountGroup.UUID> iter = toResolve.iterator();
+        while (iter.hasNext()) {
+          AccountGroup.UUID uuid = iter.next();
+          if (config.getGroup(uuid) != null) {
+            resolveToUpdate.add(uuid);
+            iter.remove();
+          }
+        }
+
+        if (!updated) {
+          continue;
+        }
+
+        md.setMessage("Switch LDAP group UUIDs to DNs\n");
+        if (!config.commit(md)) {
+          throw new OrmException("Cannot update " + name);
+        }
+      } catch (IOException e) {
+        throw new OrmException(e);
+      } catch (ConfigInvalidException e) {
+        throw new OrmException(e);
+      } finally {
+        git.close();
+      }
+    }
+
+    for (AccountGroup.UUID uuid : resolveToUpdate) {
+      AccountGroup group = resolveGroups.get(uuid);
+      group.setType(AccountGroup.Type.INTERNAL);
+      toUpdate.add(group);
+
+      ui.message(String.format(
+          "*** Group has no DN and is inuse. Updated to be INTERNAL: %s",
+          group.getName()));
+    }
+
+    for (AccountGroup.UUID uuid : toResolve) {
+      AccountGroup group = resolveGroups.get(uuid);
+      toDelete.add(group.getId());
+      namesToDelete.add(group.getNameKey());
+    }
+
+    // Update group owners
+    db.accountGroups().update(toUpdate);
+    // Delete existing LDAP groups
+    db.accountGroupNames().deleteKeys(namesToDelete);
+    db.accountGroups().deleteKeys(toDelete);
+  }
+
+  private static GroupReference groupReference(String dn)
+      throws NamingException {
+    LdapName name = new LdapName(dn);
+    Preconditions.checkState(!name.isEmpty(), "Invalid LDAP dn: %s", dn);
+    String cn = name.get(name.size() - 1);
+    int index = cn.indexOf('=');
+    if (index >= 0) {
+      cn = cn.substring(index + 1);
+    }
+    return new GroupReference(new AccountGroup.UUID("ldap:" + dn), "ldap/" + cn);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java
index 64b3afa..eff5575 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java
@@ -24,6 +24,8 @@
 
   boolean yesno(boolean def, String msg);
 
+  boolean isBatch();
+
   void pruneSchema(StatementExecutor e, List<String> pruneList)
       throws OrmException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java
new file mode 100644
index 0000000..686a108
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/**
+ * The default RequestContext to use when not in a request scope e.g.
+ * ThreadLocalRequestContext is not set.
+ */
+@Singleton
+public class FallbackRequestContext implements RequestContext {
+
+  private final AnonymousUser user;
+
+  @Inject
+  FallbackRequestContext(AnonymousUser user) {
+    this.user = user;
+  }
+
+  @Override
+  public CurrentUser getCurrentUser() {
+    return user;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
index 9befc7d..fa07176 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.util;
 
 import com.google.common.collect.Maps;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.inject.Inject;
@@ -38,17 +37,15 @@
 
   private final String url;
   private final SocketAddress peer;
-  private final CurrentUser user;
 
   @Inject
   GuiceRequestScopePropagator(
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
       @RemotePeer Provider<SocketAddress> remotePeerProvider,
-      Provider<CurrentUser> currentUserProvider) {
-    super(ServletScopes.REQUEST);
+      ThreadLocalRequestContext local) {
+    super(ServletScopes.REQUEST, local);
     this.url = urlProvider != null ? urlProvider.get() : null;
     this.peer = remotePeerProvider.get();
-    this.user = currentUserProvider.get();
   }
 
   /**
@@ -69,9 +66,6 @@
         Providers.of(peer));
     seedMap.put(Key.get(SocketAddress.class, RemotePeer.class), peer);
 
-    seedMap.put(Key.get(typeOfProvider(CurrentUser.class)), Providers.of(user));
-    seedMap.put(Key.get(CurrentUser.class), user);
-
     return ServletScopes.continueRequest(callable, seedMap);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/MagicBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/MagicBranch.java
index ef72f92..510deaa02 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/MagicBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/MagicBranch.java
@@ -108,4 +108,4 @@
 
   private MagicBranch() {
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java
similarity index 65%
copy from gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java
index 204d777..ca8573f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java
@@ -12,12 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.server.util;
 
-public class IncompleteUserInfoException extends Exception {
-  private static final long serialVersionUID = 1L;
+import com.google.gerrit.server.CurrentUser;
 
-  public IncompleteUserInfoException(final String userName, final String missingInfo) {
-    super("For the user \"" + userName + "\" " + missingInfo + " is not set.");
-  }
+/**
+ * The RequestContext is an interface exposing the fields that are needed
+ * by the GerritGlobalModule scope.
+ */
+public interface RequestContext {
+
+  CurrentUser getCurrentUser();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
index 3661aa2..84c61e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
@@ -42,9 +42,12 @@
 public abstract class RequestScopePropagator {
 
   private final Scope scope;
+  private final ThreadLocalRequestContext local;
 
-  protected RequestScopePropagator(Scope scope) {
+  protected RequestScopePropagator(Scope scope,
+      ThreadLocalRequestContext local) {
     this.scope = scope;
+    this.local = local;
   }
 
   /**
@@ -70,26 +73,8 @@
    * @return a new Callable which will execute in the current request scope.
    */
   public final <T> Callable<T> wrap(final Callable<T> callable) {
-    final Callable<T> wrapped = wrapImpl(new Callable<T>() {
-      @Override
-      public T call() throws Exception {
-        RequestCleanup cleanup = scope.scope(
-            Key.get(RequestCleanup.class),
-            new Provider<RequestCleanup>() {
-              @Override
-              public RequestCleanup get() {
-                return new RequestCleanup();
-              }
-            }).get();
-
-        try {
-          return callable.call();
-        } finally {
-          cleanup.run();
-        }
-      }
-    });
-
+    final Callable<T> wrapped =
+        wrapImpl(context(local.getContext(), cleanup(callable)));
     return new Callable<T>() {
       @Override
       public T call() throws Exception {
@@ -178,4 +163,41 @@
    * @see #wrap(Callable)
    */
   protected abstract <T> Callable<T> wrapImpl(final Callable<T> callable);
+
+  protected <T> Callable<T> context(final RequestContext context,
+      final Callable<T> callable) {
+    return new Callable<T>() {
+      @Override
+      public T call() throws Exception {
+        RequestContext old = local.setContext(context);
+        try {
+          return callable.call();
+        } finally {
+          local.setContext(old);
+        }
+      }
+    };
+  }
+
+  protected <T> Callable<T> cleanup(final Callable<T> callable) {
+    return new Callable<T>() {
+      @Override
+      public T call() throws Exception {
+        RequestCleanup cleanup = scope.scope(
+            Key.get(RequestCleanup.class),
+            new Provider<RequestCleanup>() {
+              @Override
+              public RequestCleanup get() {
+                return new RequestCleanup();
+              }
+            }).get();
+
+        try {
+          return callable.call();
+        } finally {
+          cleanup.run();
+        }
+      }
+    };
+  }
 }
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
new file mode 100644
index 0000000..b411512
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
@@ -0,0 +1,87 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import com.google.common.base.Objects;
+import com.google.gerrit.common.errors.NotSignedInException;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Provides;
+import com.google.inject.ProvisionException;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+
+import javax.annotation.Nullable;
+
+/**
+ * ThreadLocalRequestContext manages the current RequestContext using a
+ * ThreadLocal. When the context is set, the fields exposed by the context
+ * are considered in scope. Otherwise, the FallbackRequestContext is used.
+ */
+public class ThreadLocalRequestContext {
+  private static final String FALLBACK = "FALLBACK";
+
+  public static Module module() {
+    return new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(ThreadLocalRequestContext.class);
+        bind(RequestContext.class).annotatedWith(Names.named(FALLBACK))
+            .to(FallbackRequestContext.class);
+      }
+
+      @Provides
+      RequestContext provideRequestContext(
+          @Named(FALLBACK) RequestContext fallback) {
+        return Objects.firstNonNull(local.get(), fallback);
+      }
+
+      @Provides
+      CurrentUser provideCurrentUser(RequestContext ctx) {
+        return ctx.getCurrentUser();
+      }
+
+      @Provides
+      IdentifiedUser provideCurrentUser(CurrentUser user) {
+        if (user instanceof IdentifiedUser) {
+          return (IdentifiedUser) user;
+        }
+        throw new ProvisionException(NotSignedInException.MESSAGE,
+            new NotSignedInException());
+      }
+    };
+  }
+
+  private static final ThreadLocal<RequestContext> local =
+      new ThreadLocal<RequestContext>();
+
+  @Inject
+  ThreadLocalRequestContext() {
+  }
+
+  public RequestContext setContext(@Nullable RequestContext ctx) {
+    RequestContext old = getContext();
+    local.set(ctx);
+    return old;
+  }
+
+  @Nullable
+  public RequestContext getContext() {
+    return local.get();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
index 581ccc1..e465247 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
@@ -31,8 +31,8 @@
   private final ThreadLocal<C> threadLocal;
 
   protected ThreadLocalRequestScopePropagator(Scope scope,
-      ThreadLocal<C> threadLocal) {
-    super(scope);
+      ThreadLocal<C> threadLocal, ThreadLocalRequestContext local) {
+    super(scope, local);
     this.threadLocal = threadLocal;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
index d08bd1f..74c97f3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.reviewdb.client.ApprovalCategory.Id;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -57,7 +56,7 @@
 
   @Inject
   FunctionState(final ApprovalTypes approvalTypes,
-      final IdentifiedUser.GenericFactory userFactory, final GroupCache egc,
+      final IdentifiedUser.GenericFactory userFactory,
       @Assisted final ChangeControl c, @Assisted final PatchSet.Id psId,
       @Assisted final Collection<PatchSetApproval> all) {
     this.approvalTypes = approvalTypes;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
index 23cedce..8c70cad 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
-import com.google.gerrit.server.ReplicationUser;
 import com.google.gerrit.server.project.ChangeControl;
 
 import com.googlecode.prolog_cafe.lang.EvaluationException;
@@ -61,8 +60,6 @@
       resultTerm = anonymous;
     } else if (curUser instanceof PeerDaemonUser) {
       resultTerm = peerDaemon;
-    } else if (curUser instanceof ReplicationUser) {
-      resultTerm = replication;
     } else {
       throw new EvaluationException("Unknown user type");
     }
diff --git a/gerrit-server/src/main/prolog/gerrit_common.pl b/gerrit-server/src/main/prolog/gerrit_common.pl
index 5acc831..a75acc0 100644
--- a/gerrit-server/src/main/prolog/gerrit_common.pl
+++ b/gerrit-server/src/main/prolog/gerrit_common.pl
@@ -149,6 +149,7 @@
 
 is_all_ok([]).
 is_all_ok([label(_, ok(__)) | Ls]) :- is_all_ok(Ls).
+is_all_ok([label(_, may(__)) | Ls]) :- is_all_ok(Ls).
 is_all_ok(_) :- fail.
 
 
@@ -209,8 +210,8 @@
 %%
 legacy_submit_rule('MaxWithBlock', Label, Id, Min, Max, T) :- !, max_with_block(Label, Min, Max, T).
 legacy_submit_rule('MaxNoBlock', Label, Id, Min, Max, T) :- !, max_no_block(Label, Max, T).
-legacy_submit_rule('NoBlock', Label, Id, Min, Max, T) :- !, T = ok(_).
-legacy_submit_rule('NoOp', Label, Id, Min, Max, T) :- !, T = ok(_).
+legacy_submit_rule('NoBlock', Label, Id, Min, Max, T) :- !, T = may(_).
+legacy_submit_rule('NoOp', Label, Id, Min, Max, T) :- !, T = may(_).
 legacy_submit_rule(Fun, Label, Id, Min, Max, T) :- T = impossible(unsupported(Fun)).
 
 
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/documentation/pegdown.css b/gerrit-server/src/main/resources/com/google/gerrit/server/documentation/pegdown.css
new file mode 100644
index 0000000..eada653
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/documentation/pegdown.css
@@ -0,0 +1,39 @@
+body {
+  margin: 1em;
+}
+
+h1, h2, h3, h4, h5, h6 {
+  color: #527bbd;
+  font-family: sans-serif;
+}
+
+h1, h2, h3 {
+  border-bottom: 2px solid silver;
+}
+
+pre {
+  border: 2px solid silver;
+  background: #ebebeb;
+  margin-left: 2em;
+  width: 100em;
+  color: darkgreen;
+  padding: 2px;
+}
+
+dl dt {
+  margin-top: 1em;
+}
+
+table.plugin_info {
+  border-collapse: separate;
+  border-spacing: 0;
+  text-align: left;
+  margin-left: 2em;
+}
+table.plugin_info th {
+  padding-right: 0.5em;
+  border-right: 2px solid silver;
+}
+table.plugin_info td {
+  padding-left: 0.5em;
+}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm
index 547c1b4..9af98a6 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm
@@ -43,5 +43,11 @@
 $email.coverLetter
 
 #end
+##
+## It is possible to increase the span of the quoted lines by using the line
+## count parameter when calling $email.inlineComments as a function.
+##
+## Example: #if($email.inlineComments)$email.getInlineComments(5)#end
+##
 #if($email.inlineComments)$email.inlineComments#end
 #end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm
index 8e08dc4..42f2ca9 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm
@@ -43,6 +43,11 @@
 #end
 #else
 $fromName has uploaded a new change for review.
+#if($email.changeUrl)
+
+  $email.changeUrl
+
+#end
 #end
 
 Change subject: $change.subject
@@ -52,3 +57,7 @@
 #if($email.sshHost)
   git pull ssh://$email.sshHost/$projectName $patchSet.refName
 #end
+#if($email.includeDiff)
+
+$email.UnifiedDiff
+#end
\ No newline at end of file
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
new file mode 100644
index 0000000..24f3386
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import junit.framework.TestCase;
+
+public class StringUtilTest extends TestCase {
+  /**
+   * Test the boundary condition that the first character of a string
+   * should be escaped.
+   */
+  public void testEscapeFirstChar() {
+    assertEquals(StringUtil.escapeString("\tLeading tab"), "\\tLeading tab");
+  }
+
+  /**
+   * Test the boundary condition that the last character of a string
+   * should be escaped.
+   */
+  public void testEscapeLastChar() {
+    assertEquals(StringUtil.escapeString("Trailing tab\t"), "Trailing tab\\t");
+  }
+
+  /**
+   * Test that various forms of input strings are escaped (or left as-is)
+   * in the expected way.
+   */
+  public void testEscapeString() {
+    final String[] testPairs =
+      { "", "",
+        "plain string", "plain string",
+        "string with \"quotes\"", "string with \"quotes\"",
+        "string with 'quotes'", "string with 'quotes'",
+        "string with 'quotes'", "string with 'quotes'",
+        "C:\\Program Files\\MyProgram", "C:\\\\Program Files\\\\MyProgram",
+        "string\nwith\nnewlines", "string\\nwith\\nnewlines",
+        "string\twith\ttabs", "string\\twith\\ttabs" };
+    for (int i = 0; i < testPairs.length; i += 2) {
+      assertEquals(StringUtil.escapeString(testPairs[i]), testPairs[i + 1]);
+    }
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
index 2bc5d9f..02bf815 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -201,8 +202,9 @@
 
   private RevCommit commit(ProjectConfig cfg) throws IOException,
       MissingObjectException, IncorrectObjectTypeException {
-    MetaDataUpdate md = new MetaDataUpdate(new NoReplication(), //
-        cfg.getProject().getNameKey(), //
+    MetaDataUpdate md = new MetaDataUpdate(
+        GitReferenceUpdated.DISABLED,
+        cfg.getProject().getNameKey(),
         db);
     util.tick(5);
     util.setAuthorAndCommitter(md.getCommitBuilder());
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/PushReplicationTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/PushReplicationTest.java
deleted file mode 100644
index 7ae705f..0000000
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/PushReplicationTest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import static com.google.gerrit.server.git.PushReplication.ReplicationConfig.encode;
-import static com.google.gerrit.server.git.PushReplication.ReplicationConfig.needsUrlEncoding;
-
-import junit.framework.TestCase;
-
-import org.eclipse.jgit.transport.URIish;
-
-import java.net.URISyntaxException;
-
-public class PushReplicationTest extends TestCase {
-  public void testNeedsUrlEncoding() throws URISyntaxException {
-    assertTrue(needsUrlEncoding(new URIish("http://host/path")));
-    assertTrue(needsUrlEncoding(new URIish("https://host/path")));
-    assertTrue(needsUrlEncoding(new URIish("amazon-s3://config/bucket/path")));
-
-    assertFalse(needsUrlEncoding(new URIish("host:path")));
-    assertFalse(needsUrlEncoding(new URIish("user@host:path")));
-    assertFalse(needsUrlEncoding(new URIish("git://host/path")));
-    assertFalse(needsUrlEncoding(new URIish("ssh://host/path")));
-  }
-
-  public void testUrlEncoding() {
-    assertEquals("foo/bar/thing", encode("foo/bar/thing"));
-    assertEquals("--%20All%20Projects%20--", encode("-- All Projects --"));
-    assertEquals("name/with%20a%20space", encode("name/with a space"));
-    assertEquals("name%0Awith-LF", encode("name\nwith-LF"));
-  }
-}
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 0e556f3..8d061eb 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
@@ -26,6 +26,7 @@
 import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
 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.gwtorm.client.KeyUtil;
 import com.google.gwtorm.server.ListResultSet;
 import com.google.gwtorm.server.ResultSet;
@@ -71,7 +72,7 @@
   private ReviewDb schema;
   private Provider<String> urlProvider;
   private GitRepositoryManager repoManager;
-  private ReplicationQueue replication;
+  private GitReferenceUpdated replication;
 
   @SuppressWarnings("unchecked")
   @Override
@@ -84,7 +85,7 @@
     subscriptions = createStrictMock(SubmoduleSubscriptionAccess.class);
     urlProvider = createStrictMock(Provider.class);
     repoManager = createStrictMock(GitRepositoryManager.class);
-    replication = createStrictMock(ReplicationQueue.class);
+    replication = createStrictMock(GitReferenceUpdated.class);
   }
 
   private void doReplay() {
@@ -638,7 +639,7 @@
     expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
         .andReturn(targetRepository);
 
-    replication.scheduleUpdate(targetBranchNameKey.getParentKey(),
+    replication.fire(targetBranchNameKey.getParentKey(),
         targetBranchNameKey.get());
 
     expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
@@ -739,7 +740,7 @@
     expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
         .andReturn(targetRepository);
 
-    replication.scheduleUpdate(targetBranchNameKey.getParentKey(),
+    replication.fire(targetBranchNameKey.getParentKey(),
         targetBranchNameKey.get());
 
     expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
new file mode 100644
index 0000000..2d432e6
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
@@ -0,0 +1,138 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.ioutil;
+
+import com.google.gerrit.server.ioutil.ColumnFormatter;
+
+import junit.framework.TestCase;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+public class ColumnFormatterTest extends TestCase {
+  /**
+   * Holds an in-memory {@link java.io.PrintWriter} object and allows
+   * comparisons of its contents to a supplied string via an assert statement.
+   */
+  class PrintWriterComparator {
+    private PrintWriter printWriter;
+    private StringWriter stringWriter;
+
+    public PrintWriterComparator() {
+      stringWriter = new StringWriter();
+      printWriter = new PrintWriter(stringWriter);
+    }
+
+    public void assertEquals(String str) {
+      printWriter.flush();
+      TestCase.assertEquals(stringWriter.toString(), str);
+    }
+
+    public PrintWriter getPrintWriter() {
+      return printWriter;
+    }
+  }
+
+  /**
+   * Test that only lines with at least one column of text emit output.
+   */
+  public void testEmptyLine() {
+    final PrintWriterComparator comparator = new PrintWriterComparator();
+    final ColumnFormatter formatter =
+        new ColumnFormatter(comparator.getPrintWriter(), '\t');
+    formatter.addColumn("foo");
+    formatter.addColumn("bar");
+    formatter.nextLine();
+    formatter.nextLine();
+    formatter.nextLine();
+    formatter.addColumn("foo");
+    formatter.addColumn("bar");
+    formatter.finish();
+    comparator.assertEquals("foo\tbar\nfoo\tbar\n");
+  }
+
+  /**
+   * Test that there is no output if no columns are ever added.
+   */
+  public void testEmptyOutput() {
+    final PrintWriterComparator comparator = new PrintWriterComparator();
+    final ColumnFormatter formatter =
+        new ColumnFormatter(comparator.getPrintWriter(), '\t');
+    formatter.nextLine();
+    formatter.nextLine();
+    formatter.finish();
+    comparator.assertEquals("");
+  }
+
+  /**
+   * Test that there is no output (nor any exceptions) if we finalize
+   * the output immediately after the creation of the {@link ColumnFormatter}.
+   */
+  public void testNoNextLine() {
+    final PrintWriterComparator comparator = new PrintWriterComparator();
+    final ColumnFormatter formatter =
+        new ColumnFormatter(comparator.getPrintWriter(), '\t');
+    formatter.finish();
+    comparator.assertEquals("");
+  }
+
+  /**
+   * Test that the text in added columns is escaped while the column separator
+   * (which of course shouldn't be escaped) is left alone.
+   */
+  public void testEscapingTakesPlace() {
+    final PrintWriterComparator comparator = new PrintWriterComparator();
+    final ColumnFormatter formatter =
+        new ColumnFormatter(comparator.getPrintWriter(), '\t');
+    formatter.addColumn("foo");
+    formatter.addColumn(
+        "\tan indented multi-line\ntext");
+    formatter.nextLine();
+    formatter.finish();
+    comparator.assertEquals("foo\t\\tan indented multi-line\\ntext\n");
+  }
+
+  /**
+   * Test that we get the correct output with multi-line input where the number
+   * of columns in each line varies.
+   */
+  public void testMultiLineDifferentColumnCount() {
+    final PrintWriterComparator comparator = new PrintWriterComparator();
+    final ColumnFormatter formatter =
+        new ColumnFormatter(comparator.getPrintWriter(), '\t');
+    formatter.addColumn("foo");
+    formatter.addColumn("bar");
+    formatter.addColumn("baz");
+    formatter.nextLine();
+    formatter.addColumn("foo");
+    formatter.addColumn("bar");
+    formatter.nextLine();
+    formatter.finish();
+    comparator.assertEquals("foo\tbar\tbaz\nfoo\tbar\n");
+  }
+
+  /**
+   * Test that we get the correct output with a single column of input.
+   */
+  public void testOneColumn() {
+    final PrintWriterComparator comparator = new PrintWriterComparator();
+    final ColumnFormatter formatter =
+        new ColumnFormatter(comparator.getPrintWriter(), '\t');
+    formatter.addColumn("foo");
+    formatter.nextLine();
+    formatter.finish();
+    comparator.assertEquals("foo\n");
+  }
+}
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 340db7e..e4d9418 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -20,6 +20,8 @@
 import static com.google.gerrit.common.data.Permission.READ;
 import static com.google.gerrit.common.data.Permission.SUBMIT;
 
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.common.data.GroupReference;
@@ -36,7 +38,6 @@
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
-import com.google.gerrit.server.cache.ConcurrentHashMapCache;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -278,6 +279,10 @@
       }
 
       @Override
+      public void remove(Project p) {
+      }
+
+      @Override
       public Iterable<Project.NameKey> all() {
         return Collections.emptySet();
       }
@@ -317,10 +322,9 @@
     local.createInMemory();
     local.getProject().setParentName(parent.getProject().getName());
 
-    sectionSorter =
-        new PermissionCollection.Factory(
-            new SectionSortCache(
-                new ConcurrentHashMapCache<SectionSortCache.EntryKey, SectionSortCache.EntryVal>()));
+    Cache<SectionSortCache.EntryKey, SectionSortCache.EntryVal> c =
+        CacheBuilder.newBuilder().build();
+    sectionSorter = new PermissionCollection.Factory(new SectionSortCache(c));
   }
 
   private static void assertOwner(String ref, ProjectControl u) {
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 34f7430..cc8d47d 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
@@ -108,6 +108,11 @@
       }
 
       @Override
+      public boolean isBatch() {
+        return true;
+      }
+
+      @Override
       public void pruneSchema(StatementExecutor e, List<String> pruneList)
           throws OrmException {
         for (String sql : pruneList) {
diff --git a/gerrit-sshd/.settings/org.eclipse.core.resources.prefs b/gerrit-sshd/.settings/org.eclipse.core.resources.prefs
index c780f44..0871ea8 100644
--- a/gerrit-sshd/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-sshd/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,6 @@
-#Thu Jul 28 11:02:36 PDT 2011
+#Tue May 15 09:21:12 PDT 2012
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding//src/test/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-sshd/pom.xml b/gerrit-sshd/pom.xml
index 1c197a0..31b2422 100644
--- a/gerrit-sshd/pom.xml
+++ b/gerrit-sshd/pom.xml
@@ -67,7 +67,7 @@
 
     <dependency>
       <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-ehcache</artifactId>
+      <artifactId>gerrit-cache-h2</artifactId>
       <version>${project.version}</version>
     </dependency>
   </dependencies>
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
index eb8a5c2..af5df25 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
@@ -36,6 +36,9 @@
   protected ProjectControl projectControl;
 
   @Inject
+  private SshScope sshScope;
+
+  @Inject
   private GitRepositoryManager repoManager;
 
   @Inject
@@ -56,7 +59,7 @@
   @Override
   public void start(final Environment env) {
     Context ctx = context.subContext(newSession(), context.getCommandLine());
-    final Context old = SshScope.set(ctx);
+    final Context old = sshScope.set(ctx);
     try {
       startThread(new ProjectCommandRunnable() {
         @Override
@@ -76,7 +79,7 @@
         }
       });
     } finally {
-      SshScope.set(old);
+      sshScope.set(old);
     }
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
new file mode 100644
index 0000000..9582c93
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
@@ -0,0 +1,125 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd;
+
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Atomics;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.inject.Provider;
+
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.Environment;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Command that executes some other command. */
+public class AliasCommand extends BaseCommand {
+  private final DispatchCommandProvider root;
+  private final Provider<CurrentUser> currentUser;
+  private final CommandName command;
+  private final AtomicReference<Command> atomicCmd;
+
+  AliasCommand(@CommandName(Commands.ROOT) DispatchCommandProvider root,
+      Provider<CurrentUser> currentUser, CommandName command) {
+    this.root = root;
+    this.currentUser = currentUser;
+    this.command = command;
+    this.atomicCmd = Atomics.newReference();
+  }
+
+  @Override
+  public void start(Environment env) throws IOException {
+    try {
+      begin(env);
+    } catch (UnloggedFailure e) {
+      String msg = e.getMessage();
+      if (!msg.endsWith("\n")) {
+        msg += "\n";
+      }
+      err.write(msg.getBytes(ENC));
+      err.flush();
+      onExit(e.exitCode);
+    }
+  }
+
+  private void begin(Environment env) throws UnloggedFailure, IOException {
+    Map<String, Provider<Command>> map = root.getMap();
+    for (String name : chain(command)) {
+      Provider<? extends Command> p = map.get(name);
+      if (p == null) {
+        throw new UnloggedFailure(1, getName() + ": not found");
+      }
+
+      Command cmd = p.get();
+      if (!(cmd instanceof DispatchCommand)) {
+        throw new UnloggedFailure(1, getName() + ": not found");
+      }
+      map = ((DispatchCommand) cmd).getMap();
+    }
+
+    Provider<? extends Command> p = map.get(command.value());
+    if (p == null) {
+      throw new UnloggedFailure(1, getName() + ": not found");
+    }
+
+    Command cmd = p.get();
+    checkRequiresCapability(cmd);
+    if (cmd instanceof BaseCommand) {
+      BaseCommand bc = (BaseCommand)cmd;
+      bc.setName(getName());
+      bc.setArguments(getArguments());
+    }
+    provideStateTo(cmd);
+    atomicCmd.set(cmd);
+    cmd.start(env);
+  }
+
+  @Override
+  public void destroy() {
+    Command cmd = atomicCmd.getAndSet(null);
+    if (cmd != null) {
+        cmd.destroy();
+    }
+  }
+
+  private void checkRequiresCapability(Command cmd) throws UnloggedFailure {
+    RequiresCapability rc = cmd.getClass().getAnnotation(RequiresCapability.class);
+    if (rc != null) {
+      CurrentUser user = currentUser.get();
+      CapabilityControl ctl = user.getCapabilities();
+      if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
+        String msg = String.format(
+            "fatal: %s does not have \"%s\" capability.",
+            user.getUserName(), rc.value());
+        throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
+      }
+    }
+  }
+
+  private static LinkedList<String> chain(CommandName command) {
+    LinkedList<String> chain = Lists.newLinkedList();
+    while (command != null) {
+      chain.addFirst(command.value());
+      command = Commands.parentOf(command);
+    }
+    chain.removeLast();
+    return chain;
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java
new file mode 100644
index 0000000..ee28e03
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd;
+
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.apache.sshd.server.Command;
+
+/** Resolves an alias to another command. */
+public class AliasCommandProvider implements Provider<Command> {
+  private final CommandName command;
+
+  @Inject
+  @CommandName(Commands.ROOT)
+  private DispatchCommandProvider root;
+
+  @Inject
+  private Provider<CurrentUser> currentUser;
+
+  public AliasCommandProvider(CommandName command) {
+    this.command = command;
+  }
+
+  @Override
+  public Command get() {
+    return new AliasCommand(root, currentUser, command);
+  }
+}
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 c5be624..2b4543b 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
@@ -73,6 +73,9 @@
   private ExitCallback exit;
 
   @Inject
+  private SshScope sshScope;
+
+  @Inject
   private CmdLineParser.Factory cmdLineParserFactory;
 
   @Inject
@@ -117,10 +120,18 @@
     this.exit = callback;
   }
 
+  String getName() {
+    return commandName;
+  }
+
   void setName(final String prefix) {
     this.commandName = prefix;
   }
 
+  String[] getArguments() {
+    return argv;
+  }
+
   public void setArguments(final String[] argv) {
     this.argv = argv;
   }
@@ -280,7 +291,9 @@
    */
   protected void onExit(final int rc) {
     exit.onExit(rc);
-    cleanup.run();
+    if (cleanup != null) {
+      cleanup.run();
+    }
   }
 
   /** Wrap the supplied output stream in a UTF-8 encoded PrintWriter. */
@@ -379,55 +392,59 @@
 
     @Override
     public void cancel() {
-      final Context old = SshScope.set(context);
-      try {
-        onExit(STATUS_CANCEL);
-      } finally {
-        SshScope.set(old);
+      synchronized (this) {
+        final Context old = sshScope.set(context);
+        try {
+          onExit(STATUS_CANCEL);
+        } finally {
+          sshScope.set(old);
+        }
       }
     }
 
     @Override
     public void run() {
-      final Thread thisThread = Thread.currentThread();
-      final String thisName = thisThread.getName();
-      int rc = 0;
-      final Context old = SshScope.set(context);
-      try {
-        context.started = System.currentTimeMillis();
-        thisThread.setName("SSH " + taskName);
-
-        if (thunk instanceof ProjectCommandRunnable) {
-          ((ProjectCommandRunnable) thunk).executeParseCommand();
-          projectName = ((ProjectCommandRunnable) thunk).getProjectName();
-        }
-
+      synchronized (this) {
+        final Thread thisThread = Thread.currentThread();
+        final String thisName = thisThread.getName();
+        int rc = 0;
+        final Context old = sshScope.set(context);
         try {
-          thunk.run();
-        } catch (NoSuchProjectException e) {
-          throw new UnloggedFailure(1, e.getMessage() + " no such project");
-        } catch (NoSuchChangeException e) {
-          throw new UnloggedFailure(1, e.getMessage() + " no such change");
-        }
+          context.started = System.currentTimeMillis();
+          thisThread.setName("SSH " + taskName);
 
-        out.flush();
-        err.flush();
-      } catch (Throwable e) {
-        try {
+          if (thunk instanceof ProjectCommandRunnable) {
+            ((ProjectCommandRunnable) thunk).executeParseCommand();
+            projectName = ((ProjectCommandRunnable) thunk).getProjectName();
+          }
+
+          try {
+            thunk.run();
+          } catch (NoSuchProjectException e) {
+            throw new UnloggedFailure(1, e.getMessage() + " no such project");
+          } catch (NoSuchChangeException e) {
+            throw new UnloggedFailure(1, e.getMessage() + " no such change");
+          }
+
           out.flush();
-        } catch (Throwable e2) {
-        }
-        try {
           err.flush();
-        } catch (Throwable e2) {
-        }
-        rc = handleError(e);
-      } finally {
-        try {
-          onExit(rc);
+        } catch (Throwable e) {
+          try {
+            out.flush();
+          } catch (Throwable e2) {
+          }
+          try {
+            err.flush();
+          } catch (Throwable e2) {
+          }
+          rc = handleError(e);
         } finally {
-          SshScope.set(old);
-          thisThread.setName(thisName);
+          try {
+            onExit(rc);
+          } finally {
+            sshScope.set(old);
+            thisThread.setName(thisName);
+          }
         }
       }
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index 08bfaaa..be6659d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.common.util.concurrent.Atomics;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.sshd.SshScope.Context;
@@ -36,7 +38,11 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * Creates a CommandFactory using commands registered by {@link CommandModule}.
@@ -47,18 +53,26 @@
 
   private final DispatchCommandProvider dispatcher;
   private final SshLog log;
-  private final Executor startExecutor;
+  private final SshScope sshScope;
+  private final ScheduledExecutorService startExecutor;
+  private final Executor destroyExecutor;
 
   @Inject
   CommandFactoryProvider(
       @CommandName(Commands.ROOT) final DispatchCommandProvider d,
       @GerritServerConfig final Config cfg, final WorkQueue workQueue,
-      final SshLog l) {
+      final SshLog l, final SshScope s) {
     dispatcher = d;
     log = l;
+    sshScope = s;
 
     int threads = cfg.getInt("sshd","commandStartThreads", 2);
     startExecutor = workQueue.createQueue(threads, "SshCommandStart");
+    destroyExecutor = Executors.newSingleThreadExecutor(
+        new ThreadFactoryBuilder()
+          .setNameFormat("SshCommandDestroy-%s")
+          .setDaemon(true)
+          .build());
   }
 
   @Override
@@ -81,11 +95,13 @@
     private Context ctx;
     private DispatchCommand cmd;
     private final AtomicBoolean logged;
+    private final AtomicReference<Future<?>> task;
 
     Trampoline(final String cmdLine) {
       commandLine = cmdLine;
       argv = split(cmdLine);
       logged = new AtomicBoolean();
+      task = Atomics.newReference();
     }
 
     public void setInputStream(final InputStream in) {
@@ -106,13 +122,13 @@
 
     public void setSession(final ServerSession session) {
       final SshSession s = session.getAttribute(SshSession.KEY);
-      this.ctx = new Context(s, commandLine);
+      this.ctx = sshScope.newContext(s, commandLine);
     }
 
     public void start(final Environment env) throws IOException {
       this.env = env;
       final Context ctx = this.ctx;
-      startExecutor.execute(new Runnable() {
+      task.set(startExecutor.submit(new Runnable() {
         public void run() {
           try {
             onStart();
@@ -126,12 +142,12 @@
         public String toString() {
           return "start (user " + ctx.getSession().getUsername() + ")";
         }
-      });
+      }));
     }
 
     private void onStart() throws IOException {
       synchronized (this) {
-        final Context old = SshScope.set(ctx);
+        final Context old = sshScope.set(ctx);
         try {
           cmd = dispatcher.get();
           cmd.setArguments(argv);
@@ -153,7 +169,7 @@
           });
           cmd.start(env);
         } finally {
-          SshScope.set(old);
+          sshScope.set(old);
         }
       }
     }
@@ -182,16 +198,29 @@
 
     @Override
     public void destroy() {
+      Future<?> future = task.getAndSet(null);
+      if (future != null) {
+        future.cancel(true);
+        destroyExecutor.execute(new Runnable() {
+          @Override
+          public void run() {
+            onDestroy();
+          }
+        });
+      }
+    }
+
+    private void onDestroy() {
       synchronized (this) {
         if (cmd != null) {
-          final Context old = SshScope.set(ctx);
+          final Context old = sshScope.set(ctx);
           try {
             cmd.destroy();
             log(BaseCommand.STATUS_CANCEL);
           } finally {
             ctx = null;
             cmd = null;
-            SshScope.set(old);
+            sshScope.set(old);
           }
         }
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
index 12cff9c..5340d6f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
@@ -97,6 +97,13 @@
     return false;
   }
 
+  static CommandName parentOf(CommandName name) {
+    if (name instanceof NestedCommandNameImpl) {
+      return ((NestedCommandNameImpl) name).parent;
+    }
+    return null;
+  }
+
   private static final class NestedCommandNameImpl implements CommandName {
     private final CommandName parent;
     private final String name;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index a96a661..a69a2f1 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -65,6 +65,7 @@
   private final IdentifiedUser.GenericFactory userFactory;
   private final PeerDaemonUser.Factory peerFactory;
   private final Config config;
+  private final SshScope sshScope;
   private final Set<PublicKey> myHostKeys;
   private volatile PeerKeyCache peerKeyCache;
 
@@ -72,12 +73,13 @@
   DatabasePubKeyAuth(final SshKeyCacheImpl skc, final SshLog l,
       final IdentifiedUser.GenericFactory uf, final PeerDaemonUser.Factory pf,
       final SitePaths site, final KeyPairProvider hostKeyProvider,
-      final @GerritServerConfig Config cfg) {
+      final @GerritServerConfig Config cfg, final SshScope s) {
     sshKeyCache = skc;
     sshLog = l;
     userFactory = uf;
     peerFactory = pf;
     config = cfg;
+    sshScope = s;
     myHostKeys = myHostKeys(hostKeyProvider);
     peerKeyCache = new PeerKeyCache(site.peer_keys);
   }
@@ -171,24 +173,24 @@
       // session, record a login event in the log and add
       // a close listener to record a logout event.
       //
-      Context ctx = new Context(sd, null);
-      Context old = SshScope.set(ctx);
+      Context ctx = sshScope.newContext(sd, null);
+      Context old = sshScope.set(ctx);
       try {
         sshLog.onLogin();
       } finally {
-        SshScope.set(old);
+        sshScope.set(old);
       }
 
       session.getIoSession().getCloseFuture().addListener(
           new IoFutureListener<IoFuture>() {
             @Override
             public void operationComplete(IoFuture future) {
-              final Context ctx = new Context(sd, null);
-              final Context old = SshScope.set(ctx);
+              final Context ctx = sshScope.newContext(sd, null);
+              final Context old = sshScope.set(ctx);
               try {
                 sshLog.onLogout();
               } finally {
-                SshScope.set(old);
+                sshScope.set(old);
               }
             }
           });
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
index 351b2e7..301d68d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.Atomics;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.CapabilityControl;
-import com.google.gerrit.sshd.args4j.SubcommandHandler;
+import com.google.gerrit.server.args4j.SubcommandHandler;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -37,11 +39,10 @@
  */
 final class DispatchCommand extends BaseCommand {
   interface Factory {
-    DispatchCommand create(String prefix, Map<String, Provider<Command>> map);
+    DispatchCommand create(Map<String, Provider<Command>> map);
   }
 
   private final Provider<CurrentUser> currentUser;
-  private final String prefix;
   private final Map<String, Provider<Command>> commands;
   private final AtomicReference<Command> atomicCmd;
 
@@ -52,14 +53,17 @@
   private List<String> args = new ArrayList<String>();
 
   @Inject
-  DispatchCommand(final Provider<CurrentUser> cu, @Assisted final String pfx,
+  DispatchCommand(final Provider<CurrentUser> cu,
       @Assisted final Map<String, Provider<Command>> all) {
     currentUser = cu;
-    prefix = pfx;
     commands = all;
     atomicCmd = Atomics.newReference();
   }
 
+  Map<String, Provider<Command>> getMap() {
+    return commands;
+  }
+
   @Override
   public void start(final Environment env) throws IOException {
     try {
@@ -68,7 +72,7 @@
       final Provider<Command> p = commands.get(commandName);
       if (p == null) {
         String msg =
-            (prefix.isEmpty() ? "Gerrit Code Review" : prefix) + ": "
+            (getName().isEmpty() ? "Gerrit Code Review" : getName()) + ": "
                 + commandName + ": not found";
         throw new UnloggedFailure(1, msg);
       }
@@ -77,10 +81,10 @@
       checkRequiresCapability(cmd);
       if (cmd instanceof BaseCommand) {
         final BaseCommand bc = (BaseCommand) cmd;
-        if (prefix.isEmpty())
+        if (getName().isEmpty())
           bc.setName(commandName);
         else
-          bc.setName(prefix + " " + commandName);
+          bc.setName(getName() + " " + commandName);
         bc.setArguments(args.toArray(new String[args.size()]));
 
       } else if (!args.isEmpty()) {
@@ -128,22 +132,22 @@
   protected String usage() {
     final StringBuilder usage = new StringBuilder();
     usage.append("Available commands");
-    if (!prefix.isEmpty()) {
+    if (!getName().isEmpty()) {
       usage.append(" of ");
-      usage.append(prefix);
+      usage.append(getName());
     }
     usage.append(" are:\n");
     usage.append("\n");
-    for (Map.Entry<String, Provider<Command>> e : commands.entrySet()) {
+    for (String name : Sets.newTreeSet(commands.keySet())) {
       usage.append("   ");
-      usage.append(e.getKey());
+      usage.append(name);
       usage.append("\n");
     }
     usage.append("\n");
 
     usage.append("See '");
-    if (prefix.indexOf(' ') < 0) {
-      usage.append(prefix);
+    if (getName().indexOf(' ') < 0) {
+      usage.append(getName());
       usage.append(' ');
     }
     usage.append("COMMAND --help' for more information.\n");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
index d70d32f..b76ff71 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
@@ -38,24 +38,16 @@
   @Inject
   private DispatchCommand.Factory factory;
 
-  private final String dispatcherName;
   private final CommandName parent;
-
   private volatile ConcurrentMap<String, Provider<Command>> map;
 
   public DispatchCommandProvider(final CommandName cn) {
-    this(Commands.nameOf(cn), cn);
-  }
-
-  public DispatchCommandProvider(final String dispatcherName,
-      final CommandName cn) {
-    this.dispatcherName = dispatcherName;
     this.parent = cn;
   }
 
   @Override
   public DispatchCommand get() {
-    return factory.create(dispatcherName, getMap());
+    return factory.create(getMap());
   }
 
   public RegistrationHandle register(final CommandName name,
@@ -84,7 +76,7 @@
     };
   }
 
-  private ConcurrentMap<String, Provider<Command>> getMap() {
+  ConcurrentMap<String, Provider<Command>> getMap() {
     if (map == null) {
       synchronized (this) {
         if (map == null) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
index be513b3..d2fdf78 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
@@ -58,6 +58,7 @@
 
   static class SendMessage implements Command, SessionAware {
     private final Provider<MessageFactory> messageFactory;
+    private final SshScope sshScope;
 
     private InputStream in;
     private OutputStream out;
@@ -66,8 +67,9 @@
     private Context context;
 
     @Inject
-    SendMessage(Provider<MessageFactory> messageFactory) {
+    SendMessage(Provider<MessageFactory> messageFactory, SshScope sshScope) {
       this.messageFactory = messageFactory;
+      this.sshScope = sshScope;
     }
 
     public void setInputStream(final InputStream in) {
@@ -87,16 +89,16 @@
     }
 
     public void setSession(final ServerSession session) {
-      this.context = new Context(session.getAttribute(SshSession.KEY), "");
+      this.context = sshScope.newContext(session.getAttribute(SshSession.KEY), "");
     }
 
     public void start(final Environment env) throws IOException {
-      Context old = SshScope.set(context);
+      Context old = sshScope.set(context);
       String message;
       try {
         message = messageFactory.get().getMessage();
       } finally {
-        SshScope.set(old);
+        sshScope.set(old);
       }
       err.write(Constants.encode(message.toString()));
       err.flush();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
similarity index 97%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginCommandModule.java
rename to gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
index 28d267c..4dbb8d7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.sshd.commands;
+package com.google.gerrit.sshd;
 
 import com.google.common.base.Preconditions;
 import com.google.gerrit.extensions.annotations.PluginName;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
index b843893..03485f7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
@@ -69,6 +69,6 @@
   @Override
   public Module create() throws InvalidPluginException {
     Preconditions.checkState(command != null, "pluginName must be provided");
-    return this;
+    return !commands.isEmpty() ? this : null;
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCurrentUserProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCurrentUserProvider.java
deleted file mode 100644
index 5839cf1..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCurrentUserProvider.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.sshd;
-
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-@Singleton
-class SshCurrentUserProvider implements Provider<CurrentUser> {
-  private final Provider<SshSession> session;
-  private final Provider<IdentifiedUser> identifiedProvider;
-
-  @Inject
-  SshCurrentUserProvider(Provider<SshSession> s, Provider<IdentifiedUser> p) {
-    session = s;
-    identifiedProvider = p;
-  }
-
-  @Override
-  public CurrentUser get() {
-    final CurrentUser user = session.get().getCurrentUser();
-    if (user instanceof IdentifiedUser) {
-      return identifiedProvider.get();
-    }
-    return session.get().getCurrentUser();
-  }
-}
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 d382a57..664ce45 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
@@ -18,7 +18,7 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.gerrit.common.Version;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.ssh.SshInfo;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshIdentifiedUserProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshIdentifiedUserProvider.java
deleted file mode 100644
index 516b59c..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshIdentifiedUserProvider.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.sshd;
-
-import com.google.gerrit.common.errors.NotSignedInException;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-import com.google.inject.Singleton;
-
-@Singleton
-class SshIdentifiedUserProvider implements Provider<IdentifiedUser> {
-  private final Provider<SshSession> session;
-  private final IdentifiedUser.RequestFactory factory;
-
-  @Inject
-  SshIdentifiedUserProvider(Provider<SshSession> s,
-      IdentifiedUser.RequestFactory f) {
-    session = s;
-    factory = f;
-  }
-
-  @Override
-  public IdentifiedUser get() {
-    final CurrentUser user = session.get().getCurrentUser();
-    if (user instanceof IdentifiedUser) {
-      return factory.create(user.getAccessPath(), //
-          ((IdentifiedUser) user).getAccountId());
-    }
-    throw new ProvisionException(NotSignedInException.MESSAGE,
-        new NotSignedInException());
-  }
-}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index 1f5ac28..0a1f708 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -16,13 +16,13 @@
 
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
 
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
@@ -42,6 +42,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.ExecutionException;
 
 /** Provides the {@link SshKeyCacheEntry}. */
 @Singleton
@@ -57,9 +58,10 @@
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<String, Iterable<SshKeyCacheEntry>>> type =
-            new TypeLiteral<Cache<String, Iterable<SshKeyCacheEntry>>>() {};
-        core(type, CACHE_NAME).populateWith(Loader.class);
+        cache(CACHE_NAME,
+            String.class,
+            new TypeLiteral<Iterable<SshKeyCacheEntry>>(){})
+          .loader(Loader.class);
         bind(SshKeyCacheImpl.class);
         bind(SshKeyCache.class).to(SshKeyCacheImpl.class);
       }
@@ -71,20 +73,27 @@
         .asList(new SshKeyCacheEntry[0]));
   }
 
-  private final Cache<String, Iterable<SshKeyCacheEntry>> cache;
+  private final LoadingCache<String, Iterable<SshKeyCacheEntry>> cache;
 
   @Inject
   SshKeyCacheImpl(
-      @Named(CACHE_NAME) final Cache<String, Iterable<SshKeyCacheEntry>> cache) {
+      @Named(CACHE_NAME) LoadingCache<String, Iterable<SshKeyCacheEntry>> cache) {
     this.cache = cache;
   }
 
-  public Iterable<SshKeyCacheEntry> get(String username) {
-    return cache.get(username);
+  Iterable<SshKeyCacheEntry> get(String username) {
+    try {
+      return cache.get(username);
+    } catch (ExecutionException e) {
+      log.warn("Cannot load SSH keys for " + username, e);
+      return Collections.emptyList();
+    }
   }
 
   public void evict(String username) {
-    cache.remove(username);
+    if (username != null) {
+      cache.invalidate(username);
+    }
   }
 
   @Override
@@ -107,7 +116,7 @@
     }
   }
 
-  static class Loader extends EntryCreator<String, Iterable<SshKeyCacheEntry>> {
+  static class Loader extends CacheLoader<String, Iterable<SshKeyCacheEntry>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
@@ -116,8 +125,7 @@
     }
 
     @Override
-    public Iterable<SshKeyCacheEntry> createEntry(String username)
-        throws Exception {
+    public Iterable<SshKeyCacheEntry> load(String username) throws Exception {
       final ReviewDb db = schema.open();
       try {
         final AccountExternalId.Key key =
@@ -143,11 +151,6 @@
       }
     }
 
-    @Override
-    public Iterable<SshKeyCacheEntry> missing(String username) {
-      return Collections.emptyList();
-    }
-
     private void add(ReviewDb db, List<SshKeyCacheEntry> kl, AccountSshKey k) {
       try {
         kl.add(new SshKeyCacheEntry(k.getKey(), SshUtil.parse(k)));
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 32d5a07..8fbea9d 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
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.sshd;
 
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
index bc094f9..7f4a1f7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
@@ -15,60 +15,59 @@
 package com.google.gerrit.sshd;
 
 import static com.google.inject.Scopes.SINGLETON;
+import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
 
+import com.google.common.collect.Maps;
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.reviewdb.client.Account;
-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.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.CmdLineParserModule;
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.ChangeUserName;
 import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GerritRequestModule;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.QueueProvider;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.plugins.ModuleGenerator;
 import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.plugins.StartPluginListener;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.gerrit.sshd.args4j.AccountGroupIdHandler;
-import com.google.gerrit.sshd.args4j.AccountGroupUUIDHandler;
-import com.google.gerrit.sshd.args4j.AccountIdHandler;
-import com.google.gerrit.sshd.args4j.ChangeIdHandler;
-import com.google.gerrit.sshd.args4j.ObjectIdHandler;
-import com.google.gerrit.sshd.args4j.PatchSetIdHandler;
-import com.google.gerrit.sshd.args4j.ProjectControlHandler;
-import com.google.gerrit.sshd.args4j.SocketAddressHandler;
 import com.google.gerrit.sshd.commands.DefaultCommandModule;
 import com.google.gerrit.sshd.commands.QueryShell;
-import com.google.gerrit.util.cli.CmdLineParser;
-import com.google.gerrit.util.cli.OptionHandlerUtil;
+import com.google.inject.Inject;
 import com.google.inject.internal.UniqueAnnotations;
 import com.google.inject.servlet.RequestScoped;
 
 import org.apache.sshd.common.KeyPairProvider;
 import org.apache.sshd.server.CommandFactory;
 import org.apache.sshd.server.PublickeyAuthenticator;
-import org.eclipse.jgit.lib.ObjectId;
-import org.kohsuke.args4j.spi.OptionHandler;
-
+import org.eclipse.jgit.lib.Config;
 import java.net.SocketAddress;
+import java.util.Map;
 
 /** Configures standard dependencies for {@link SshDaemon}. */
 public class SshModule extends FactoryModule {
+  private final Map<String, String> aliases;
+
+  @Inject
+  SshModule(@GerritServerConfig Config cfg) {
+    aliases = Maps.newHashMap();
+    for (String name : cfg.getNames("ssh-alias")) {
+      aliases.put(name, cfg.getString("ssh-alias", null, name));
+    }
+  }
+
   @Override
   protected void configure() {
     bindScope(RequestScoped.class, SshScope.REQUEST);
     bind(RequestScopePropagator.class).to(SshScope.Propagator.class);
+    bind(SshScope.class).in(SINGLETON);
 
     configureRequestScope();
-    configureCmdLineParser();
+    install(new CmdLineParserModule());
+    configureAliases();
 
     install(SshKeyCacheImpl.module());
     bind(SshLog.class);
@@ -78,7 +77,7 @@
     factory(PeerDaemonUser.Factory.class);
 
     bind(DispatchCommandProvider.class).annotatedWith(Commands.CMD_ROOT)
-        .toInstance(new DispatchCommandProvider("", Commands.CMD_ROOT));
+        .toInstance(new DispatchCommandProvider(Commands.CMD_ROOT));
     bind(CommandFactoryProvider.class);
     bind(CommandFactory.class).toProvider(CommandFactoryProvider.class);
     bind(WorkQueue.Executor.class).annotatedWith(StreamCommandExecutor.class)
@@ -105,12 +104,27 @@
           .annotatedWith(UniqueAnnotations.create())
           .to(SshPluginStarterCallback.class);
 
+        listener().toInstance(registerInParentInjectors());
         listener().to(SshLog.class);
         listener().to(SshDaemon.class);
       }
     });
   }
 
+  private void configureAliases() {
+    CommandName gerrit = Commands.named("gerrit");
+    for (Map.Entry<String, String> e : aliases.entrySet()) {
+      String name = e.getKey();
+      String[] dest = e.getValue().split("[ \\t]+");
+      CommandName cmd = Commands.named(dest[0]);
+      for (int i = 1; i < dest.length; i++) {
+        cmd = Commands.named(cmd, dest[i]);
+      }
+      bind(Commands.key(gerrit, name))
+        .toProvider(new AliasCommandProvider(cmd));
+    }
+  }
+
   private void configureRequestScope() {
     bind(SshScope.Context.class).toProvider(SshScope.ContextProvider.class);
 
@@ -119,32 +133,9 @@
     bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
         SshRemotePeerProvider.class).in(SshScope.REQUEST);
 
-    bind(CurrentUser.class).toProvider(SshCurrentUserProvider.class).in(
-        SshScope.REQUEST);
-    bind(IdentifiedUser.class).toProvider(SshIdentifiedUserProvider.class).in(
-        SshScope.REQUEST);
-
     bind(WorkQueue.Executor.class).annotatedWith(CommandExecutor.class)
         .toProvider(CommandExecutorProvider.class).in(SshScope.REQUEST);
 
     install(new GerritRequestModule());
   }
-
-  private void configureCmdLineParser() {
-    factory(CmdLineParser.Factory.class);
-
-    registerOptionHandler(Account.Id.class, AccountIdHandler.class);
-    registerOptionHandler(AccountGroup.Id.class, AccountGroupIdHandler.class);
-    registerOptionHandler(AccountGroup.UUID.class, AccountGroupUUIDHandler.class);
-    registerOptionHandler(Change.Id.class, ChangeIdHandler.class);
-    registerOptionHandler(ObjectId.class, ObjectIdHandler.class);
-    registerOptionHandler(PatchSet.Id.class, PatchSetIdHandler.class);
-    registerOptionHandler(ProjectControl.class, ProjectControlHandler.class);
-    registerOptionHandler(SocketAddress.class, SocketAddressHandler.class);
-  }
-
-  private <T> void registerOptionHandler(Class<T> type,
-      Class<? extends OptionHandler<T>> impl) {
-    install(OptionHandlerUtil.moduleFor(type, impl));
-  }
 }
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 92609b5..d6f66ca 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
@@ -14,8 +14,13 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RequestCleanup;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
+import com.google.inject.Inject;
 import com.google.inject.Key;
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
@@ -26,10 +31,10 @@
 
 /** Guice scopes for state during an SSH connection. */
 class SshScope {
-  static class Context {
-    private static final Key<RequestCleanup> RC_KEY =
-        Key.get(RequestCleanup.class);
+  private static final Key<RequestCleanup> RC_KEY =
+      Key.get(RequestCleanup.class);
 
+  class Context implements RequestContext {
     private final RequestCleanup cleanup;
     private final SshSession session;
     private final String commandLine;
@@ -56,10 +61,6 @@
       finished = p.finished;
     }
 
-    Context(final SshSession s, final String c) {
-      this(s, c, System.currentTimeMillis());
-    }
-
     String getCommandLine() {
       return commandLine;
     }
@@ -68,6 +69,16 @@
       return session;
     }
 
+    @Override
+    public CurrentUser getCurrentUser() {
+      final CurrentUser user = session.getCurrentUser();
+      if (user instanceof IdentifiedUser) {
+        return userFactory.create(user.getAccessPath(), //
+            ((IdentifiedUser) user).getAccountId());
+      }
+      return user;
+    }
+
     synchronized <T> T get(Key<T> key, Provider<T> creator) {
       @SuppressWarnings("unchecked")
       T t = (T) map.get(key);
@@ -100,15 +111,19 @@
   }
 
   static class Propagator extends ThreadLocalRequestScopePropagator<Context> {
-    Propagator() {
-      super(REQUEST, current);
+    private final SshScope sshScope;
+
+    @Inject
+    Propagator(SshScope sshScope, ThreadLocalRequestContext local) {
+      super(REQUEST, current, local);
+      this.sshScope = sshScope;
     }
 
     @Override
     protected Context continuingContext(Context ctx) {
       // The cleanup is not chained, since the RequestScopePropagator executors
       // the Context's cleanup when finished executing.
-      return new Context(ctx, ctx.getSession(), ctx.getCommandLine());
+      return sshScope.newContinuingContext(ctx);
     }
   }
 
@@ -123,9 +138,28 @@
     return ctx;
   }
 
-  static Context set(Context ctx) {
+  private final ThreadLocalRequestContext local;
+  private final IdentifiedUser.RequestFactory userFactory;
+
+  @Inject
+  SshScope(ThreadLocalRequestContext local,
+      IdentifiedUser.RequestFactory userFactory) {
+    this.local = local;
+    this.userFactory = userFactory;
+  }
+
+  Context newContext(SshSession session, String commandLine) {
+    return new Context(session, commandLine, System.currentTimeMillis());
+  }
+
+  private Context newContinuingContext(Context ctx) {
+    return new Context(ctx, ctx.getSession(), ctx.getCommandLine());
+  }
+
+  Context set(Context ctx) {
     Context old = current.get();
     current.set(ctx);
+    local.setContext(ctx);
     return old;
   }
 
@@ -149,7 +183,4 @@
       return "SshScopes.REQUEST";
     }
   };
-
-  private SshScope() {
-  }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
index b4c59a8..33459c2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
@@ -43,6 +43,7 @@
  * key, or a key on this daemon's peer host key ring.
  */
 public final class SuExec extends BaseCommand {
+  private final SshScope sshScope;
   private final DispatchCommandProvider dispatcher;
 
   private Provider<CurrentUser> caller;
@@ -62,10 +63,12 @@
   private final AtomicReference<Command> atomicCmd;
 
   @Inject
-  SuExec(@CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher,
+  SuExec(final SshScope sshScope,
+      @CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher,
       final Provider<CurrentUser> caller, final Provider<SshSession> session,
       final IdentifiedUser.GenericFactory userFactory,
       final SshScope.Context callingContext) {
+    this.sshScope = sshScope;
     this.dispatcher = dispatcher;
     this.caller = caller;
     this.session = session;
@@ -81,7 +84,7 @@
         parseCommandLine();
 
         final Context ctx = callingContext.subContext(newSession(), join(args));
-        final Context old = SshScope.set(ctx);
+        final Context old = sshScope.set(ctx);
         try {
           final BaseCommand cmd = dispatcher.get();
           cmd.setArguments(args.toArray(new String[args.size()]));
@@ -89,7 +92,7 @@
           atomicCmd.set(cmd);
           cmd.start(env);
         } finally {
-          SshScope.set(old);
+          sshScope.set(old);
         }
 
       } else {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
index 5f1992c..08c650c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.sshd.AdminHighPriorityCommand;
-import com.google.gerrit.sshd.RequiresCapability;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
index 047cdd4..cfd917c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.MetaDataUpdate;
@@ -22,7 +23,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.sshd.RequiresCapability;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
index f13e1a6..4350d1e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.server.git.BanCommit;
 import com.google.gerrit.server.git.BanCommitResult;
-import com.google.gerrit.server.git.IncompleteUserInfoException;
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.sshd.SshCommand;
@@ -77,8 +76,6 @@
       throw die(e);
     } catch (IOException e) {
       throw die(e);
-    } catch (IncompleteUserInfoException e) {
-      throw die(e);
     } catch (MergeException e) {
       throw die(e);
     } catch (InterruptedException e) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
index 1e7c5b3..500c84a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
@@ -14,37 +14,33 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.ehcache.EhcachePoolImpl;
+import com.google.common.cache.Cache;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
-import net.sf.ehcache.CacheManager;
-import net.sf.ehcache.Ehcache;
-
-import java.util.Arrays;
 import java.util.SortedSet;
-import java.util.TreeSet;
 
 abstract class CacheCommand extends SshCommand {
   @Inject
-  protected EhcachePoolImpl cachePool;
+  protected DynamicMap<Cache<?, ?>> cacheMap;
 
   protected SortedSet<String> cacheNames() {
-    final SortedSet<String> names = new TreeSet<String>();
-    for (final Ehcache c : getAllCaches()) {
-      names.add(c.getName());
+    SortedSet<String> names = Sets.newTreeSet();
+    for (String plugin : cacheMap.plugins()) {
+      for (String name : cacheMap.byPlugin(plugin).keySet()) {
+        names.add(cacheNameOf(plugin, name));
+      }
     }
     return names;
   }
 
-  protected Ehcache[] getAllCaches() {
-    final CacheManager cacheMgr = cachePool.getCacheManager();
-    final String[] cacheNames = cacheMgr.getCacheNames();
-    Arrays.sort(cacheNames);
-    final Ehcache[] r = new Ehcache[cacheNames.length];
-    for (int i = 0; i < cacheNames.length; i++) {
-      r[i] = cacheMgr.getEhcache(cacheNames[i]);
+  protected String cacheNameOf(String plugin, String name) {
+    if ("gerrit".equals(plugin)) {
+      return name;
+    } else {
+      return plugin + "." + name;
     }
-    return r;
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index 29f2294..ac7ee08 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -27,7 +28,6 @@
 import com.google.gerrit.server.account.AccountByEmailCache;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gerrit.sshd.RequiresCapability;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 28b6f48..728c20c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -17,10 +17,10 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.common.errors.PermissionDeniedException;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.PerformCreateGroup;
-import com.google.gerrit.sshd.RequiresCapability;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
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 1f5bc6f..5f5b1e3 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
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.errors.ProjectCreationFailedException;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.Project.SubmitType;
@@ -23,11 +24,9 @@
 import com.google.gerrit.server.project.CreateProjectArgs;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.SuggestParentCandidates;
-import com.google.gerrit.sshd.RequiresCapability;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
-import org.eclipse.jgit.lib.Constants;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -79,7 +78,7 @@
 
   @Option(name = "--branch", aliases = {"-b"}, metaVar = "BRANCH", usage = "initial branch name\n"
       + "(default: master)")
-  private String branch = Constants.MASTER;
+  private List<String> branch;
 
   @Option(name = "--empty-commit", usage = "to create initial empty commit")
   private boolean createEmptyCommit;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
index 9ba20ed..fa63041 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -14,18 +14,19 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.common.cache.Cache;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.sshd.BaseCommand;
-import com.google.gerrit.sshd.RequiresCapability;
 import com.google.inject.Inject;
-
-import net.sf.ehcache.Ehcache;
+import com.google.inject.Provider;
 
 import org.kohsuke.args4j.Option;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.SortedSet;
 
 /** Causes the caches to purge all entries and reload. */
@@ -95,13 +96,16 @@
 
   private void doBulkFlush() {
     try {
-      for (final Ehcache c : getAllCaches()) {
-        final String name = c.getName();
-        if (flush(name)) {
-          try {
-            c.removeAll();
-          } catch (Throwable e) {
-            stderr.println("error: cannot flush cache \"" + name + "\": " + e);
+      for (String plugin : cacheMap.plugins()) {
+        for (Map.Entry<String, Provider<Cache<?, ?>>> entry :
+            cacheMap.byPlugin(plugin).entrySet()) {
+          String n = cacheNameOf(plugin, entry.getKey());
+          if (flush(n)) {
+            try {
+              entry.getValue().get().invalidateAll();
+            } catch (Throwable err) {
+              stderr.println("error: cannot flush cache \"" + n + "\": " + err);
+            }
           }
         }
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
index 12ab225..83e88e5 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.Task;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.gerrit.sshd.AdminHighPriorityCommand;
-import com.google.gerrit.sshd.RequiresCapability;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
index 9eeaf74..c5cdbd5 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
@@ -33,7 +33,6 @@
     command(gerrit, "gsql").to(AdminQueryShell.class);
     command(gerrit, "set-reviewers").to(SetReviewersCommand.class);
     command(gerrit, "receive-pack").to(Receive.class);
-    command(gerrit, "replicate").to(Replicate.class);
     command(gerrit, "set-project-parent").to(AdminSetParent.class);
     command(gerrit, "review").to(ReviewCommand.class);
     command(gerrit, "set-account").to(SetAccountCommand.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
index 2328847..12722ec 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
@@ -16,9 +16,9 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.plugins.PluginInstallException;
 import com.google.gerrit.server.plugins.PluginLoader;
-import com.google.gerrit.sshd.RequiresCapability;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
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 6044151..6d7490f 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
@@ -14,38 +14,29 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.server.plugins.Plugin;
-import com.google.gerrit.server.plugins.PluginLoader;
-import com.google.gerrit.sshd.RequiresCapability;
-import com.google.gerrit.sshd.SshCommand;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.plugins.ListPlugins;
+import com.google.gerrit.sshd.BaseCommand;
 import com.google.inject.Inject;
 
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
+import org.apache.sshd.server.Environment;
+
+import java.io.IOException;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-final class PluginLsCommand extends SshCommand {
+final class PluginLsCommand extends BaseCommand {
   @Inject
-  private PluginLoader loader;
+  private ListPlugins impl;
 
   @Override
-  protected void run() {
-    List<Plugin> running = loader.getPlugins();
-    Collections.sort(running, new Comparator<Plugin>() {
+  public void start(Environment env) throws IOException {
+    startThread(new CommandRunnable() {
       @Override
-      public int compare(Plugin a, Plugin b) {
-        return a.getName().compareTo(b.getName());
+      public void run() throws Exception {
+        parseCommandLine(impl);
+        impl.display(out);
       }
     });
-
-    stdout.format("%-30s %-10s\n", "Name", "Version");
-    stdout.print("----------------------------------------------------------------------\n");
-    for (Plugin p : running) {
-      stdout.format("%-30s %-10s\n", p.getName(),
-          Strings.nullToEmpty(p.getVersion()));
-    }
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
index 4b76942..d2429a9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
@@ -15,18 +15,37 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.server.plugins.InvalidPluginException;
+import com.google.gerrit.server.plugins.PluginInstallException;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.plugins.PluginLoader;
-import com.google.gerrit.sshd.RequiresCapability;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
+import org.kohsuke.args4j.Argument;
+
+import java.util.List;
+
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 final class PluginReloadCommand extends SshCommand {
+  @Argument(index = 0, metaVar = "NAME", usage = "plugins to reload/restart")
+  private List<String> names;
+
   @Inject
   private PluginLoader loader;
 
   @Override
-  protected void run() {
-    loader.rescan(true);
+  protected void run() throws UnloggedFailure {
+    if (names == null || names.isEmpty()) {
+      loader.rescan();
+    } else {
+      try {
+        loader.reload(names);
+      } catch (InvalidPluginException e) {
+        throw die(e.getMessage());
+      } catch (PluginInstallException e) {
+        throw die(e.getMessage());
+      }
+    }
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
index 6444e71..8baab77 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
@@ -16,8 +16,8 @@
 
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.plugins.PluginLoader;
-import com.google.gerrit.sshd.RequiresCapability;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java
deleted file mode 100644
index 0a3f336..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.sshd.commands;
-
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.git.PushAllProjectsOp;
-import com.google.gerrit.server.git.ReplicationQueue;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.sshd.RequiresCapability;
-import com.google.gerrit.sshd.SshCommand;
-import com.google.inject.Inject;
-
-import org.kohsuke.args4j.Argument;
-import org.kohsuke.args4j.Option;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-/** Force a project to replicate, again. */
-@RequiresCapability(GlobalCapability.START_REPLICATION)
-final class Replicate extends SshCommand {
-  @Option(name = "--all", usage = "push all known projects")
-  private boolean all;
-
-  @Option(name = "--url", metaVar = "PATTERN", usage = "pattern to match URL on")
-  private String urlMatch;
-
-  @Argument(index = 0, multiValued = true, metaVar = "PROJECT", usage = "project name")
-  private List<String> projectNames = new ArrayList<String>(2);
-
-  @Inject
-  IdentifiedUser currentUser;
-
-  @Inject
-  private PushAllProjectsOp.Factory pushAllOpFactory;
-
-  @Inject
-  private ReplicationQueue replication;
-
-  @Inject
-  private ProjectCache projectCache;
-
-  @Override
-  protected void run() throws Failure {
-    if (all && projectNames.size() > 0) {
-      throw new UnloggedFailure(1, "error: cannot combine --all and PROJECT");
-    }
-
-    if (!replication.isEnabled()) {
-      throw new Failure(1, "error: replication not enabled");
-    }
-
-    if (all) {
-      pushAllOpFactory.create(urlMatch).start(0, TimeUnit.SECONDS);
-
-    } else {
-      for (final String name : projectNames) {
-        final Project.NameKey key = new Project.NameKey(name);
-        if (projectCache.get(key) != null) {
-          replication.scheduleFullSync(key, urlMatch);
-        } else {
-          throw new UnloggedFailure(1, "error: '" + name + "': not a Gerrit project");
-        }
-      }
-    }
-  }
-}
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 f38e17e..236d9f1 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
@@ -248,6 +248,9 @@
         case CHANGE_IS_CLOSED:
           errMsg += "change is closed";
           break;
+        case CHANGE_NOT_ABANDONED:
+          errMsg += "change is not abandoned";
+          break;
         case PUBLISH_NOT_PERMITTED:
           errMsg += "not permitted to publish change";
           break;
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 f8d3c41f..bdcb4fb 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
@@ -14,22 +14,24 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheStats;
+import com.google.common.collect.Maps;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.cache.h2.H2CacheImpl;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.Task;
-import com.google.gerrit.sshd.RequiresCapability;
 import com.google.gerrit.sshd.SshDaemon;
 import com.google.inject.Inject;
-
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.Statistics;
-import net.sf.ehcache.config.CacheConfiguration;
+import com.google.inject.Provider;
 
 import org.apache.mina.core.service.IoAcceptor;
 import org.apache.mina.core.session.IoSession;
+import org.apache.sshd.server.Environment;
 import org.eclipse.jgit.storage.file.WindowCacheStatAccessor;
 import org.kohsuke.args4j.Option;
 
@@ -43,6 +45,8 @@
 import java.text.SimpleDateFormat;
 import java.util.Collection;
 import java.util.Date;
+import java.util.Map;
+import java.util.SortedMap;
 
 /** Show the current cache states. */
 @RequiresCapability(GlobalCapability.VIEW_CACHES)
@@ -76,8 +80,26 @@
   @SitePath
   private File sitePath;
 
+  @Option(name = "--width", aliases = {"-w"}, metaVar = "COLS", usage = "width of output table")
+  private int columns = 80;
+  private int nw;
+
+  @Override
+  public void start(Environment env) throws IOException {
+    String s = env.getEnv().get(Environment.ENV_COLUMNS);
+    if (s != null && !s.isEmpty()) {
+      try {
+        columns = Integer.parseInt(s);
+      } catch (NumberFormatException err) {
+        columns = 80;
+      }
+    }
+    super.start(env);
+  }
+
   @Override
   protected void run() {
+    nw = columns - 50;
     Date now = new Date();
     stdout.format(
         "%-25s %-20s      now  %16s\n",
@@ -91,60 +113,46 @@
     stdout.print('\n');
 
     stdout.print(String.format(//
-        "%1s %-18s %-4s|%-20s|  %-5s  |%-14s|\n" //
+        "%1s %-"+nw+"s|%-21s|  %-5s |%-9s|\n" //
         , "" //
         , "Name" //
-        , "Max" //
-        , "Object Count" //
+        , "Entries" //
         , "AvgGet" //
         , "Hit Ratio" //
     ));
     stdout.print(String.format(//
-        "%1s %-18s %-4s|%6s %6s %6s|  %-5s   |%-4s %-4s %-4s|\n" //
+        "%1s %-"+nw+"s|%6s %6s %7s|  %-5s  |%-4s %-4s|\n" //
         , "" //
         , "" //
-        , "Age" //
-        , "Disk" //
         , "Mem" //
-        , "Cnt" //
-        , "" //
         , "Disk" //
+        , "Space" //
+        , "" //
         , "Mem" //
-        , "Agg" //
+        , "Disk" //
     ));
-    stdout.print("------------------"
-        + "-------+--------------------+----------+--------------+\n");
-    for (final Ehcache cache : getAllCaches()) {
-      final CacheConfiguration cfg = cache.getCacheConfiguration();
-      final boolean useDisk = cfg.isDiskPersistent() || cfg.isOverflowToDisk();
-      final Statistics stat = cache.getStatistics();
-      final long total = stat.getCacheHits() + stat.getCacheMisses();
+    stdout.print("--");
+    for (int i = 0; i < nw; i++) {
+      stdout.print('-');
+    }
+    stdout.print("+---------------------+---------+---------+\n");
 
-      if (useDisk) {
-        stdout.print(String.format(//
-            "D %-18s %-4s|%6s %6s %6s| %7s  |%4s %4s %4s|\n" //
-            , cache.getName() //
-            , interval(cfg.getTimeToLiveSeconds()) //
-            , count(stat.getDiskStoreObjectCount()) //
-            , count(stat.getMemoryStoreObjectCount()) //
-            , count(stat.getObjectCount()) //
-            , duration(stat.getAverageGetTime()) //
-            , percent(stat.getOnDiskHits(), total) //
-            , percent(stat.getInMemoryHits(), total) //
-            , percent(stat.getCacheHits(), total) //
-            ));
-      } else {
-        stdout.print(String.format(//
-            "  %-18s %-4s|%6s %6s %6s| %7s  |%4s %4s %4s|\n" //
-            , cache.getName() //
-            , interval(cfg.getTimeToLiveSeconds()) //
-            , "", "" //
-            , count(stat.getObjectCount()) //
-            , duration(stat.getAverageGetTime()) //
-            , "", "" //
-            , percent(stat.getCacheHits(), total) //
-            ));
-      }
+    Map<String, H2CacheImpl<?, ?>> disks = Maps.newTreeMap();
+    printMemoryCaches(disks, sortedCoreCaches());
+    printMemoryCaches(disks, sortedPluginCaches());
+    for (Map.Entry<String, H2CacheImpl<?, ?>> entry : disks.entrySet()) {
+      H2CacheImpl<?, ?> cache = entry.getValue();
+      CacheStats stat = cache.stats();
+      H2CacheImpl.DiskStats disk = cache.diskStats();
+      stdout.print(String.format(
+          "D %-"+nw+"s|%6s %6s %7s| %7s |%4s %4s|\n",
+          entry.getKey(),
+          count(cache.size()),
+          count(disk.size()),
+          bytes(disk.space()),
+          duration(stat.averageLoadPenalty()),
+          percent(stat.hitCount(), stat.requestCount()),
+          percent(disk.hitCount(), disk.requestCount())));
     }
     stdout.print('\n');
 
@@ -165,6 +173,51 @@
     stdout.flush();
   }
 
+  private void printMemoryCaches(
+      Map<String, H2CacheImpl<?, ?>> disks,
+      Map<String, Cache<?,?>> caches) {
+    for (Map.Entry<String, Cache<?,?>> entry : caches.entrySet()) {
+      Cache<?,?> cache = entry.getValue();
+      if (cache instanceof H2CacheImpl) {
+        disks.put(entry.getKey(), (H2CacheImpl<?,?>)cache);
+        continue;
+      }
+      CacheStats stat = cache.stats();
+      stdout.print(String.format(
+          "  %-"+nw+"s|%6s %6s %7s| %7s |%4s %4s|\n",
+          entry.getKey(),
+          count(cache.size()),
+          "",
+          "",
+          duration(stat.averageLoadPenalty()),
+          percent(stat.hitCount(), stat.requestCount()),
+          ""));
+    }
+  }
+
+  private Map<String, Cache<?, ?>> sortedCoreCaches() {
+    SortedMap<String, Cache<?, ?>> m = Maps.newTreeMap();
+    for (Map.Entry<String, Provider<Cache<?, ?>>> entry :
+        cacheMap.byPlugin("gerrit").entrySet()) {
+      m.put(cacheNameOf("gerrit", entry.getKey()), entry.getValue().get());
+    }
+    return m;
+  }
+
+  private Map<String, Cache<?, ?>> sortedPluginCaches() {
+    SortedMap<String, Cache<?, ?>> m = Maps.newTreeMap();
+    for (String plugin : cacheMap.plugins()) {
+      if ("gerrit".equals(plugin)) {
+        continue;
+      }
+      for (Map.Entry<String, Provider<Cache<?, ?>>> entry :
+          cacheMap.byPlugin(plugin).entrySet()) {
+        m.put(cacheNameOf(plugin, entry.getKey()), entry.getValue().get());
+      }
+    }
+    return m;
+  }
+
   private void memSummary() {
     final Runtime r = Runtime.getRuntime();
     final long mMax = r.maxMemory();
@@ -300,45 +353,24 @@
     return String.format("%6d", cnt);
   }
 
-  private String duration(double ms) {
-    if (Math.abs(ms) <= 0.05) {
+  private String duration(double ns) {
+    if (ns < 0.5) {
       return "";
     }
-    String suffix = "ms";
-    if (ms >= 1000) {
-      ms /= 1000;
+    String suffix = "ns";
+    if (ns >= 1000.0) {
+      ns /= 1000.0;
+      suffix = "us";
+    }
+    if (ns >= 1000.0) {
+      ns /= 1000.0;
+      suffix = "ms";
+    }
+    if (ns >= 1000.0) {
+      ns /= 1000.0;
       suffix = "s ";
     }
-    return String.format("%4.1f%s", ms, suffix);
-  }
-
-  private String interval(double ttl) {
-    if (ttl == 0) {
-      return "inf";
-    }
-
-    String suffix = "s";
-    if (ttl >= 60) {
-      ttl /= 60;
-      suffix = "m";
-
-      if (ttl >= 60) {
-        ttl /= 60;
-        suffix = "h";
-      }
-
-      if (ttl >= 24) {
-        ttl /= 24;
-        suffix = "d";
-
-        if (ttl >= 365) {
-          ttl /= 365;
-          suffix = "y";
-        }
-      }
-    }
-
-    return Integer.toString((int) ttl) + suffix;
+    return String.format("%4.1f%s", ns, suffix);
   }
 
   private String percent(final long value, final long total) {
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 4085dcb..a1a5b8f 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
@@ -15,10 +15,10 @@
 package com.google.gerrit.sshd.commands;
 
 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.sshd.RequiresCapability;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.sshd.SshDaemon;
 import com.google.gerrit.sshd.SshSession;
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java
index 233d53d..52467a0 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.httpd;
 
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
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 8db75e2..519dec8 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
@@ -18,12 +18,12 @@
 import static com.google.inject.Stage.PRODUCTION;
 
 import com.google.gerrit.common.ChangeHookRunner;
-import com.google.gerrit.ehcache.EhcachePoolImpl;
 import com.google.gerrit.httpd.auth.openid.OpenIdModule;
 import com.google.gerrit.httpd.plugins.HttpPluginModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.AuthConfigModule;
 import com.google.gerrit.server.config.CanonicalWebUrlModule;
@@ -33,7 +33,6 @@
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.contact.HttpContactStoreConnection;
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
-import com.google.gerrit.server.git.PushReplication;
 import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
@@ -201,10 +200,9 @@
     modules.add(new ChangeHookRunner.Module());
     modules.add(new ReceiveCommitsExecutorModule());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
-    modules.add(new EhcachePoolImpl.Module());
+    modules.add(new DefaultCacheFactory.Module());
     modules.add(new SmtpEmailSender.Module());
     modules.add(new SignedTokenEmailTokenVerifier.Module());
-    modules.add(new PushReplication.Module());
     modules.add(new PluginModule());
     modules.add(new CanonicalWebUrlModule() {
       @Override
@@ -218,13 +216,14 @@
 
   private Injector createSshInjector() {
     final List<Module> modules = new ArrayList<Module>();
-    modules.add(new SshModule());
+    modules.add(sysInjector.getInstance(SshModule.class));
     modules.add(new MasterCommandModule());
     return sysInjector.createChildInjector(modules);
   }
 
   private Injector createWebInjector() {
     final List<Module> modules = new ArrayList<Module>();
+    modules.add(RequestContextFilter.module());
     modules.add(sysInjector.getInstance(GitOverHttpModule.class));
     modules.add(sshInjector.getInstance(WebModule.class));
     modules.add(sshInjector.getInstance(WebSshGlueModule.class));
diff --git a/gerrit-war/src/main/resources/log4j.properties b/gerrit-war/src/main/resources/log4j.properties
index 5993790..45f630e 100644
--- a/gerrit-war/src/main/resources/log4j.properties
+++ b/gerrit-war/src/main/resources/log4j.properties
@@ -48,10 +48,6 @@
 log4j.logger.org.openid4java.server.RealmVerifier=ERROR
 log4j.logger.org.openid4java.message.AuthSuccess=ERROR
 
-# Silence non-critical messages from ehcache
-#
-log4j.logger.net.sf.ehcache=WARN
-
 # Silence non-critical messages from c3p0 (if used).
 #
 log4j.logger.com.mchange.v2.c3p0=WARN
diff --git a/pom.xml b/pom.xml
index f366c4d..440bd1c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -46,7 +46,7 @@
   </issueManagement>
 
   <properties>
-    <jgitVersion>1.3.0.201202151440-r.75-gff13648</jgitVersion>
+    <jgitVersion>2.0.0.201206130900-r.23-gb3dbf19</jgitVersion>
     <gwtormVersion>1.4</gwtormVersion>
     <gwtjsonrpcVersion>1.3</gwtjsonrpcVersion>
     <gwtexpuiVersion>1.2.5</gwtexpuiVersion>
@@ -74,7 +74,7 @@
 
     <module>gerrit-antlr</module>
     <module>gerrit-common</module>
-    <module>gerrit-ehcache</module>
+    <module>gerrit-cache-h2</module>
     <module>gerrit-httpd</module>
     <module>gerrit-launcher</module>
     <module>gerrit-main</module>
@@ -89,6 +89,7 @@
 
     <module>gerrit-extension-api</module>
     <module>gerrit-plugin-api</module>
+    <module>gerrit-plugin-archetype</module>
 
     <module>gerrit-gwtui</module>
   </modules>
@@ -461,6 +462,12 @@
       </dependency>
 
       <dependency>
+        <groupId>com.google.guava</groupId>
+        <artifactId>guava</artifactId>
+        <version>12.0</version>
+      </dependency>
+
+      <dependency>
         <groupId>gwtorm</groupId>
         <artifactId>gwtorm</artifactId>
         <version>${gwtormVersion}</version>
@@ -553,12 +560,6 @@
       </dependency>
 
       <dependency>
-        <groupId>net.sf.ehcache</groupId>
-        <artifactId>ehcache-core</artifactId>
-        <version>1.7.2</version>
-      </dependency>
-
-      <dependency>
         <groupId>args4j</groupId>
         <artifactId>args4j</artifactId>
         <version>2.0.16</version>
@@ -822,18 +823,24 @@
         <artifactId>PrologCafe</artifactId>
         <version>1.3</version>
       </dependency>
+
+      <dependency>
+        <groupId>org.pegdown</groupId>
+        <artifactId>pegdown</artifactId>
+        <version>1.1.0</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>
 
   <repositories>
     <repository>
-      <id>jgit-repository</id>
-      <url>http://download.eclipse.org/jgit/maven</url>
+      <id>gerrit-maven</id>
+      <url>https://gerrit-maven.commondatastorage.googleapis.com</url>
     </repository>
 
     <repository>
-      <id>gerrit-maven-repository</id>
-      <url>https://gerrit-maven-repository.googlecode.com/svn/</url>
+      <id>jgit-repository</id>
+      <url>http://download.eclipse.org/jgit/maven</url>
     </repository>
 
     <repository>
@@ -850,5 +857,10 @@
       <id>clojars-repo</id>
       <url>http://clojars.org/repo</url>
     </repository>
+
+    <repository>
+      <id>scala-tools</id>
+      <url>http://scala-tools.org/repo-releases</url>
+    </repository>
   </repositories>
 </project>
diff --git a/tools/deploy_api.sh b/tools/deploy_api.sh
index eda841f..e5909e54 100755
--- a/tools/deploy_api.sh
+++ b/tools/deploy_api.sh
@@ -1,5 +1,7 @@
 #!/bin/sh
 
+set -e
+
 SRC=$(ls gerrit-plugin-api/target/gerrit-plugin-api-*-sources.jar)
 VER=${SRC#gerrit-plugin-api/target/gerrit-plugin-api-}
 VER=${VER%-sources.jar}
@@ -15,25 +17,44 @@
 esac
 URL=s3://gerrit-api@commondatastorage.googleapis.com/$type
 
-echo "Deploying API $VER to $URL"
-for module in gerrit-extension-api gerrit-plugin-api
-do
-  mvn deploy:deploy-file \
-    -DgroupId=com.google.gerrit \
-    -DartifactId=$module \
-    -Dversion=$VER \
-    -Dpackaging=jar \
-    -Dfile=$module/target/$module-$VER.jar \
-    -DrepositoryId=gerrit-api-repository \
-    -Durl=$URL
 
-  mvn deploy:deploy-file \
-    -DgroupId=com.google.gerrit \
-    -DartifactId=$module \
-    -Dversion=$VER \
-    -Dpackaging=java-source \
-    -Dfile=$module/target/$module-$VER-sources.jar \
-    -Djava-source=false \
-    -DrepositoryId=gerrit-api-repository \
-    -Durl=$URL
-done
+echo "Deploying $type gerrit-extension-api $VER"
+mvn deploy:deploy-file \
+  -DgroupId=com.google.gerrit \
+  -DartifactId=gerrit-extension-api \
+  -Dversion=$VER \
+  -Dpackaging=jar \
+  -Dfile=gerrit-extension-api/target/gerrit-extension-api-$VER-all.jar \
+  -DrepositoryId=gerrit-api-repository \
+  -Durl=$URL
+
+mvn deploy:deploy-file \
+  -DgroupId=com.google.gerrit \
+  -DartifactId=gerrit-extension-api \
+  -Dversion=$VER \
+  -Dpackaging=java-source \
+  -Dfile=gerrit-extension-api/target/gerrit-extension-api-$VER-all-sources.jar \
+  -Djava-source=false \
+  -DrepositoryId=gerrit-api-repository \
+  -Durl=$URL
+
+
+echo "Deploying $type gerrit-plugin-api $VER"
+mvn deploy:deploy-file \
+  -DgroupId=com.google.gerrit \
+  -DartifactId=gerrit-plugin-api \
+  -Dversion=$VER \
+  -Dpackaging=jar \
+  -Dfile=gerrit-plugin-api/target/gerrit-plugin-api-$VER.jar \
+  -DrepositoryId=gerrit-api-repository \
+  -Durl=$URL
+
+mvn deploy:deploy-file \
+  -DgroupId=com.google.gerrit \
+  -DartifactId=gerrit-plugin-api \
+  -Dversion=$VER \
+  -Dpackaging=java-source \
+  -Dfile=gerrit-plugin-api/target/gerrit-plugin-api-$VER-sources.jar \
+  -Djava-source=false \
+  -DrepositoryId=gerrit-api-repository \
+  -Durl=$URL