Merge "Change style of commit message box display and use UiBinder."
diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt
index 85d1a92..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::
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/config-gerrit.txt b/Documentation/config-gerrit.txt
index 2c050d4..6e50ef4 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -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"`::
 +
@@ -517,6 +518,12 @@
 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].
 
@@ -598,13 +605,6 @@
 +
 Default is true, enabled.
 
-cache.plugin_resources.memoryLimit::
-+
-Number of bytes of memory to use to cache formatted plugin resources,
-such as plugin documentation that has been converted from Markdown to
-HTML. Default is 2 MiB. Common unit suffixes of 'k', 'm', or 'g' are
-supported.
-
 cache.projects.checkFrequency::
 +
 How often project configuration should be checked for update from Git.
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 7484795..f65f7fc 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
@@ -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]
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index e50979a..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>>
diff --git a/ReleaseNotes/ReleaseNotes-2.4.txt b/ReleaseNotes/ReleaseNotes-2.4.txt
index 4f144f3..82f3ed4 100644
--- a/ReleaseNotes/ReleaseNotes-2.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.4.txt
@@ -70,6 +70,11 @@
 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.
@@ -219,6 +224,7 @@
 * 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
 -------------
diff --git a/ReleaseNotes/ReleaseNotes-2.5.txt b/ReleaseNotes/ReleaseNotes-2.5.txt
index 34af3dd..60c4f08 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.txt
@@ -14,3 +14,42 @@
 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/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 c0c317c..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
@@ -40,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());
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/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/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/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/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 97e731b..0000000
--- a/gerrit-ehcache/.settings/org.eclipse.core.resources.prefs
+++ /dev/null
@@ -1,4 +0,0 @@
-#Tue May 15 09:21:09 PDT 2012
-eclipse.preferences.version=1
-encoding//src/main/java=UTF-8
-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 db421ea..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.extensions.events.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-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-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 701e1fc..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
@@ -577,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/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 130241e..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;
@@ -452,13 +319,6 @@
 
     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..3b3d0bb 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
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 dead713..9efea22 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
@@ -111,8 +111,7 @@
       }
     }
 
-    Collections.sort(out.asList(), compare());
-    Collections.sort(in.asList(), compare());
+    Collections.sort(out.asList(), outComparator());
 
     table.updateColumnsForLabels(out, in, done);
     outgoing.display(out);
@@ -121,18 +120,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..52e3bd2 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,6 +68,7 @@
   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 String _sortkey() /*-{ return this._sortkey; }-*/;
@@ -81,6 +94,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 +107,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/PatchSetComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
index 9b53c73..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,7 +27,6 @@
 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;
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 f588044..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
@@ -1386,3 +1386,7 @@
   font-style: italic;
   padding: 2px 6px 1px;
 }
+
+/** PluginListScreen **/
+.pluginsTable {
+}
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-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index 7914991..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)
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 1f26227..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);
         }
       });
     }
@@ -320,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/HttpRequestContext.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
index 4c88240..8ef826b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 class HttpRequestContext implements RequestContext {
   private final WebSession session;
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 852caae..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
@@ -25,6 +25,7 @@
 import com.google.gerrit.httpd.gitweb.GitWebModule;
 import com.google.gerrit.httpd.rpc.UiRpcModule;
 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;
@@ -134,7 +134,7 @@
     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(
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 ee02ef4..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;
@@ -55,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;
 
@@ -76,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);
 
@@ -120,7 +120,7 @@
 
     Val val = new Val(who, refreshCookieAt, remember,
         lastLogin, xsrfToken, expiresAt);
-    self.put(key, val);
+    self.put(key.token, val);
     return val;
   }
 
@@ -141,21 +141,19 @@
   }
 
   Val get(final Key key) {
-    Val val = self.get(key);
+    Val val = self.getIfPresent(key.token);
     if (val != null && val.expiresAt <= now()) {
-      self.remove(key);
+      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) {
@@ -175,18 +173,10 @@
     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;
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 b5c228e..79f9011 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
@@ -16,8 +16,6 @@
 
 import com.google.common.base.Strings;
 import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.cache.Weigher;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
@@ -32,6 +30,7 @@
 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 org.eclipse.jgit.lib.Config;
@@ -57,7 +56,6 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import javax.annotation.Nullable;
 import javax.servlet.FilterChain;
 import javax.servlet.ServletConfig;
 import javax.servlet.ServletException;
@@ -90,22 +88,12 @@
   @Inject
   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 = CacheBuilder.newBuilder()
-      .maximumWeight(cfg.getInt(
-          "cache", "plugin_resources", "memoryLimit",
-          2 * 1024 * 1024))
-      .weigher(new Weigher<ResourceKey, Resource>() {
-        @Override
-        public int weigh(ResourceKey key, Resource value) {
-          return key.weight() + value.weight();
-        }
-      })
-     .build();
+    this.resourceCache = cache;
 
     String sshHost = "review.example.com";
     int sshPort = 29418;
@@ -247,8 +235,8 @@
       if (exists(entry)) {
         sendResource(jar, entry, key, res);
       } else {
-        resourceCache.put(key, NOT_FOUND);
-        NOT_FOUND.send(req, res);
+        resourceCache.put(key, Resource.NOT_FOUND);
+        Resource.NOT_FOUND.send(req, res);
       }
     } else if (file.equals("Documentation")) {
       res.sendRedirect(uri + "/index.html");
@@ -268,12 +256,12 @@
       } else if (exists(entry)) {
         sendResource(jar, entry, key, res);
       } else {
-        resourceCache.put(key, NOT_FOUND);
-        NOT_FOUND.send(req, res);
+        resourceCache.put(key, Resource.NOT_FOUND);
+        Resource.NOT_FOUND.send(req, res);
       }
     } else {
-      resourceCache.put(key, NOT_FOUND);
-      NOT_FOUND.send(req, res);
+      resourceCache.put(key, Resource.NOT_FOUND);
+      Resource.NOT_FOUND.send(req, res);
     }
   }
 
@@ -559,7 +547,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");
@@ -576,99 +564,6 @@
     }
   }
 
-  private static 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 weight() {
-      return 28 + resource.length();
-    }
-
-    @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;
-    }
-  }
-
-  private static abstract class Resource {
-    abstract int weight();
-    abstract void send(HttpServletRequest req, HttpServletResponse res)
-        throws IOException;
-  }
-
-  private static 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 weight() {
-      return 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);
-    }
-  }
-
-  private static final Resource NOT_FOUND = new Resource() {
-    @Override
-    int weight() {
-      return 4;
-    }
-
-    @Override
-    void send(HttpServletRequest req, HttpServletResponse res)
-        throws IOException {
-      noCache(res);
-      res.sendError(HttpServletResponse.SC_NOT_FOUND);
-    }
-  };
-
   private static class WrappedRequest extends HttpServletRequestWrapper {
     private final String contextPath;
 
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 c1c9169..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
@@ -243,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 d9f327d..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);
 
-        AccountGroup owner =
-            groupCache.get(new AccountGroup.NameKey(newOwnerName));
+        GroupReference owner =
+            GroupBackends.findExactSuggestion(groupBackend, newOwnerName);
         if (owner == null) {
           throw new Failure(new NoSuchEntityException());
         }
 
-        group.setOwnerGroupUUID(owner.getGroupUUID());
+        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/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/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/patch/PatchScriptFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
index ac00b8d..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;
@@ -154,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();
     }
@@ -169,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/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/CreateProjectHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java
index 039a301..e0a2f9c 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
@@ -28,6 +28,8 @@
 
 import org.eclipse.jgit.lib.Constants;
 
+import java.util.Collections;
+
 public class CreateProjectHandler extends Handler<VoidResult> {
 
   interface Factory {
@@ -74,7 +76,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/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/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/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-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 c05948d..ff5ee17 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,7 +17,6 @@
 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;
@@ -36,6 +35,7 @@
 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.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;
@@ -157,6 +157,7 @@
 
       manager.start();
       RuntimeShutdown.add(new Runnable() {
+        @Override
         public void run() {
           log.info("caught shutdown, cleaning up");
           if (runId != null) {
@@ -209,7 +210,7 @@
     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 PluginModule());
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 5f0bc80..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;
@@ -100,7 +99,7 @@
 
         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/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-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 1f244bb..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
@@ -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. */
@@ -201,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;
 
@@ -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/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 ceb3c55..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>
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 2ba1304..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;
@@ -80,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;
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/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/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 ea388c7..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)
@@ -45,7 +50,7 @@
 
     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);
       }
@@ -67,22 +72,22 @@
   }
 
   private final CurrentUser user;
-  private final AccountGroup group;
+  private final GroupDescription.Basic group;
   private Boolean isOwner;
 
-  GroupControl(CurrentUser who, AccountGroup gc) {
+  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()
@@ -91,8 +96,11 @@
   }
 
   public boolean isOwner() {
-    if (isOwner == null) {
-      AccountGroup.UUID ownerUUID = group.getOwnerGroupUUID();
+    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 2d455e9..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,6 +14,7 @@
 
 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;
@@ -40,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;
 
@@ -49,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();
 
@@ -64,10 +68,10 @@
   @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);
-    AccountGroup ownerGroup = groupCache.get(group.getOwnerGroupUUID());
+    GroupDescription.Basic ownerGroup = groupBackend.get(group.getOwnerGroupUUID());
     if (ownerGroup != null) {
       detail.setOwnerGroup(GroupReference.forGroup(ownerGroup));
     }
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/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/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 2875920..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,9 +16,11 @@
 
 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;
@@ -36,11 +38,15 @@
 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;
@@ -68,7 +74,7 @@
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.workflow.FunctionState;
 import com.google.inject.Inject;
-import com.google.inject.servlet.RequestScoped;
+import com.google.inject.TypeLiteral;
 
 import org.apache.velocity.runtime.RuntimeInstance;
 import org.eclipse.jgit.lib.Config;
@@ -128,12 +134,18 @@
     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);
@@ -156,6 +168,7 @@
     factory(FunctionState.Factory.class);
 
     bind(GitReferenceUpdated.class);
+    DynamicMap.mapOf(binder(), new TypeLiteral<Cache<?, ?>>() {});
     DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
     DynamicSet.setOf(binder(), NewProjectCreatedListener.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 5d6ecc3..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;
@@ -79,7 +77,6 @@
 
     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/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/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/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/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 6ddcb39..4db9409 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
@@ -900,6 +900,7 @@
 
     final ObjectId id = commit(m, mergeCommit);
     final CodeReviewCommit newCommit = (CodeReviewCommit) rw.parseCommit(id);
+    final Change oldChange = n.change;
 
     n.change =
         db.changes().atomicUpdate(n.change.getId(),
@@ -929,6 +930,9 @@
               }
             });
 
+    this.submitted.remove(oldChange);
+    this.submitted.add(n.change);
+
     if (approvalList != null) {
       for (PatchSetApproval a : approvalList) {
         db.patchSetApprovals().insert(
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 75c9bd1..507f064 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
@@ -21,6 +21,7 @@
 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;
@@ -31,8 +32,9 @@
 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.server.account.GroupBackend;
 import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.common.data.RefConfigSection;
 import com.google.gerrit.server.mail.Address;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -233,13 +235,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());
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 b72b162..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;
@@ -80,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;
@@ -456,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();
@@ -474,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)) {
@@ -573,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;
@@ -621,7 +624,7 @@
           continue;
       }
 
-      if (cmd.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) {
+      if (cmd.getResult() != NOT_ATTEMPTED) {
         continue;
       }
 
@@ -687,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");
@@ -702,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);
@@ -735,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");
@@ -762,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.");
     }
   }
@@ -877,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;
     }
@@ -1052,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) {
@@ -1080,7 +1091,7 @@
       }
       newProgress.update(1);
     }
-    newChange.setResult(ReceiveCommand.Result.OK);
+    newChange.setResult(OK);
   }
 
   private static boolean isValidChangeId(String idStr) {
@@ -1207,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.");
@@ -1460,7 +1471,7 @@
     }
     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() {
@@ -1599,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);
     }
   }
@@ -2043,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/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..bf0d75d 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(db.getAllRefs()).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/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/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
index e1a1725..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
@@ -16,6 +16,7 @@
 
 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;
@@ -35,6 +36,7 @@
 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;
@@ -270,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. */
@@ -387,7 +388,8 @@
   private void add(Watchers matching, NotifyConfig nc, Project.NameKey project)
       throws OrmException, QueryParseException {
     for (GroupReference ref : nc.getGroups()) {
-      AccountGroup group = args.groupCache.get(ref.getUUID());
+      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",
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 eaf9eee..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
@@ -21,6 +21,7 @@
 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;
 
@@ -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,6 +158,18 @@
     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());
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 fa49b06..e6c96a3 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
@@ -20,7 +20,7 @@
 import com.google.gerrit.server.IdentifiedUser.GenericFactory;
 import com.google.gerrit.server.account.AccountCache;
 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.config.AllProjectsName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -39,7 +39,7 @@
 class EmailArguments {
   final GitRepositoryManager server;
   final ProjectCache projectCache;
-  final GroupCache groupCache;
+  final GroupBackend groupBackend;
   final AccountCache accountCache;
   final PatchListCache patchListCache;
   final FromAddressGenerator fromAddressGenerator;
@@ -58,7 +58,7 @@
 
   @Inject
   EmailArguments(GitRepositoryManager server, ProjectCache projectCache,
-      GroupCache groupCache, AccountCache accountCache,
+      GroupBackend groupBackend, AccountCache accountCache,
       PatchListCache patchListCache, FromAddressGenerator fromAddressGenerator,
       EmailSender emailSender, PatchSetInfoFactory patchSetInfoFactory,
       GenericFactory identifiedUserFactory,
@@ -71,7 +71,7 @@
       RuntimeInstance velocityRuntime) {
     this.server = server;
     this.projectCache = projectCache;
-    this.groupCache = groupCache;
+    this.groupBackend = groupBackend;
     this.accountCache = accountCache;
     this.patchListCache = patchListCache;
     this.fromAddressGenerator = fromAddressGenerator;
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..f1f83bb 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,22 @@
 
 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.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.util.RawParseUtils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
@@ -28,6 +40,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 +100,41 @@
     }
     return host;
   }
+
+  /** Show patch set as unified difference.  */
+  public String getUnifiedDiff() {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    Repository repo = getRepository();
+    if (repo != null) {
+      DiffFormatter df = new DiffFormatter(out);
+      try {
+        PatchList patchList = getPatchList();
+        if (patchList.getOldId() != null) {
+          df.setRepository(repo);
+          df.setDetectRenames(true);
+          df.format(patchList.getOldId(), patchList.getNewId());
+        }
+      } catch (PatchListNotAvailableException e) {
+        log.error("Cannot format patch", e);
+      } catch (IOException e) {
+        log.error("Cannot format patch", e);
+      } finally {
+        df.release();
+        repo.close();
+      }
+    }
+    return RawParseUtils.decode(out.toByteArray());
+  }
+
+  private Repository getRepository() {
+    try {
+      return args.server.openRepository(change.getProject());
+    } catch (RepositoryNotFoundException e) {
+      log.error("Cannot open repository", e);
+      return null;
+    } catch (IOException e) {
+      log.error("Cannot open repository", e);
+      return null;
+    }
+  }
 }
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..d6e84bb 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);
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/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/PluginGuiceEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
index 3aa259e..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
@@ -385,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/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index f232c5c..64f7d97 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
@@ -292,13 +292,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 +499,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 577a92d..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,6 +15,7 @@
 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;
@@ -26,7 +27,7 @@
 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;
@@ -51,6 +52,8 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.Set;
 
 
@@ -71,7 +74,7 @@
   private final PersonIdent serverIdent;
   private CreateProjectArgs createProjectArgs;
   private ProjectCache projectCache;
-  private GroupCache groupCache;
+  private GroupBackend groupBackend;
   private MetaDataUpdate.User metaDataUpdateFactory;
 
   @Inject
@@ -80,8 +83,8 @@
       GitReferenceUpdated referenceUpdated,
       DynamicSet<NewProjectCreatedListener> createdListener,
       ReviewDb db,
-      @GerritPersonIdent PersonIdent personIdent, final GroupCache groupCache,
-      final MetaDataUpdate.User metaDataUpdateFactory,
+      @GerritPersonIdent PersonIdent personIdent, GroupBackend groupBackend,
+      MetaDataUpdate.User metaDataUpdateFactory,
       @Assisted CreateProjectArgs createPArgs, ProjectCache pCache) {
     this.projectOwnerGroups = pOwnerGroups;
     this.currentUser = identifiedUser;
@@ -91,7 +94,7 @@
     this.serverIdent = personIdent;
     this.createProjectArgs = createPArgs;
     this.projectCache = pCache;
-    this.groupCache = groupCache;
+    this.groupBackend = groupBackend;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
   }
 
@@ -101,7 +104,7 @@
     try {
       final String head =
           createProjectArgs.permissionsOnly ? GitRepositoryManager.REF_CONFIG
-              : createProjectArgs.branch;
+              : createProjectArgs.branch.get(0);
       final Repository repo = repoManager.createRepository(nameKey);
       try {
         NewProjectCreatedListener.Event event = new NewProjectCreatedListener.Event() {
@@ -127,7 +130,7 @@
 
         if (!createProjectArgs.permissionsOnly
             && createProjectArgs.createEmptyCommit) {
-          createEmptyCommit(repo, nameKey, createProjectArgs.branch);
+          createEmptyCommits(repo, nameKey, createProjectArgs.branch);
         }
       } finally {
         repo.close();
@@ -185,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));
           }
@@ -235,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();
@@ -260,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:
-          referenceUpdated.fire(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/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 3b1f55c..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,18 +105,26 @@
    * @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());
     }
   }
 
@@ -118,10 +132,11 @@
   public void remove(final Project p) {
     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.remove(p.getNameKey());
       list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n));
+    } catch (ExecutionException e) {
+      log.warn("Cannot list avaliable projects", e);
     } finally {
       listLock.unlock();
     }
@@ -132,10 +147,11 @@
   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();
     }
@@ -143,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() {
@@ -196,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;
 
@@ -207,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();
       }
     }
   }
@@ -231,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
@@ -240,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 67d91d5..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
@@ -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;
@@ -196,8 +197,12 @@
 
   /** Can this user see all the refs in this projects? */
   public boolean allRefsAreVisible() {
+    return allRefsAreVisibleExcept(Collections.<String> emptySet());
+  }
+
+  public boolean allRefsAreVisibleExcept(Set<String> except) {
     return user instanceof InternalUser
-        || canPerformOnAllRefs(Permission.READ);
+        || canPerformOnAllRefs(Permission.READ, except);
   }
 
   /** Is this user a project owner? Ownership does not imply {@link #isVisible()} */
@@ -347,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)) {
@@ -358,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/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/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/ListChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ListChanges.java
index 6f9094a..adf4f19 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
@@ -205,6 +205,9 @@
   }
 
   private AccountAttribute asAccountAttribute(Account.Id user) {
+    if (user == null) {
+      return null;
+    }
     AccountAttribute a = accounts.get(user);
     if (a == null) {
       a = new AccountAttribute();
@@ -226,7 +229,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 +246,7 @@
               n.rejected = asAccountAttribute(r.appliedBy);
               break;
           }
+          n.optional = n._status == SubmitRecord.Label.Status.MAY ? true : null;
           labels.put(r.label, n);
         }
       }
@@ -314,5 +318,6 @@
     AccountAttribute recommended;
     AccountAttribute disliked;
     Short value;
+    Boolean optional;
   }
 }
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 08d94af..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
@@ -252,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 9c89c73..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_67> C = Schema_67.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/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/GuiceRequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
index cc3c2f7..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;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
index 88240ca..b411512 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
@@ -21,7 +21,6 @@
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Module;
-import com.google.inject.Provider;
 import com.google.inject.Provides;
 import com.google.inject.ProvisionException;
 import com.google.inject.name.Named;
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/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/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..3e3257f 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
@@ -52,3 +52,6 @@
 #if($email.sshHost)
   git pull ssh://$email.sshHost/$projectName $patchSet.refName
 #end
+
+## It is possible to include the patch as a unified diff in the email:
+#$email.UnifiedDiff
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/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 469dafe..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;
@@ -321,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/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/AliasCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
index d28d102..9582c93 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
@@ -16,6 +16,7 @@
 
 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;
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 f0810ba..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
@@ -16,9 +16,10 @@
 
 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;
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/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
index bcb1d9f..f800783 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
@@ -23,8 +23,7 @@
 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;
@@ -40,18 +39,8 @@
 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;
@@ -61,7 +50,6 @@
 import org.apache.sshd.server.PublickeyAuthenticator;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
-import org.kohsuke.args4j.spi.OptionHandler;
 
 import java.net.SocketAddress;
 import java.util.Map;
@@ -85,7 +73,7 @@
     bind(SshScope.class).in(SINGLETON);
 
     configureRequestScope();
-    configureCmdLineParser();
+    install(new CmdLineParserModule());
     configureAliases();
 
     install(SshKeyCacheImpl.module());
@@ -157,22 +145,4 @@
 
     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/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..5dff6b0 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,7 +24,6 @@
 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;
 
@@ -79,7 +79,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/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 644cf13..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,39 +14,29 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
 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 = Lists.newArrayList(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 d60465c..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
@@ -17,8 +17,8 @@
 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;
 
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/ShowCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
index 97a0d86..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.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/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 58d6116..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;
@@ -200,7 +200,7 @@
     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 PluginModule());
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 ad32cbd..2f5a296 100644
--- a/pom.xml
+++ b/pom.xml
@@ -46,7 +46,7 @@
   </issueManagement>
 
   <properties>
-    <jgitVersion>1.3.0.201202151440-r.140-g8c73245</jgitVersion>
+    <jgitVersion>1.3.0.201202151440-r.190-g65f6e06</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>
@@ -833,13 +834,13 @@
 
   <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>